mirror of
https://github.com/rs/zerolog
synced 2026-06-08 17:13:30 +00:00
Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13966551e7 | |||
| 4b65a2f6f6 | |||
| b83579670f | |||
| 134caf82aa | |||
| e133b6a517 | |||
| 82017d8fff | |||
| 2f5b8a91be | |||
| d64c9a7138 | |||
| a0d61dc2c7 | |||
| f6fbd330be | |||
| 2094837a2c | |||
| 0cf9361666 | |||
| 0d69d7537a | |||
| 3d20da0255 | |||
| 4685edb80b | |||
| 05348ba6f6 | |||
| a728d746c0 | |||
| a64148507a | |||
| 42f33fff03 | |||
| 8148645974 | |||
| 5391dd7c34 | |||
| 9dacc014f3 | |||
| a21d6107dc | |||
| db9d1bebd9 | |||
| 5f4b880a01 | |||
| ffb27080ca | |||
| cc4dde7383 | |||
| 04ea0f4371 | |||
| 039860087c | |||
| 1869fa55be | |||
| 31e7995c5b | |||
| 582f820cf0 | |||
| 6abadab488 | |||
| c78e50e2da | |||
| 8582bed24f | |||
| 7d9db06a53 | |||
| e5aa7e3627 | |||
| 0efa414907 | |||
| eb081e1fa2 | |||
| 2d899f0cf9 | |||
| 74cf37a396 | |||
| e5edd4b8ec | |||
| 582007f21d | |||
| 54ebf468e5 | |||
| bda298df4a | |||
| 0d16f63a8a | |||
| dfd022fdfd | |||
| 159cb37bb9 | |||
| bd2896587d | |||
| af58707cd9 | |||
| 9e60b1cb8e | |||
| 147ae65350 | |||
| 4d78dc5ffa | |||
| c1ab4ed9bf | |||
| 417580d1ce | |||
| 602e90aeea | |||
| a9ec232b3e | |||
| 3e8ae07aba | |||
| 7fa45a4dda | |||
| 93fb5cb215 | |||
| 83e03c75d9 | |||
| bb14b8b9de | |||
| e7034c2572 | |||
| c4046fe2cb | |||
| 507a426bf8 | |||
| 8834256667 | |||
| 2c1cbecf52 | |||
| 6ed7439d9c | |||
| f115bfade5 | |||
| f70fcca1ab | |||
| 6e8841b038 | |||
| 11f895c15e | |||
| ed609e7fe6 | |||
| 9e34cb475c | |||
| 8344fc0c93 | |||
| 4cb8cc5622 | |||
| ae9b265137 | |||
| 1bac5cca50 | |||
| b81cc57e5d | |||
| ad77222f68 | |||
| 95cf29c88c | |||
| 802c88f065 | |||
| 158e4ad5c3 | |||
| 7d5aa987d0 | |||
| 70ac648f5c | |||
| 9c29f785f9 | |||
| 06ec071bc1 | |||
| 873cbf13ee | |||
| 61485f3857 | |||
| 9070d49a1a | |||
| b662f088b9 | |||
| 4612e098d2 | |||
| 8981d80ed3 | |||
| 927516bcf1 | |||
| a712f61936 | |||
| 64a5863c5e | |||
| 1f50797d7d | |||
| 902d72012d | |||
| 4fff5db29c | |||
| 762546b5c6 | |||
| fa9bf3742a | |||
| db22191211 | |||
| 164f7aa1a6 | |||
| 3543e9d94b | |||
| 5bdc93f7eb | |||
| e3027a5732 | |||
| a9a8199d2d | |||
| 89617ff99b | |||
| 55aaf043cf | |||
| 315967f32d | |||
| e218d18951 | |||
| c2b9d0e2de | |||
| d894f123bc | |||
| 4099072c03 | |||
| 4c85986254 | |||
| 43be301386 | |||
| afdf9978ec | |||
| 14d6629e41 | |||
| dbdec88d16 | |||
| b30730fab2 | |||
| 68a6bd49b5 | |||
| 5c08a2724f | |||
| 60f57432ed | |||
| 2a13872817 | |||
| a4ec5e4cdd | |||
| e9344a8c50 | |||
| 263b0bde36 | |||
| 588a61c2df | |||
| 361cdf616a | |||
| fc26014bd4 | |||
| 025f9f1819 | |||
| 3efdd82416 | |||
| c0c2e11fc3 | |||
| 665519c4da | |||
| 0c8d3c0b10 | |||
| 791ca15d99 | |||
| 0d2f0be4ee | |||
| 6973188a74 | |||
| 78448ee023 | |||
| 197adb44cc | |||
| 07d347e658 | |||
| 6f8a5f9ccb | |||
| 9b9fc5c6b7 | |||
| 65adfd88ec | |||
| cdd74175d0 | |||
| fe931004ff | |||
| be6f6fd8f1 | |||
| d92a906fca | |||
| fad20d83d3 | |||
| 164ec91b0c | |||
| c1533bd5f8 | |||
| 0872592ea2 | |||
| da1cb97713 | |||
| 117cb53bc6 | |||
| e05605c215 | |||
| 6ed1127758 | |||
| 47a03bc5eb | |||
| ffbd37b8d7 | |||
| 3c3b4a354e | |||
| 4de2fcc128 | |||
| f09463fbe1 | |||
| 19c98f6d3e | |||
| 0f923d7926 | |||
| 582f0cf0e3 | |||
| 98f889fcde | |||
| f85e803cc5 | |||
| f2cc3cf8b7 | |||
| 72b5b1ea58 | |||
| 7ccd4c940b | |||
| 7a3aa8746f | |||
| a8f5328bb7 | |||
| 4f50ae2ed0 | |||
| 0aba2e4ae5 | |||
| a3b272d512 | |||
| b7e31f4fe7 | |||
| 29d8dac5e8 | |||
| cac3894be4 | |||
| 9336c4d0ed | |||
| e11d470c08 | |||
| 72acd6cfe8 | |||
| 9e51190d47 | |||
| 1b763497ee | |||
| 7248ae2fb4 | |||
| 7825d86337 | |||
| 63767a55ec | |||
| f83de79b81 | |||
| 663cbb4c84 | |||
| e027a834ab | |||
| 50ffd2b67d | |||
| e86e8f2f49 | |||
| de5a95dced | |||
| 14dcf38e7d | |||
| a06edf20d7 | |||
| fe394c81ce | |||
| 1c32ee06a7 | |||
| d9df1802de | |||
| f7c93dce1c | |||
| 68a3fd989d | |||
| 505b18daf2 | |||
| 65ed30bfb0 | |||
| cb951d468e | |||
| d2a97b366b | |||
| f1dd50b8c6 | |||
| 686705b4f0 | |||
| 5d9d7660cc | |||
| 54e95fe699 | |||
| e709c5d91e | |||
| 4502cc1942 | |||
| 43d05e8ddf | |||
| 5861452d64 | |||
| 7592fcbe60 | |||
| 2e41c37ac4 | |||
| 19e454b4c7 | |||
| 33a4561a07 | |||
| 06599535fa | |||
| 61d1749124 | |||
| b806a5ecbe | |||
| a7f9fc2a17 | |||
| 42d101e9f7 | |||
| 77a1695358 | |||
| 60d4b07b61 | |||
| 9938a23cba | |||
| 1a2c7daec4 | |||
| ffd0e9625d | |||
| ad0401954a | |||
| acf3980132 | |||
| 33f552ec3d | |||
| 2a07580c27 | |||
| 3e85c4b21c | |||
| 509d727fba | |||
| 651d361cfe | |||
| 8e5449ab35 | |||
| 6d6350a511 | |||
| 299ff038c1 | |||
| 4daee2b758 | |||
| aa55558e4c | |||
| c482b20623 | |||
| 7179aeef58 | |||
| 8747b7b3a5 | |||
| 848482bc3d | |||
| 7bcaa1a99e | |||
| 8e30c71369 | |||
| 3f112dae87 | |||
| a4c54e5d8b | |||
| 96f91bb4f5 | |||
| 51c79ca476 | |||
| e7627a4f73 | |||
| baa31cfa85 | |||
| 8e36cbf881 | |||
| 20ad1708e7 | |||
| 338f9bc140 |
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -0,0 +1,36 @@
|
||||
on: [push, pull_request]
|
||||
name: Test
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.23.x, 1.24.x]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Test
|
||||
run: go test -race -bench . -benchmem ./...
|
||||
- name: Test CBOR
|
||||
run: go test -tags binary_log -race ./...
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update coverage report
|
||||
uses: ncruces/go-coverage-report@main
|
||||
with:
|
||||
report: 'true'
|
||||
chart: 'true'
|
||||
amend: 'true'
|
||||
continue-on-error: true
|
||||
@@ -6,6 +6,7 @@
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
tmp
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
@@ -22,3 +23,5 @@ _testmain.go
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
coverage.out
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- "1.7"
|
||||
- "1.8"
|
||||
- "1.9"
|
||||
- "1.10"
|
||||
- "master"
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: "master"
|
||||
script:
|
||||
- go test -v -race -cpu=1,2,4 -bench . -benchmem ./...
|
||||
- go test -v -tags binary_log -race -cpu=1,2,4 -bench . -benchmem ./...
|
||||
@@ -0,0 +1,43 @@
|
||||
# Contributing to Zerolog
|
||||
|
||||
Thank you for your interest in contributing to **Zerolog**!
|
||||
|
||||
Zerolog is a **feature-complete**, high-performance logging library designed to be **lean** and **non-bloated**. The focus of ongoing development is on **bug fixes**, **performance improvements**, and **modernization efforts** (such as keeping up with Go best practices and compatibility with newer Go versions).
|
||||
|
||||
## What We're Looking For
|
||||
|
||||
We welcome contributions in the following areas:
|
||||
|
||||
- **Bug Fixes**: If you find an issue or unexpected behavior, please open an issue and/or submit a fix.
|
||||
- **Performance Optimizations**: Improvements that reduce memory usage, allocation count, or CPU cycles without introducing complexity are appreciated.
|
||||
- **Modernization**: Compatibility updates for newer Go versions or idiomatic improvements that do not increase library size or complexity.
|
||||
- **Documentation Enhancements**: Corrections, clarifications, and improvements to documentation or code comments.
|
||||
|
||||
## What We're *Not* Looking For
|
||||
|
||||
Zerolog is intended to remain **minimalistic and efficient**. Therefore, we are **not accepting**:
|
||||
|
||||
- New features that add optional behaviors or extend API surface area.
|
||||
- Built-in support for frameworks or external systems (e.g., bindings, integrations).
|
||||
- General-purpose abstractions or configuration helpers.
|
||||
|
||||
If you're unsure whether a change aligns with the project's philosophy, feel free to open an issue for discussion before submitting a PR.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
1. **Fork the repository**
|
||||
2. **Create a branch** for your fix or improvement
|
||||
3. **Write tests** to cover your changes
|
||||
4. Ensure `go test ./...` passes
|
||||
5. Run `go fmt` and `go vet` to ensure code consistency
|
||||
6. **Submit a pull request** with a clear explanation of the motivation and impact
|
||||
|
||||
## Code Style
|
||||
|
||||
- Keep the code simple, efficient, and idiomatic.
|
||||
- Avoid introducing new dependencies.
|
||||
- Preserve backwards compatibility unless explicitly discussed.
|
||||
|
||||
---
|
||||
|
||||
We appreciate your effort in helping us keep Zerolog fast, minimal, and reliable!
|
||||
@@ -1,6 +1,6 @@
|
||||
# Zero Allocation JSON Logger
|
||||
|
||||
[](https://godoc.org/github.com/rs/zerolog) [](https://raw.githubusercontent.com/rs/zerolog/master/LICENSE) [](https://travis-ci.org/rs/zerolog) [](http://gocover.io/github.com/rs/zerolog)
|
||||
[](https://godoc.org/github.com/rs/zerolog) [](https://raw.githubusercontent.com/rs/zerolog/master/LICENSE) [](https://github.com/rs/zerolog/actions/workflows/test.yml) [](https://raw.githack.com/wiki/rs/zerolog/coverage.html)
|
||||
|
||||
The zerolog package provides a fast and simple logger dedicated to JSON output.
|
||||
|
||||
@@ -8,7 +8,7 @@ Zerolog's API is designed to provide both a great developer experience and stunn
|
||||
|
||||
Uber's [zap](https://godoc.org/go.uber.org/zap) library pioneered this approach. Zerolog is taking this concept to the next level with a simpler to use API and even better performance.
|
||||
|
||||
To keep the code base and the API simple, zerolog focuses on efficient structured logging only. Pretty logging on the console is made possible using the provided (but inefficient) `zerolog.ConsoleWriter`.
|
||||
To keep the code base and the API simple, zerolog focuses on efficient structured logging only. Pretty logging on the console is made possible using the provided (but inefficient) [`zerolog.ConsoleWriter`](#pretty-logging).
|
||||
|
||||

|
||||
|
||||
@@ -18,20 +18,22 @@ Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog)
|
||||
|
||||
## Features
|
||||
|
||||
* Blazing fast
|
||||
* Low to zero allocation
|
||||
* Level logging
|
||||
* Sampling
|
||||
* Hooks
|
||||
* Contextual fields
|
||||
* `context.Context` integration
|
||||
* `net/http` helpers
|
||||
* JSON and CBOR encoding formats
|
||||
* Pretty logging for development
|
||||
- [Blazing fast](#benchmarks)
|
||||
- [Low to zero allocation](#benchmarks)
|
||||
- [Leveled logging](#leveled-logging)
|
||||
- [Sampling](#log-sampling)
|
||||
- [Hooks](#hooks)
|
||||
- [Contextual fields](#contextual-logging)
|
||||
- [`context.Context` integration](#contextcontext-integration)
|
||||
- [Integration with `net/http`](#integration-with-nethttp)
|
||||
- [JSON and CBOR encoding formats](#binary-encoding)
|
||||
- [Pretty logging for development](#pretty-logging)
|
||||
- [Error Logging (with optional Stacktrace)](#error-logging)
|
||||
- [`log/slog` integration](#integration-with-logslog)
|
||||
|
||||
## Installation
|
||||
|
||||
```go
|
||||
```bash
|
||||
go get -u github.com/rs/zerolog/log
|
||||
```
|
||||
|
||||
@@ -51,17 +53,16 @@ import (
|
||||
|
||||
func main() {
|
||||
// UNIX Time is faster and smaller than most timestamps
|
||||
// If you set zerolog.TimeFieldFormat to an empty string,
|
||||
// logs will write with UNIX time
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Print("hello world")
|
||||
}
|
||||
|
||||
// Output: {"time":1516134303,"level":"debug","message":"hello world"}
|
||||
```
|
||||
> Note: By default log writes to `os.Stderr`
|
||||
> Note: The default log level for `log.Print` is *debug*
|
||||
|
||||
> Note: By default log writes to `os.Stderr`
|
||||
> Note: The default log level for `log.Print` is _trace_
|
||||
|
||||
### Contextual Logging
|
||||
|
||||
@@ -76,15 +77,20 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Debug().
|
||||
Str("Scale", "833 cents").
|
||||
Float64("Interval", 833.09).
|
||||
Msg("Fibonacci is everywhere")
|
||||
|
||||
log.Debug().
|
||||
Str("Name", "Tom").
|
||||
Send()
|
||||
}
|
||||
|
||||
// Output: {"time":1524104936,"level":"debug","Scale":"833 cents","Interval":833.09,"message":"Fibonacci is everywhere"}
|
||||
// Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}
|
||||
// Output: {"level":"debug","Name":"Tom","time":1562212768}
|
||||
```
|
||||
|
||||
> You'll note in the above example that when adding contextual fields, the fields are strongly typed. You can find the full list of supported fields [here](#standard-types)
|
||||
@@ -102,7 +108,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Info().Msg("hello world")
|
||||
}
|
||||
@@ -114,14 +120,15 @@ func main() {
|
||||
|
||||
**zerolog** allows for logging at the following levels (from highest to lowest):
|
||||
|
||||
* panic (`zerolog.PanicLevel`, 5)
|
||||
* fatal (`zerolog.FatalLevel`, 4)
|
||||
* error (`zerolog.ErrorLevel`, 3)
|
||||
* warn (`zerolog.WarnLevel`, 2)
|
||||
* info (`zerolog.InfoLevel`, 1)
|
||||
* debug (`zerolog.DebugLevel`, 0)
|
||||
- panic (`zerolog.PanicLevel`, 5)
|
||||
- fatal (`zerolog.FatalLevel`, 4)
|
||||
- error (`zerolog.ErrorLevel`, 3)
|
||||
- warn (`zerolog.WarnLevel`, 2)
|
||||
- info (`zerolog.InfoLevel`, 1)
|
||||
- debug (`zerolog.DebugLevel`, 0)
|
||||
- trace (`zerolog.TraceLevel`, -1)
|
||||
|
||||
You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
|
||||
You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
|
||||
|
||||
#### Setting Global Log Level
|
||||
|
||||
@@ -138,7 +145,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
debug := flag.Bool("debug", false, "sets log level to debug")
|
||||
|
||||
flag.Parse()
|
||||
@@ -189,7 +196,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
@@ -199,6 +206,80 @@ func main() {
|
||||
// Output: {"time":1494567715,"foo":"bar"}
|
||||
```
|
||||
|
||||
### Error Logging
|
||||
|
||||
You can log errors using the `Err` method
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
err := errors.New("seems we have an error here")
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
// Output: {"level":"error","error":"seems we have an error here","time":1609085256}
|
||||
```
|
||||
|
||||
> The default field name for errors is `error`, you can change this by setting `zerolog.ErrorFieldName` to meet your needs.
|
||||
|
||||
#### Error Logging with Stacktrace
|
||||
|
||||
Using `github.com/pkg/errors`, you can add a formatted stacktrace to your errors.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
err := outer()
|
||||
log.Error().Stack().Err(err).Msg("")
|
||||
}
|
||||
|
||||
func inner() error {
|
||||
return errors.New("seems we have an error here")
|
||||
}
|
||||
|
||||
func middle() error {
|
||||
err := inner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outer() error {
|
||||
err := middle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Output: {"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683}
|
||||
```
|
||||
|
||||
> zerolog.ErrorStackMarshaler must be set in order for the stack to output anything.
|
||||
|
||||
#### Logging Fatal Messages
|
||||
|
||||
```go
|
||||
@@ -215,7 +296,7 @@ func main() {
|
||||
err := errors.New("A repo man spends his life getting into tense situations")
|
||||
service := "myservice"
|
||||
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
@@ -252,14 +333,69 @@ sublogger.Info().Msg("hello world")
|
||||
|
||||
### Pretty logging
|
||||
|
||||
To log a human-friendly, colorized output, use `zerolog.ConsoleWriter`:
|
||||
|
||||
```go
|
||||
if isConsole {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
}
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
log.Info().Str("foo", "bar").Msg("Hello world")
|
||||
|
||||
// Output: 1494567715 |INFO| Hello world foo=bar
|
||||
// Output: 3:04PM INF Hello World foo=bar
|
||||
```
|
||||
|
||||
To customize the configuration and formatting:
|
||||
|
||||
```go
|
||||
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
|
||||
output.FormatLevel = func(i interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||
}
|
||||
output.FormatMessage = func(i interface{}) string {
|
||||
return fmt.Sprintf("***%s****", i)
|
||||
}
|
||||
output.FormatFieldName = func(i interface{}) string {
|
||||
return fmt.Sprintf("%s:", i)
|
||||
}
|
||||
output.FormatFieldValue = func(i interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf("%s", i))
|
||||
}
|
||||
|
||||
log := zerolog.New(output).With().Timestamp().Logger()
|
||||
|
||||
log.Info().Str("foo", "bar").Msg("Hello World")
|
||||
|
||||
// Output: 2006-01-02T15:04:05Z07:00 | INFO | ***Hello World**** foo:BAR
|
||||
```
|
||||
|
||||
To use custom advanced formatting:
|
||||
|
||||
```go
|
||||
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true,
|
||||
PartsOrder: []string{"level", "one", "two", "three", "message"},
|
||||
FieldsExclude: []string{"one", "two", "three"}}
|
||||
output.FormatLevel = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%-6s", i)) }
|
||||
output.FormatFieldName = func(i interface{}) string { return fmt.Sprintf("%s:", i) }
|
||||
output.FormatPartValueByName = func(i interface{}, s string) string {
|
||||
var ret string
|
||||
switch s {
|
||||
case "one":
|
||||
ret = strings.ToUpper(fmt.Sprintf("%s", i))
|
||||
case "two":
|
||||
ret = strings.ToLower(fmt.Sprintf("%s", i))
|
||||
case "three":
|
||||
ret = strings.ToLower(fmt.Sprintf("(%s)", i))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
log := zerolog.New(output)
|
||||
|
||||
log.Info().Str("foo", "bar").
|
||||
Str("two", "TEST_TWO").
|
||||
Str("one", "test_one").
|
||||
Str("three", "test_three").
|
||||
Msg("Hello World")
|
||||
|
||||
// Output: INFO TEST_ONE test_two (test_three) Hello World foo:bar
|
||||
```
|
||||
|
||||
### Sub dictionary
|
||||
@@ -295,6 +431,8 @@ log.Logger = log.With().Str("foo", "bar").Logger()
|
||||
|
||||
### Add file and line number to log
|
||||
|
||||
Equivalent of `Llongfile`:
|
||||
|
||||
```go
|
||||
log.Logger = log.With().Caller().Logger()
|
||||
log.Info().Msg("hello world")
|
||||
@@ -302,16 +440,27 @@ log.Info().Msg("hello world")
|
||||
// Output: {"level": "info", "message": "hello world", "caller": "/go/src/your_project/some_file:21"}
|
||||
```
|
||||
|
||||
Equivalent of `Lshortfile`:
|
||||
|
||||
```go
|
||||
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
|
||||
return filepath.Base(file) + ":" + strconv.Itoa(line)
|
||||
}
|
||||
log.Logger = log.With().Caller().Logger()
|
||||
log.Info().Msg("hello world")
|
||||
|
||||
// Output: {"level": "info", "message": "hello world", "caller": "some_file:21"}
|
||||
```
|
||||
|
||||
### Thread-safe, lock-free, non-blocking writer
|
||||
|
||||
If your writer might be slow or not thread-safe and you need your log producers to never get slowed down by a slow writer, you can use a `diode.Writer` as follow:
|
||||
If your writer might be slow or not thread-safe and you need your log producers to never get slowed down by a slow writer, you can use a `diode.Writer` as follows:
|
||||
|
||||
```go
|
||||
wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("Logger Dropped %d messages", missed)
|
||||
})
|
||||
log := zerolog.New(w)
|
||||
fmt.Printf("Logger Dropped %d messages", missed)
|
||||
})
|
||||
log := zerolog.New(wr)
|
||||
log.Print("test")
|
||||
```
|
||||
|
||||
@@ -386,6 +535,58 @@ stdlog.Print("hello world")
|
||||
// Output: {"foo":"bar","message":"hello world"}
|
||||
```
|
||||
|
||||
### context.Context integration
|
||||
|
||||
Go contexts are commonly passed throughout Go code, and this can help you pass
|
||||
your Logger into places it might otherwise be hard to inject. The `Logger`
|
||||
instance may be attached to Go context (`context.Context`) using
|
||||
`Logger.WithContext(ctx)` and extracted from it using `zerolog.Ctx(ctx)`.
|
||||
For example:
|
||||
|
||||
```go
|
||||
func f() {
|
||||
logger := zerolog.New(os.Stdout)
|
||||
ctx := context.Background()
|
||||
|
||||
// Attach the Logger to the context.Context
|
||||
ctx = logger.WithContext(ctx)
|
||||
someFunc(ctx)
|
||||
}
|
||||
|
||||
func someFunc(ctx context.Context) {
|
||||
// Get Logger from the go Context. if it's nil, then
|
||||
// `zerolog.DefaultContextLogger` is returned, if
|
||||
// `DefaultContextLogger` is nil, then a disabled logger is returned.
|
||||
logger := zerolog.Ctx(ctx)
|
||||
logger.Info().Msg("Hello")
|
||||
}
|
||||
```
|
||||
|
||||
A second form of `context.Context` integration allows you to pass the current
|
||||
`context.Context` into the logged event, and retrieve it from hooks. This can be
|
||||
useful to log trace and span IDs or other information stored in the go context,
|
||||
and facilitates the unification of logging and tracing in some systems:
|
||||
|
||||
```go
|
||||
type TracingHook struct{}
|
||||
|
||||
func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
ctx := e.GetCtx()
|
||||
spanId := getSpanIdFromContext(ctx) // as per your tracing framework
|
||||
e.Str("span-id", spanId)
|
||||
}
|
||||
|
||||
func f() {
|
||||
// Setup the logger
|
||||
logger := zerolog.New(os.Stdout)
|
||||
logger = logger.Hook(TracingHook{})
|
||||
|
||||
ctx := context.Background()
|
||||
// Use the Ctx function to make the context available to the hook
|
||||
logger.Info().Ctx(ctx).Msg("Hello")
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with `net/http`
|
||||
|
||||
The `github.com/rs/zerolog/hlog` package provides some helpers to integrate zerolog with `http.Handler`.
|
||||
@@ -405,11 +606,11 @@ c := alice.New()
|
||||
c = c.Append(hlog.NewHandler(log))
|
||||
|
||||
// Install some provided extra handler to set some request's context fields.
|
||||
// Thanks to those handler, all our logs will come with some pre-populated fields.
|
||||
// Thanks to that handler, all our logs will come with some prepopulated fields.
|
||||
c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
||||
hlog.FromRequest(r).Info().
|
||||
Str("method", r.Method).
|
||||
Str("url", r.URL.String()).
|
||||
Stringer("url", r.URL).
|
||||
Int("status", status).
|
||||
Int("size", size).
|
||||
Dur("duration", duration).
|
||||
@@ -439,62 +640,121 @@ if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple Log Output
|
||||
|
||||
`zerolog.MultiLevelWriter` may be used to send the log message to multiple outputs.
|
||||
|
||||
In this example, we send the log message to both `os.Stdout` and the in-built `ConsoleWriter`.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout)
|
||||
logger := zerolog.New(multi).With().Timestamp().Logger()
|
||||
logger.Info().Msg("Hello World!")
|
||||
}
|
||||
|
||||
// Output (Line 1: Console; Line 2: Stdout)
|
||||
// 12:36PM INF Hello World!
|
||||
// {"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"}
|
||||
```
|
||||
|
||||
## Global Settings
|
||||
|
||||
Some settings can be changed and will by applied to all loggers:
|
||||
Some settings can be changed and will be applied to all loggers:
|
||||
|
||||
* `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
|
||||
* `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Set this to `zerolog.Disabled` to disable logging altogether (quiet mode).
|
||||
* `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
|
||||
* `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
|
||||
* `zerolog.LevelFieldName`: Can be set to customize level field name.
|
||||
* `zerolog.MessageFieldName`: Can be set to customize message field name.
|
||||
* `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
|
||||
* `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with an empty string, times are formated as UNIX timestamp.
|
||||
// DurationFieldUnit defines the unit for time.Duration type fields added
|
||||
// using the Dur method.
|
||||
* `DurationFieldUnit`: Sets the unit of the fields added by `Dur` (default: `time.Millisecond`).
|
||||
* `DurationFieldInteger`: If set to true, `Dur` fields are formatted as integers instead of floats.
|
||||
- `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
|
||||
- `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Call this with `zerolog.Disabled` to disable logging altogether (quiet mode).
|
||||
- `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
|
||||
- `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
|
||||
- `zerolog.LevelFieldName`: Can be set to customize level field name.
|
||||
- `zerolog.MessageFieldName`: Can be set to customize message field name.
|
||||
- `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
|
||||
- `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formatted as UNIX timestamp.
|
||||
- `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`).
|
||||
- `zerolog.DurationFieldFormat`: Can be set to `DurationFormatFloat`, `DurationFormatInt`, or `DurationFormatString` (default: `DurationFormatFloat`) to append the `Duration` as a `Float64`, `Int64`, or by calling `String()` (respectively).
|
||||
- `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`). Deprecated: Use `zerolog.DurationFieldFormat = DurationFormatInt` instead.
|
||||
- `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking.
|
||||
- `zerolog.FloatingPointPrecision`: If set to a value other than -1, controls the number of digits when formatting float numbers in JSON. See [strconv.FormatFloat](https://pkg.go.dev/strconv#FormatFloat)
|
||||
for more details.
|
||||
|
||||
## Field Types
|
||||
|
||||
### Standard Types
|
||||
|
||||
* `Str`
|
||||
* `Bool`
|
||||
* `Int`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||
* `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
|
||||
* `Float32`, `Float64`
|
||||
- `Str`
|
||||
- `Bool`
|
||||
- `Int`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||
- `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
|
||||
- `Float32`, `Float64`
|
||||
|
||||
### Advanced Fields
|
||||
|
||||
* `Err`: Takes an `error` and render it as a string using the `zerolog.ErrorFieldName` field name.
|
||||
* `Timestamp`: Insert a timestamp field with `zerolog.TimestampFieldName` field name and formatted using `zerolog.TimeFieldFormat`.
|
||||
* `Time`: Adds a field with the time formated with the `zerolog.TimeFieldFormat`.
|
||||
* `Dur`: Adds a field with a `time.Duration`.
|
||||
* `Dict`: Adds a sub-key/value as a field of the event.
|
||||
* `Interface`: Uses reflection to marshal the type.
|
||||
- `Err`: Takes an `error` and renders it as a string using the `zerolog.ErrorFieldName` field name.
|
||||
- `Func`: Run a `func` only if the level is enabled.
|
||||
- `Timestamp`: Inserts a timestamp field with `zerolog.TimestampFieldName` field name, formatted using `zerolog.TimeFieldFormat`.
|
||||
- `Time`: Adds a field with time formatted with `zerolog.TimeFieldFormat`.
|
||||
- `Dur`: Adds a field with `time.Duration`.
|
||||
- `Dict`: Adds a sub-key/value as a field of the event.
|
||||
- `RawJSON`: Adds a field with an already encoded JSON (`[]byte`)
|
||||
- `Hex`: Adds a field with value formatted as a hexadecimal string (`[]byte`)
|
||||
- `Interface`: Uses reflection to marshal the type.
|
||||
- `IPAddr`: Adds a field with `net.IP`.
|
||||
- `IPPrefix`: Adds a field with `net.IPNet`.
|
||||
- `MACAddr`: Adds a field with `net.HardwareAddr`
|
||||
|
||||
Most fields are also available in the slice format (`Strs` for `[]string`, `Errs` for `[]error` etc.)
|
||||
|
||||
## Binary Encoding
|
||||
|
||||
In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](http://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows:
|
||||
In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](https://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows:
|
||||
|
||||
```bash
|
||||
go build -tags binary_log .
|
||||
```
|
||||
|
||||
To Decode binary encoded log files you can use any CBOR decoder. One has been tested to work
|
||||
To decode binary encoded log files you can use any CBOR decoder. One has been tested to work
|
||||
with zerolog library is [CSD](https://github.com/toravir/csd/).
|
||||
|
||||
## Integration with `log/slog`
|
||||
|
||||
zerolog provides a `slog.Handler` implementation that routes `log/slog` records through a zerolog logger. This lets you use the standard library's `slog` API while keeping zerolog's performance and encoding:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zl := log.Logger
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
logger := slog.New(handler)
|
||||
|
||||
logger.Info("user logged in", "user", "alice", "role", "admin")
|
||||
}
|
||||
|
||||
// Output: {"level":"info","user":"alice","role":"admin","time":"...","message":"user logged in"}
|
||||
```
|
||||
|
||||
The handler supports all `slog` features including `WithAttrs`, `WithGroup`, nested groups, and `LogValuer` resolution. slog levels are mapped to zerolog levels (e.g. `slog.LevelDebug` to `zerolog.DebugLevel`).
|
||||
|
||||
## Related Projects
|
||||
|
||||
* [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog`
|
||||
- [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog`
|
||||
- [overlog](https://github.com/Trendyol/overlog): Implementation of `Mapped Diagnostic Context` interface using `zerolog`
|
||||
- [zerologr](https://github.com/go-logr/zerologr): Implementation of `logr.LogSink` interface using `zerolog`
|
||||
- [logze](https://github.com/maxbolgarin/logze): Implementation of `log/slog` interface using `zerolog`
|
||||
|
||||
## Benchmarks
|
||||
|
||||
See [logbench](http://hackemist.com/logbench/) for more comprehensive and up-to-date benchmarks.
|
||||
See [logbench](http://bench.zerolog.io/) for more comprehensive and up-to-date benchmarks.
|
||||
|
||||
All operations are allocation free (those numbers *include* JSON encoding):
|
||||
All operations are allocation free (those numbers _include_ JSON encoding):
|
||||
|
||||
```text
|
||||
BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op
|
||||
@@ -506,54 +766,56 @@ BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
|
||||
|
||||
There are a few Go logging benchmarks and comparisons that include zerolog.
|
||||
|
||||
* [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
|
||||
* [uber-common/zap](https://github.com/uber-go/zap#performance)
|
||||
- [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
|
||||
- [uber-common/zap](https://github.com/uber-go/zap#performance)
|
||||
|
||||
Using Uber's zap comparison benchmark:
|
||||
|
||||
Log a message and 10 fields:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
|
||||
| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
|
||||
| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
|
||||
| lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
|
||||
| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
|
||||
| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
|
||||
| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :---------: | :-------------: | :---------------: |
|
||||
| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
|
||||
| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
|
||||
| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
|
||||
| lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
|
||||
| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
|
||||
| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
|
||||
| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
|
||||
|
||||
Log a message with a logger that already has 10 fields of context:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
|
||||
| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
|
||||
| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
|
||||
| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
|
||||
| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :---------: | :-------------: | :---------------: |
|
||||
| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
|
||||
| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
|
||||
| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
|
||||
| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
|
||||
| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
|
||||
|
||||
Log a static string, without any context or `printf`-style templating:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
|
||||
| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
|
||||
| lion | 771 ns/op | 1224 B/op | 10 allocs/op |
|
||||
| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
|
||||
| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
|
||||
| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :--------: | :-------------: | :---------------: |
|
||||
| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
|
||||
| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
|
||||
| lion | 771 ns/op | 1224 B/op | 10 allocs/op |
|
||||
| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
|
||||
| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
|
||||
| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
|
||||
|
||||
## Caveats
|
||||
|
||||
Note that zerolog does de-duplication fields. Using the same key multiple times creates multiple keys in final JSON:
|
||||
### Field duplication
|
||||
|
||||
Note that zerolog does no de-duplication of fields. Using the same key multiple times creates multiple keys in final JSON:
|
||||
|
||||
```go
|
||||
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
@@ -563,4 +825,34 @@ logger.Info().
|
||||
// Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
|
||||
```
|
||||
|
||||
However, it’s not a big deal as JSON accepts dup keys; the last one prevails.
|
||||
In this case, many consumers will take the last value, but this is not guaranteed; check yours if in doubt.
|
||||
|
||||
### Concurrency safety
|
||||
|
||||
Be careful when calling `UpdateContext`. It is not concurrency safe. Use the `With()` method to create a child logger:
|
||||
|
||||
```go
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// Create a child logger for concurrency safety
|
||||
logger := log.Logger.With().Logger()
|
||||
|
||||
// Add context fields, for example User-Agent from HTTP headers
|
||||
logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `Event` object returned from the `Logger` level-specific message functions (e.g. `Log()`, `Trace()`, `Debug()`, etc.)
|
||||
is allocated in `sync.Pool` memory that will be returned to the pool as soon as the `Msg()`, `Msgf()`, `Send()`,
|
||||
or `MsgFunc()` writes the message and **must not** be accessed afterwards.
|
||||
|
||||
**Do not** hold a reference to the `*Event` while in callback functions or your own code. This is especially important in
|
||||
`Hook.Run()` and `HookFunc` functions or `MarshalZerologObject(e *Event)` callback (e.g. `LogObjectMarshaler` implementations).
|
||||
|
||||
Any `Array` objects returned from `Context.CreateArray()` or `Event.CreateArray()` are from a `sync.Pool` so **do not** hold
|
||||
references to them from within any `MarshalZerologArray(a *Array)` callback (e.g. `LogArrayMarshaler` implementations) or your
|
||||
own code as they will be cleared and returned to the pool after being buffered by a call to `Context.Array()` or `Event.Array()`.
|
||||
|
||||
Any _dictionary_ `Event` returned from `Context.CreateDict()` or `Event.CreateDict()` **must not** be referenced after being
|
||||
buffered by a call to `Array.Dict()`, `Context.Dict()`, or `Event.Dict()` as they will be cleared and returned to the pool.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
remote_theme: rs/gh-readme
|
||||
@@ -1,6 +1,7 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -17,10 +18,19 @@ var arrayPool = &sync.Pool{
|
||||
// Array is used to prepopulate an array of items
|
||||
// which can be re-used to add to log messages.
|
||||
type Array struct {
|
||||
buf []byte
|
||||
buf []byte
|
||||
stack bool // enable error stack trace
|
||||
ctx context.Context // Optional Go context
|
||||
ch []Hook // hooks
|
||||
}
|
||||
|
||||
func putArray(a *Array) {
|
||||
// prevent any subsequent use of the Array contextual state and truncate the buffer
|
||||
a.stack = false
|
||||
a.ctx = nil
|
||||
a.ch = nil
|
||||
a.buf = a.buf[:0]
|
||||
|
||||
// Proper usage of a sync.Pool requires each entry to have approximately
|
||||
// the same memory cost. To obtain this property when the stored type
|
||||
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
|
||||
@@ -28,28 +38,34 @@ func putArray(a *Array) {
|
||||
//
|
||||
// See https://golang.org/issue/23199
|
||||
const maxSize = 1 << 16 // 64KiB
|
||||
if cap(a.buf) > maxSize {
|
||||
return
|
||||
if cap(a.buf) <= maxSize {
|
||||
arrayPool.Put(a)
|
||||
}
|
||||
arrayPool.Put(a)
|
||||
}
|
||||
|
||||
// Arr creates an array to be added to an Event or Context.
|
||||
// WARNING: This function is deprecated because it does not preserve
|
||||
// the stack, hooks, and context from the parent event.
|
||||
// Deprecated: Use Event.CreateArray or Context.CreateArray instead.
|
||||
func Arr() *Array {
|
||||
a := arrayPool.Get().(*Array)
|
||||
a.buf = a.buf[:0]
|
||||
a.stack = false
|
||||
a.ctx = nil
|
||||
a.ch = nil
|
||||
return a
|
||||
}
|
||||
|
||||
// MarshalZerologArray method here is no-op - since data is
|
||||
// already in the needed format.
|
||||
func (*Array) MarshalZerologArray(*Array) {
|
||||
// untestable: there's no code to be covered
|
||||
}
|
||||
|
||||
func (a *Array) write(dst []byte) []byte {
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
if len(a.buf) > 0 {
|
||||
dst = append(append(dst, a.buf...))
|
||||
dst = append(dst, a.buf...)
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
putArray(a)
|
||||
@@ -57,46 +73,47 @@ func (a *Array) write(dst []byte) []byte {
|
||||
}
|
||||
|
||||
// Object marshals an object that implement the LogObjectMarshaler
|
||||
// interface and append append it to the array.
|
||||
// interface and appends it to the array.
|
||||
func (a *Array) Object(obj LogObjectMarshaler) *Array {
|
||||
e := Dict()
|
||||
obj.MarshalZerologObject(e)
|
||||
e.buf = enc.AppendEndMarker(e.buf)
|
||||
a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...)
|
||||
putEvent(e)
|
||||
a.buf = appendObject(enc.AppendArrayDelim(a.buf), obj, a.stack, a.ctx, a.ch)
|
||||
return a
|
||||
}
|
||||
|
||||
// Str append append the val as a string to the array.
|
||||
// Str appends the val as a string to the array.
|
||||
func (a *Array) Str(val string) *Array {
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// Bytes append append the val as a string to the array.
|
||||
// Bytes appends the val as a string to the array.
|
||||
func (a *Array) Bytes(val []byte) *Array {
|
||||
a.buf = enc.AppendBytes(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// Hex append append the val as a hex string to the array.
|
||||
// Hex appends the val as a hex string to the array.
|
||||
func (a *Array) Hex(val []byte) *Array {
|
||||
a.buf = enc.AppendHex(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// RawJSON adds already encoded JSON to the array.
|
||||
func (a *Array) RawJSON(val []byte) *Array {
|
||||
a.buf = appendJSON(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// Err serializes and appends the err to the array.
|
||||
func (a *Array) Err(err error) *Array {
|
||||
marshaled := ErrorMarshalFunc(err)
|
||||
switch m := marshaled.(type) {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf))
|
||||
case LogObjectMarshaler:
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(m)
|
||||
a.buf = append(enc.AppendArrayDelim(a.buf), e.buf...)
|
||||
putEvent(e)
|
||||
a = a.Object(m)
|
||||
case error:
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
|
||||
if !isNilValue(m) {
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
|
||||
}
|
||||
case string:
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m)
|
||||
default:
|
||||
@@ -106,97 +123,118 @@ func (a *Array) Err(err error) *Array {
|
||||
return a
|
||||
}
|
||||
|
||||
// Bool append append the val as a bool to the array.
|
||||
// Errs serializes and appends errors to the array.
|
||||
func (a *Array) Errs(errs []error) *Array {
|
||||
for _, err := range errs {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
a = a.Interface(nil)
|
||||
case LogObjectMarshaler:
|
||||
a = a.Object(m)
|
||||
case error:
|
||||
if !isNilValue(m) {
|
||||
a = a.Str(m.Error())
|
||||
}
|
||||
case string:
|
||||
a = a.Str(m)
|
||||
default:
|
||||
a = a.Interface(m)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Bool appends the val as a bool to the array.
|
||||
func (a *Array) Bool(b bool) *Array {
|
||||
a.buf = enc.AppendBool(enc.AppendArrayDelim(a.buf), b)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int append append i as a int to the array.
|
||||
// Int appends i as a int to the array.
|
||||
func (a *Array) Int(i int) *Array {
|
||||
a.buf = enc.AppendInt(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int8 append append i as a int8 to the array.
|
||||
// Int8 appends i as a int8 to the array.
|
||||
func (a *Array) Int8(i int8) *Array {
|
||||
a.buf = enc.AppendInt8(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int16 append append i as a int16 to the array.
|
||||
// Int16 appends i as a int16 to the array.
|
||||
func (a *Array) Int16(i int16) *Array {
|
||||
a.buf = enc.AppendInt16(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int32 append append i as a int32 to the array.
|
||||
// Int32 appends i as a int32 to the array.
|
||||
func (a *Array) Int32(i int32) *Array {
|
||||
a.buf = enc.AppendInt32(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int64 append append i as a int64 to the array.
|
||||
// Int64 appends i as a int64 to the array.
|
||||
func (a *Array) Int64(i int64) *Array {
|
||||
a.buf = enc.AppendInt64(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint append append i as a uint to the array.
|
||||
// Uint appends i as a uint to the array.
|
||||
func (a *Array) Uint(i uint) *Array {
|
||||
a.buf = enc.AppendUint(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint8 append append i as a uint8 to the array.
|
||||
// Uint8 appends i as a uint8 to the array.
|
||||
func (a *Array) Uint8(i uint8) *Array {
|
||||
a.buf = enc.AppendUint8(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint16 append append i as a uint16 to the array.
|
||||
// Uint16 appends i as a uint16 to the array.
|
||||
func (a *Array) Uint16(i uint16) *Array {
|
||||
a.buf = enc.AppendUint16(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint32 append append i as a uint32 to the array.
|
||||
// Uint32 appends i as a uint32 to the array.
|
||||
func (a *Array) Uint32(i uint32) *Array {
|
||||
a.buf = enc.AppendUint32(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint64 append append i as a uint64 to the array.
|
||||
// Uint64 appends i as a uint64 to the array.
|
||||
func (a *Array) Uint64(i uint64) *Array {
|
||||
a.buf = enc.AppendUint64(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Float32 append append f as a float32 to the array.
|
||||
// Float32 appends f as a float32 to the array.
|
||||
func (a *Array) Float32(f float32) *Array {
|
||||
a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f)
|
||||
a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision)
|
||||
return a
|
||||
}
|
||||
|
||||
// Float64 append append f as a float64 to the array.
|
||||
// Float64 appends f as a float64 to the array.
|
||||
func (a *Array) Float64(f float64) *Array {
|
||||
a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f)
|
||||
a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision)
|
||||
return a
|
||||
}
|
||||
|
||||
// Time append append t formated as string using zerolog.TimeFieldFormat.
|
||||
// Time appends t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (a *Array) Time(t time.Time) *Array {
|
||||
a.buf = enc.AppendTime(enc.AppendArrayDelim(a.buf), t, TimeFieldFormat)
|
||||
return a
|
||||
}
|
||||
|
||||
// Dur append append d to the array.
|
||||
// Dur appends d to the array.
|
||||
func (a *Array) Dur(d time.Duration) *Array {
|
||||
a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldInteger)
|
||||
a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return a
|
||||
}
|
||||
|
||||
// Interface append append i marshaled using reflection.
|
||||
// Interface appends i marshaled using reflection.
|
||||
func (a *Array) Interface(i interface{}) *Array {
|
||||
if obj, ok := i.(LogObjectMarshaler); ok {
|
||||
return a.Object(obj)
|
||||
@@ -205,20 +243,34 @@ func (a *Array) Interface(i interface{}) *Array {
|
||||
return a
|
||||
}
|
||||
|
||||
// IPAddr adds IPv4 or IPv6 address to the array
|
||||
// IPAddr adds a net.IP IPv4 or IPv6 address to the array
|
||||
func (a *Array) IPAddr(ip net.IP) *Array {
|
||||
a.buf = enc.AppendIPAddr(enc.AppendArrayDelim(a.buf), ip)
|
||||
return a
|
||||
}
|
||||
|
||||
// IPPrefix adds IPv4 or IPv6 Prefix (IP + mask) to the array
|
||||
// IPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (IP + mask) to the array
|
||||
func (a *Array) IPPrefix(pfx net.IPNet) *Array {
|
||||
a.buf = enc.AppendIPPrefix(enc.AppendArrayDelim(a.buf), pfx)
|
||||
return a
|
||||
}
|
||||
|
||||
// MACAddr adds a MAC (Ethernet) address to the array
|
||||
// MACAddr adds a net.HardwareAddr MAC (Ethernet) address to the array
|
||||
func (a *Array) MACAddr(ha net.HardwareAddr) *Array {
|
||||
a.buf = enc.AppendMACAddr(enc.AppendArrayDelim(a.buf), ha)
|
||||
return a
|
||||
}
|
||||
|
||||
// Dict adds the dict Event to the array
|
||||
func (a *Array) Dict(dict *Event) *Array {
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
a.buf = append(enc.AppendArrayDelim(a.buf), dict.buf...)
|
||||
putEvent(dict)
|
||||
return a
|
||||
}
|
||||
|
||||
// Type adds the val's type using reflection to the array.
|
||||
func (a *Array) Type(val interface{}) *Array {
|
||||
a.buf = enc.AppendType(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
+34
-2
@@ -1,6 +1,7 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -24,11 +25,42 @@ func TestArray(t *testing.T) {
|
||||
Str("a").
|
||||
Bytes([]byte("b")).
|
||||
Hex([]byte{0x1f}).
|
||||
RawJSON([]byte(`{"some":"json"}`)).
|
||||
RawJSON([]byte(`{"longer":[1111,2222,3333,4444,5555]}`)).
|
||||
Time(time.Time{}).
|
||||
IPAddr(net.IP{192, 168, 0, 10}).
|
||||
Dur(0)
|
||||
want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f","0001-01-01T00:00:00Z","192.168.0.10",0]`
|
||||
IPPrefix(net.IPNet{IP: net.IP{127, 0, 0, 0}, Mask: net.CIDRMask(24, 32)}).
|
||||
MACAddr(net.HardwareAddr{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}).
|
||||
Interface(struct {
|
||||
Pub string
|
||||
Tag string `json:"tag"`
|
||||
priv int
|
||||
}{"A", "j", -5}).
|
||||
Interface(logObjectMarshalerImpl{
|
||||
name: "ZOT",
|
||||
age: 35,
|
||||
}).
|
||||
Dur(0).
|
||||
Dict(Dict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).
|
||||
Err(nil).
|
||||
Err(fmt.Errorf("failure")).
|
||||
Err(loggableError{fmt.Errorf("oops")}).
|
||||
Object(logObjectMarshalerImpl{
|
||||
name: "ZIT",
|
||||
age: 22,
|
||||
}).
|
||||
Type(3.14)
|
||||
|
||||
want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f",{"some":"json"},{"longer":[1111,2222,3333,4444,5555]},"0001-01-01T00:00:00Z","192.168.0.10","127.0.0.0/24","01:23:45:67:89:ab",{"Pub":"A","tag":"j"},{"name":"zot","age":-35},0,{"bar":"baz","n":1},null,"failure",{"l":"OOPS"},{"name":"zit","age":-22},"float64"]`
|
||||
if got := decodeObjectToStr(a.write([]byte{})); got != want {
|
||||
t.Errorf("Array.write()\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArray_MarshalZerologArray(t *testing.T) {
|
||||
a := Arr()
|
||||
a.MarshalZerologArray(nil) // no-op method, should not panic
|
||||
}
|
||||
|
||||
+172
-160
@@ -2,7 +2,7 @@ package zerolog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -13,7 +13,7 @@ var (
|
||||
)
|
||||
|
||||
func BenchmarkLogEmpty(b *testing.B) {
|
||||
logger := New(ioutil.Discard)
|
||||
logger := New(io.Discard)
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
@@ -23,7 +23,7 @@ func BenchmarkLogEmpty(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkDisabled(b *testing.B) {
|
||||
logger := New(ioutil.Discard).Level(Disabled)
|
||||
logger := New(io.Discard).Level(Disabled)
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
@@ -33,7 +33,7 @@ func BenchmarkDisabled(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkInfo(b *testing.B) {
|
||||
logger := New(ioutil.Discard)
|
||||
logger := New(io.Discard)
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
@@ -43,7 +43,7 @@ func BenchmarkInfo(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkContextFields(b *testing.B) {
|
||||
logger := New(ioutil.Discard).With().
|
||||
logger := New(io.Discard).With().
|
||||
Str("string", "four!").
|
||||
Time("time", time.Time{}).
|
||||
Int("int", 123).
|
||||
@@ -58,7 +58,7 @@ func BenchmarkContextFields(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkContextAppend(b *testing.B) {
|
||||
logger := New(ioutil.Discard).With().
|
||||
logger := New(io.Discard).With().
|
||||
Str("foo", "bar").
|
||||
Logger()
|
||||
b.ResetTimer()
|
||||
@@ -70,7 +70,7 @@ func BenchmarkContextAppend(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkLogFields(b *testing.B) {
|
||||
logger := New(ioutil.Discard)
|
||||
logger := New(io.Discard)
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
@@ -84,23 +84,11 @@ func BenchmarkLogFields(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
type obj struct {
|
||||
Pub string
|
||||
Tag string `json:"tag"`
|
||||
priv int
|
||||
}
|
||||
|
||||
func (o obj) MarshalZerologObject(e *Event) {
|
||||
e.Str("Pub", o.Pub).
|
||||
Str("Tag", o.Tag).
|
||||
Int("priv", o.priv)
|
||||
}
|
||||
|
||||
func BenchmarkLogArrayObject(b *testing.B) {
|
||||
obj1 := obj{"a", "b", 2}
|
||||
obj2 := obj{"c", "d", 3}
|
||||
obj3 := obj{"e", "f", 4}
|
||||
logger := New(ioutil.Discard)
|
||||
obj1 := fixtureObj{"a", "b", 2}
|
||||
obj2 := fixtureObj{"c", "d", 3}
|
||||
obj3 := fixtureObj{"e", "f", 4}
|
||||
logger := New(io.Discard)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -113,112 +101,125 @@ func BenchmarkLogArrayObject(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkLogFieldType(b *testing.B) {
|
||||
bools := []bool{true, false, true, false, true, false, true, false, true, false}
|
||||
ints := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
floats := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
strings := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
|
||||
durations := []time.Duration{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
times := []time.Time{
|
||||
time.Unix(0, 0),
|
||||
time.Unix(1, 0),
|
||||
time.Unix(2, 0),
|
||||
time.Unix(3, 0),
|
||||
time.Unix(4, 0),
|
||||
time.Unix(5, 0),
|
||||
time.Unix(6, 0),
|
||||
time.Unix(7, 0),
|
||||
time.Unix(8, 0),
|
||||
time.Unix(9, 0),
|
||||
}
|
||||
interfaces := []struct {
|
||||
Pub string
|
||||
Tag string `json:"tag"`
|
||||
priv int
|
||||
}{
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
}
|
||||
objects := []obj{
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
}
|
||||
errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e")}
|
||||
fixtures := makeFieldFixtures()
|
||||
types := map[string]func(e *Event) *Event{
|
||||
"Any": func(e *Event) *Event {
|
||||
return e.Any("k", fixtures.Interfaces[0])
|
||||
},
|
||||
"Bool": func(e *Event) *Event {
|
||||
return e.Bool("k", bools[0])
|
||||
return e.Bool("k", fixtures.Bools[0])
|
||||
},
|
||||
"Bools": func(e *Event) *Event {
|
||||
return e.Bools("k", bools)
|
||||
return e.Bools("k", fixtures.Bools)
|
||||
},
|
||||
"Bytes": func(e *Event) *Event {
|
||||
return e.Bytes("k", fixtures.Bytes)
|
||||
},
|
||||
"Hex": func(e *Event) *Event {
|
||||
return e.Hex("k", fixtures.Bytes)
|
||||
},
|
||||
"Int": func(e *Event) *Event {
|
||||
return e.Int("k", ints[0])
|
||||
return e.Int("k", fixtures.Ints[0])
|
||||
},
|
||||
"Ints": func(e *Event) *Event {
|
||||
return e.Ints("k", ints)
|
||||
return e.Ints("k", fixtures.Ints)
|
||||
},
|
||||
"Float": func(e *Event) *Event {
|
||||
return e.Float64("k", floats[0])
|
||||
"Float32": func(e *Event) *Event {
|
||||
return e.Float32("k", fixtures.Floats32[0])
|
||||
},
|
||||
"Floats": func(e *Event) *Event {
|
||||
return e.Floats64("k", floats)
|
||||
"Floats32": func(e *Event) *Event {
|
||||
return e.Floats32("k", fixtures.Floats32)
|
||||
},
|
||||
"Float64": func(e *Event) *Event {
|
||||
return e.Float64("k", fixtures.Floats64[0])
|
||||
},
|
||||
"Floats64": func(e *Event) *Event {
|
||||
return e.Floats64("k", fixtures.Floats64)
|
||||
},
|
||||
"Str": func(e *Event) *Event {
|
||||
return e.Str("k", strings[0])
|
||||
return e.Str("k", fixtures.Strings[0])
|
||||
},
|
||||
"Strs": func(e *Event) *Event {
|
||||
return e.Strs("k", strings)
|
||||
return e.Strs("k", fixtures.Strings)
|
||||
},
|
||||
"StrsV": func(e *Event) *Event {
|
||||
return e.StrsV("k", fixtures.Strings...)
|
||||
},
|
||||
"Stringer": func(e *Event) *Event {
|
||||
return e.Stringer("k", fixtures.Stringers[0])
|
||||
},
|
||||
"Stringers": func(e *Event) *Event {
|
||||
return e.Stringers("k", fixtures.Stringers)
|
||||
},
|
||||
"StringersV": func(e *Event) *Event {
|
||||
return e.StringersV("k", fixtures.Stringers...)
|
||||
},
|
||||
"Err": func(e *Event) *Event {
|
||||
return e.Err(errs[0])
|
||||
return e.Err(fixtures.Errs[0])
|
||||
},
|
||||
"Errs": func(e *Event) *Event {
|
||||
return e.Errs("k", errs)
|
||||
return e.Errs("k", fixtures.Errs)
|
||||
},
|
||||
"Ctx": func(e *Event) *Event {
|
||||
return e.Ctx(fixtures.Ctx)
|
||||
},
|
||||
"Time": func(e *Event) *Event {
|
||||
return e.Time("k", times[0])
|
||||
return e.Time("k", fixtures.Times[0])
|
||||
},
|
||||
"Times": func(e *Event) *Event {
|
||||
return e.Times("k", times)
|
||||
return e.Times("k", fixtures.Times)
|
||||
},
|
||||
"Dur": func(e *Event) *Event {
|
||||
return e.Dur("k", durations[0])
|
||||
return e.Dur("k", fixtures.Durations[0])
|
||||
},
|
||||
"Durs": func(e *Event) *Event {
|
||||
return e.Durs("k", durations)
|
||||
return e.Durs("k", fixtures.Durations)
|
||||
},
|
||||
"Interface": func(e *Event) *Event {
|
||||
return e.Interface("k", interfaces[0])
|
||||
return e.Interface("k", fixtures.Interfaces[0])
|
||||
},
|
||||
"Interfaces": func(e *Event) *Event {
|
||||
return e.Interface("k", interfaces)
|
||||
return e.Interface("k", fixtures.Interfaces)
|
||||
},
|
||||
"Interface(Object)": func(e *Event) *Event {
|
||||
return e.Interface("k", objects[0])
|
||||
return e.Interface("k", fixtures.Objects[0])
|
||||
},
|
||||
"Interface(Objects)": func(e *Event) *Event {
|
||||
return e.Interface("k", objects)
|
||||
return e.Interface("k", fixtures.Objects)
|
||||
},
|
||||
"Object": func(e *Event) *Event {
|
||||
return e.Object("k", objects[0])
|
||||
return e.Object("k", fixtures.Objects[0])
|
||||
},
|
||||
"Objects": func(e *Event) *Event {
|
||||
return e.Objects("k", fixtures.Objects)
|
||||
},
|
||||
"ObjectsV": func(e *Event) *Event {
|
||||
return e.ObjectsV("k", fixtures.Objects...)
|
||||
},
|
||||
"Timestamp": func(e *Event) *Event {
|
||||
return e.Timestamp()
|
||||
},
|
||||
"IPAddr": func(e *Event) *Event {
|
||||
return e.IPAddr("k", fixtures.IPAddrs[0])
|
||||
},
|
||||
"IPAddrs": func(e *Event) *Event {
|
||||
return e.IPAddrs("k", fixtures.IPAddrs)
|
||||
},
|
||||
"IPPrefix": func(e *Event) *Event {
|
||||
return e.IPPrefix("k", fixtures.IPPfxs[0])
|
||||
},
|
||||
"IPPrefixes": func(e *Event) *Event {
|
||||
return e.IPPrefixes("k", fixtures.IPPfxs)
|
||||
},
|
||||
"MACAddr": func(e *Event) *Event {
|
||||
return e.MACAddr("k", fixtures.MACAddr)
|
||||
},
|
||||
"Type": func(e *Event) *Event {
|
||||
return e.Type("k", fixtures.Type)
|
||||
},
|
||||
}
|
||||
logger := New(ioutil.Discard)
|
||||
|
||||
logger := New(io.Discard)
|
||||
b.ResetTimer()
|
||||
for name := range types {
|
||||
f := types[name]
|
||||
@@ -234,117 +235,128 @@ func BenchmarkLogFieldType(b *testing.B) {
|
||||
|
||||
func BenchmarkContextFieldType(b *testing.B) {
|
||||
oldFormat := TimeFieldFormat
|
||||
TimeFieldFormat = ""
|
||||
TimeFieldFormat = TimeFormatUnix
|
||||
defer func() { TimeFieldFormat = oldFormat }()
|
||||
bools := []bool{true, false, true, false, true, false, true, false, true, false}
|
||||
ints := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
floats := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
strings := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
|
||||
durations := []time.Duration{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
times := []time.Time{
|
||||
time.Unix(0, 0),
|
||||
time.Unix(1, 0),
|
||||
time.Unix(2, 0),
|
||||
time.Unix(3, 0),
|
||||
time.Unix(4, 0),
|
||||
time.Unix(5, 0),
|
||||
time.Unix(6, 0),
|
||||
time.Unix(7, 0),
|
||||
time.Unix(8, 0),
|
||||
time.Unix(9, 0),
|
||||
}
|
||||
interfaces := []struct {
|
||||
Pub string
|
||||
Tag string `json:"tag"`
|
||||
priv int
|
||||
}{
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
{"a", "a", 0},
|
||||
}
|
||||
objects := []obj{
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
obj{"a", "a", 0},
|
||||
}
|
||||
errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e")}
|
||||
|
||||
fixtures := makeFieldFixtures()
|
||||
types := map[string]func(c Context) Context{
|
||||
"Any": func(c Context) Context {
|
||||
return c.Any("k", fixtures.Interfaces[0])
|
||||
},
|
||||
"Bool": func(c Context) Context {
|
||||
return c.Bool("k", bools[0])
|
||||
return c.Bool("k", fixtures.Bools[0])
|
||||
},
|
||||
"Bools": func(c Context) Context {
|
||||
return c.Bools("k", bools)
|
||||
return c.Bools("k", fixtures.Bools)
|
||||
},
|
||||
"Bytes": func(c Context) Context {
|
||||
return c.Bytes("k", fixtures.Bytes)
|
||||
},
|
||||
"Hex": func(c Context) Context {
|
||||
return c.Hex("k", fixtures.Bytes)
|
||||
},
|
||||
"Int": func(c Context) Context {
|
||||
return c.Int("k", ints[0])
|
||||
return c.Int("k", fixtures.Ints[0])
|
||||
},
|
||||
"Ints": func(c Context) Context {
|
||||
return c.Ints("k", ints)
|
||||
return c.Ints("k", fixtures.Ints)
|
||||
},
|
||||
"Float": func(c Context) Context {
|
||||
return c.Float64("k", floats[0])
|
||||
"Float32": func(c Context) Context {
|
||||
return c.Float32("k", fixtures.Floats32[0])
|
||||
},
|
||||
"Floats": func(c Context) Context {
|
||||
return c.Floats64("k", floats)
|
||||
"Floats32": func(c Context) Context {
|
||||
return c.Floats32("k", fixtures.Floats32)
|
||||
},
|
||||
"Float64": func(c Context) Context {
|
||||
return c.Float64("k", fixtures.Floats64[0])
|
||||
},
|
||||
"Floats64": func(c Context) Context {
|
||||
return c.Floats64("k", fixtures.Floats64)
|
||||
},
|
||||
"Str": func(c Context) Context {
|
||||
return c.Str("k", strings[0])
|
||||
return c.Str("k", fixtures.Strings[0])
|
||||
},
|
||||
"Strs": func(c Context) Context {
|
||||
return c.Strs("k", strings)
|
||||
return c.Strs("k", fixtures.Strings)
|
||||
},
|
||||
"StrsV": func(c Context) Context {
|
||||
return c.StrsV("k", fixtures.Strings...)
|
||||
},
|
||||
"Stringer": func(c Context) Context {
|
||||
return c.Stringer("k", fixtures.Stringers[0])
|
||||
},
|
||||
"Stringers": func(c Context) Context {
|
||||
return c.Stringers("k", fixtures.Stringers)
|
||||
},
|
||||
"StringersV": func(c Context) Context {
|
||||
return c.StringersV("k", fixtures.Stringers...)
|
||||
},
|
||||
"Err": func(c Context) Context {
|
||||
return c.Err(errs[0])
|
||||
return c.Err(fixtures.Errs[0])
|
||||
},
|
||||
"Errs": func(c Context) Context {
|
||||
return c.Errs("k", errs)
|
||||
return c.Errs("k", fixtures.Errs)
|
||||
},
|
||||
"Ctx": func(c Context) Context {
|
||||
return c.Ctx(fixtures.Ctx)
|
||||
},
|
||||
"Time": func(c Context) Context {
|
||||
return c.Time("k", times[0])
|
||||
return c.Time("k", fixtures.Times[0])
|
||||
},
|
||||
"Times": func(c Context) Context {
|
||||
return c.Times("k", times)
|
||||
return c.Times("k", fixtures.Times)
|
||||
},
|
||||
"Dur": func(c Context) Context {
|
||||
return c.Dur("k", durations[0])
|
||||
return c.Dur("k", fixtures.Durations[0])
|
||||
},
|
||||
"Durs": func(c Context) Context {
|
||||
return c.Durs("k", durations)
|
||||
return c.Durs("k", fixtures.Durations)
|
||||
},
|
||||
"Interface": func(c Context) Context {
|
||||
return c.Interface("k", interfaces[0])
|
||||
return c.Interface("k", fixtures.Interfaces[0])
|
||||
},
|
||||
"Interfaces": func(c Context) Context {
|
||||
return c.Interface("k", interfaces)
|
||||
return c.Interface("k", fixtures.Interfaces)
|
||||
},
|
||||
"Interface(Object)": func(c Context) Context {
|
||||
return c.Interface("k", objects[0])
|
||||
return c.Interface("k", fixtures.Objects[0])
|
||||
},
|
||||
"Interface(Objects)": func(c Context) Context {
|
||||
return c.Interface("k", objects)
|
||||
return c.Interface("k", fixtures.Objects)
|
||||
},
|
||||
"Object": func(c Context) Context {
|
||||
return c.Object("k", objects[0])
|
||||
return c.Object("k", fixtures.Objects[0])
|
||||
},
|
||||
"Objects": func(c Context) Context {
|
||||
return c.Objects("k", fixtures.Objects)
|
||||
},
|
||||
"ObjectsV": func(c Context) Context {
|
||||
return c.ObjectsV("k", fixtures.Objects...)
|
||||
},
|
||||
"Timestamp": func(c Context) Context {
|
||||
return c.Timestamp()
|
||||
},
|
||||
"IPAddr": func(c Context) Context {
|
||||
return c.IPAddr("k", fixtures.IPAddrs[0])
|
||||
},
|
||||
"IPAddrs": func(c Context) Context {
|
||||
return c.IPAddrs("k", fixtures.IPAddrs)
|
||||
},
|
||||
"IPPrefix": func(c Context) Context {
|
||||
return c.IPPrefix("k", fixtures.IPPfxs[0])
|
||||
},
|
||||
"IPPrefixes": func(c Context) Context {
|
||||
return c.IPPrefixes("k", fixtures.IPPfxs)
|
||||
},
|
||||
"MACAddr": func(c Context) Context {
|
||||
return c.MACAddr("k", fixtures.MACAddr)
|
||||
},
|
||||
"Type": func(c Context) Context {
|
||||
return c.Type("k", fixtures.Type)
|
||||
},
|
||||
}
|
||||
logger := New(ioutil.Discard)
|
||||
|
||||
logger := New(io.Discard)
|
||||
b.ResetTimer()
|
||||
for name := range types {
|
||||
f := types[name]
|
||||
|
||||
+184
-36
@@ -1,3 +1,4 @@
|
||||
//go:build binary_log
|
||||
// +build binary_log
|
||||
|
||||
package zerolog
|
||||
@@ -6,21 +7,12 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
// "io/ioutil"
|
||||
stdlog "log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExampleBinaryNew() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst)
|
||||
|
||||
log.Info().Msg("hello world")
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"level":"info","message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleLogger_With() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).
|
||||
@@ -108,6 +100,19 @@ func ExampleLogger_Printf() {
|
||||
// Output: {"level":"debug","message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleLogger_Trace() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst)
|
||||
|
||||
log.Trace().
|
||||
Str("foo", "bar").
|
||||
Int("n", 123).
|
||||
Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"level":"trace","foo":"bar","n":123,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleLogger_Debug() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst)
|
||||
@@ -201,12 +206,13 @@ func ExampleEvent_Dict() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Dict("dict", Dict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).
|
||||
e := log.Log().
|
||||
Str("foo", "bar")
|
||||
|
||||
e.Dict("dict", e.CreateDict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).
|
||||
Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
@@ -227,6 +233,7 @@ func (u User) MarshalZerologObject(e *Event) {
|
||||
|
||||
type Users []User
|
||||
|
||||
// User implements LogObjectMarshaler
|
||||
func (uu Users) MarshalZerologArray(a *Array) {
|
||||
for _, u := range uu {
|
||||
a.Object(u)
|
||||
@@ -237,12 +244,13 @@ func ExampleEvent_Array() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Array("array", Arr().
|
||||
Str("baz").
|
||||
Int(1),
|
||||
).
|
||||
e := log.Log().
|
||||
Str("foo", "bar")
|
||||
|
||||
e.Array("array", e.CreateArray().
|
||||
Str("baz").
|
||||
Int(1),
|
||||
).
|
||||
Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
@@ -284,6 +292,25 @@ func ExampleEvent_Object() {
|
||||
// Output: {"foo":"bar","user":{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Objects() {
|
||||
// In go, arrays are type invariant so even if you have a variable u of type []User array and User implements
|
||||
// the LogObjectMarshaler interface, you cannot pass that to func that takes an []LogObjectMarshaler array in the
|
||||
// Objects call. In 1.24+ it allows passing the variadic covariant slice (e.g. u...) but the unit test needs to
|
||||
// work in earlier versions so we'll declare the array as []LogObjectMarshaler here.
|
||||
u := []LogObjectMarshaler{User{"John", 35, time.Time{}}, User{"Bob", 55, time.Time{}}}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
Objects("users", u).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bob","age":55,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_EmbedObject() {
|
||||
price := Price{val: 6449, prec: 2, unit: "$"}
|
||||
|
||||
@@ -351,16 +378,53 @@ func ExampleEvent_Durs() {
|
||||
// Output: {"foo":"bar","durs":[10000,20000],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_Fields_map() {
|
||||
fields := map[string]interface{}{
|
||||
"bar": "baz",
|
||||
"n": 1,
|
||||
}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_Fields_slice() {
|
||||
fields := []interface{}{
|
||||
"bar", "baz",
|
||||
"n", 1,
|
||||
}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Dict() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
Dict("dict", Dict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).Logger()
|
||||
ctx := New(&dst).With().
|
||||
Str("foo", "bar")
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
logger := ctx.Dict("dict", ctx.CreateDict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).Logger()
|
||||
|
||||
logger.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}
|
||||
@@ -368,14 +432,15 @@ func ExampleContext_Dict() {
|
||||
|
||||
func ExampleContext_Array() {
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
Array("array", Arr().
|
||||
Str("baz").
|
||||
Int(1),
|
||||
).Logger()
|
||||
ctx := New(&dst).With().
|
||||
Str("foo", "bar")
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
logger := ctx.Array("array", ctx.CreateArray().
|
||||
Str("baz").
|
||||
Int(1),
|
||||
).Logger()
|
||||
|
||||
logger.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","array":["baz",1],"message":"hello world"}
|
||||
@@ -497,3 +562,86 @@ func ExampleContext_Durs() {
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","durs":[10000,20000],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Fields_map() {
|
||||
fields := map[string]interface{}{
|
||||
"bar": "baz",
|
||||
"n": 1,
|
||||
}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Fields_slice() {
|
||||
fields := []interface{}{
|
||||
"bar", "baz",
|
||||
"n", 1,
|
||||
}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_IPAddr() {
|
||||
ipV4 := net.IP{192, 168, 0, 1}
|
||||
ipV6 := net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
IPAddr("v4", ipV4).
|
||||
IPAddr("v6", ipV6).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","v4":"192.168.0.1","v6":"2001:db8:85a3::8a2e:370:7334","message":"hello world"}
|
||||
}
|
||||
func ExampleContext_IPPrefix() {
|
||||
pfxV4 := net.IPNet{IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}
|
||||
pfxV6 := net.IPNet{IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x00}, Mask: net.CIDRMask(64, 128)}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
IPPrefix("v4", pfxV4).
|
||||
IPPrefix("v6", pfxV6).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","v4":"192.168.0.100/24","v6":"2001:db8:85a3::8a2e:370:7300/64","message":"hello world"}
|
||||
}
|
||||
func ExampleContext_MACAddr() {
|
||||
mac := net.HardwareAddr{0x12, 0x34, 0x56, 0x78, 0x90, 0xab}
|
||||
|
||||
dst := bytes.Buffer{}
|
||||
log := New(&dst).With().
|
||||
Str("foo", "bar").
|
||||
MACAddr("mac", mac).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
fmt.Println(decodeIfBinaryToString(dst.Bytes()))
|
||||
// Output: {"foo":"bar","mac":"12:34:56:78:90:ab","message":"hello world"}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Zerolog Lint
|
||||
|
||||
**DEPRECATED: In favor of https://github.com/ykadowak/zerologlint which is integrated with `go vet` and [golangci-lint](https://golangci-lint.run/).**
|
||||
|
||||
This is a basic linter that checks for missing log event finishers. Finds errors like: `log.Error().Int64("userID": 5)` - missing the `Msg`/`Msgf` finishers.
|
||||
|
||||
## Problem
|
||||
@@ -27,7 +29,7 @@ The command accepts only one argument - the package to be inspected - and 4 opti
|
||||
- ignoreFile
|
||||
- which files to ignore, either by full path or by go path (package/file.go)
|
||||
- ignorePkg
|
||||
- do not inspect the specified package if found in the dependecy tree
|
||||
- do not inspect the specified package if found in the dependency tree
|
||||
- ignorePkgRecursively
|
||||
- do not inspect the specified package or its subpackages if found in the dependency tree
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module github.com/rs/zerolog/cmd/lint
|
||||
|
||||
go 1.15
|
||||
|
||||
require golang.org/x/tools v0.1.8
|
||||
@@ -0,0 +1,28 @@
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -0,0 +1,40 @@
|
||||
# Zerolog PrettyLog
|
||||
|
||||
This is a basic CLI utility that will colorize and pretty print your structured JSON logs.
|
||||
|
||||
## Usage
|
||||
|
||||
You can compile it or run it directly. The only issue is that by default Zerolog does not output to `stdout`
|
||||
but rather to `stderr` so we must pipe `stderr` stream to this CLI tool.
|
||||
|
||||
### Linux
|
||||
|
||||
These commands will redirect `stderr` to our `prettylog` tool and `stdout` will remain unaffected.
|
||||
|
||||
1. Compiled version
|
||||
|
||||
```shell
|
||||
some_program_with_zerolog 2> >(prettylog)
|
||||
```
|
||||
|
||||
2. Run it directly with `go run`
|
||||
|
||||
```shell
|
||||
some_program_with_zerolog 2> >(go run cmd/prettylog/prettylog.go)
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
These commands will redirect `stderr` to `stdout` and then pipe it to our `prettylog` tool.
|
||||
|
||||
1. Compiled version
|
||||
|
||||
```shell
|
||||
some_program_with_zerolog 2>&1 | prettylog
|
||||
```
|
||||
|
||||
2. Run it directly with `go run`
|
||||
|
||||
```shell
|
||||
some_program_with_zerolog 2>&1 | go run cmd/prettylog/prettylog.go
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func isInputFromPipe() bool {
|
||||
fileInfo, _ := os.Stdin.Stat()
|
||||
return fileInfo.Mode()&os.ModeCharDevice == 0
|
||||
}
|
||||
|
||||
func processInput(reader io.Reader, writer io.Writer) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
bytesToWrite := scanner.Bytes()
|
||||
_, err := writer.Write(bytesToWrite)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bytesToWrite)
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func main() {
|
||||
timeFormats := map[string]string{
|
||||
"default": time.Kitchen,
|
||||
"full": time.RFC1123,
|
||||
}
|
||||
|
||||
timeFormatFlag := flag.String(
|
||||
"time-format",
|
||||
"default",
|
||||
"Time format, either 'default' or 'full'",
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
timeFormat, ok := timeFormats[*timeFormatFlag]
|
||||
if !ok {
|
||||
panic("Invalid time-format provided")
|
||||
}
|
||||
|
||||
writer := zerolog.NewConsoleWriter()
|
||||
writer.TimeFormat = timeFormat
|
||||
|
||||
if isInputFromPipe() {
|
||||
_ = processInput(os.Stdin, writer)
|
||||
} else if flag.NArg() >= 1 {
|
||||
for _, filename := range flag.Args() {
|
||||
// Scan each line from filename and write it into writer
|
||||
reader, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("%s open: %v", filename, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := processInput(reader, writer); err != nil {
|
||||
fmt.Printf("%s scan: %v", filename, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" app_with_zerolog | 2> >(prettylog)")
|
||||
fmt.Println(" prettylog zerolog_output.jsonl")
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
+476
-91
@@ -5,141 +5,377 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
)
|
||||
|
||||
const (
|
||||
cReset = 0
|
||||
cBold = 1
|
||||
cRed = 31
|
||||
cGreen = 32
|
||||
cYellow = 33
|
||||
cBlue = 34
|
||||
cMagenta = 35
|
||||
cCyan = 36
|
||||
cGray = 37
|
||||
cDarkGray = 90
|
||||
colorBlack = iota + 30
|
||||
colorRed
|
||||
colorGreen
|
||||
colorYellow
|
||||
colorBlue
|
||||
colorMagenta
|
||||
colorCyan
|
||||
colorWhite
|
||||
|
||||
colorBold = 1
|
||||
colorDarkGray = 90
|
||||
|
||||
unknownLevel = "???"
|
||||
)
|
||||
|
||||
var consoleBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, 100))
|
||||
},
|
||||
}
|
||||
var (
|
||||
consoleBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, 100))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// LevelWidth defines the desired character width of the log level column.
|
||||
// Default 0 does not trim or pad (variable width based level text, e.g. "INFO" or "ERROR")
|
||||
var LevelWidth = 0
|
||||
const (
|
||||
consoleDefaultTimeFormat = time.Kitchen
|
||||
)
|
||||
|
||||
// ConsoleWriter reads a JSON object per write operation and output an
|
||||
// optionally colored human readable version on the Out writer.
|
||||
// Formatter transforms the input into a formatted string.
|
||||
type Formatter func(interface{}) string
|
||||
|
||||
// FormatterByFieldName transforms the input into a formatted string,
|
||||
// being able to differentiate formatting based on field name.
|
||||
type FormatterByFieldName func(interface{}, string) string
|
||||
|
||||
// ConsoleWriter parses the JSON input and writes it in an
|
||||
// (optionally) colorized, human-friendly format to Out.
|
||||
type ConsoleWriter struct {
|
||||
Out io.Writer
|
||||
// Out is the output destination.
|
||||
Out io.Writer
|
||||
|
||||
// NoColor disables the colorized output.
|
||||
NoColor bool
|
||||
|
||||
// TimeFormat specifies the format for timestamp in output.
|
||||
TimeFormat string
|
||||
|
||||
// TimeLocation tells ConsoleWriter’s default FormatTimestamp
|
||||
// how to localize the time.
|
||||
TimeLocation *time.Location
|
||||
|
||||
// PartsOrder defines the order of parts in output.
|
||||
PartsOrder []string
|
||||
|
||||
// PartsExclude defines parts to not display in output.
|
||||
PartsExclude []string
|
||||
|
||||
// FieldsOrder defines the order of contextual fields in output.
|
||||
FieldsOrder []string
|
||||
|
||||
fieldIsOrdered map[string]int
|
||||
|
||||
// FieldsExclude defines contextual fields to not display in output.
|
||||
FieldsExclude []string
|
||||
|
||||
FormatTimestamp Formatter
|
||||
FormatLevel Formatter
|
||||
FormatCaller Formatter
|
||||
FormatMessage Formatter
|
||||
FormatFieldName Formatter
|
||||
FormatFieldValue Formatter
|
||||
FormatErrFieldName Formatter
|
||||
FormatErrFieldValue Formatter
|
||||
// If this is configured it is used for "part" values and
|
||||
// has precedence on FormatFieldValue
|
||||
FormatPartValueByName FormatterByFieldName
|
||||
|
||||
FormatExtra func(map[string]interface{}, *bytes.Buffer) error
|
||||
|
||||
FormatPrepare func(map[string]interface{}) error
|
||||
}
|
||||
|
||||
// NewConsoleWriter creates and initializes a new ConsoleWriter.
|
||||
func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter {
|
||||
w := ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: consoleDefaultTimeFormat,
|
||||
PartsOrder: consoleDefaultPartsOrder(),
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(&w)
|
||||
}
|
||||
|
||||
// Fix color on Windows
|
||||
if w.Out == os.Stdout || w.Out == os.Stderr {
|
||||
w.Out = colorable.NewColorable(w.Out.(*os.File))
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Write transforms the JSON input with formatters and appends to w.Out.
|
||||
func (w ConsoleWriter) Write(p []byte) (n int, err error) {
|
||||
var event map[string]interface{}
|
||||
// Fix color on Windows
|
||||
if w.Out == os.Stdout || w.Out == os.Stderr {
|
||||
w.Out = colorable.NewColorable(w.Out.(*os.File))
|
||||
}
|
||||
|
||||
if w.PartsOrder == nil {
|
||||
w.PartsOrder = consoleDefaultPartsOrder()
|
||||
}
|
||||
|
||||
var buf = consoleBufPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
consoleBufPool.Put(buf)
|
||||
}()
|
||||
|
||||
var evt map[string]interface{}
|
||||
p = decodeIfBinaryToBytes(p)
|
||||
d := json.NewDecoder(bytes.NewReader(p))
|
||||
d.UseNumber()
|
||||
err = d.Decode(&event)
|
||||
err = d.Decode(&evt)
|
||||
if err != nil {
|
||||
return
|
||||
return n, fmt.Errorf("cannot decode event: %s", err)
|
||||
}
|
||||
buf := consoleBufPool.Get().(*bytes.Buffer)
|
||||
defer consoleBufPool.Put(buf)
|
||||
lvlColor := cReset
|
||||
level := "????"
|
||||
if l, ok := event[LevelFieldName].(string); ok {
|
||||
if !w.NoColor {
|
||||
lvlColor = levelColor(l)
|
||||
|
||||
if w.FormatPrepare != nil {
|
||||
err = w.FormatPrepare(evt)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
level = strings.ToUpper(l)
|
||||
if LevelWidth > 0 {
|
||||
if padding := LevelWidth - len(level); padding > 0 {
|
||||
level += strings.Repeat(" ", padding)
|
||||
} else {
|
||||
level = level[0:LevelWidth]
|
||||
}
|
||||
|
||||
for _, p := range w.PartsOrder {
|
||||
w.writePart(buf, evt, p)
|
||||
}
|
||||
|
||||
w.writeFields(evt, buf)
|
||||
|
||||
if w.FormatExtra != nil {
|
||||
err = w.FormatExtra(evt, buf)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
err = buf.WriteByte('\n')
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
_, err = buf.WriteTo(w.Out)
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (w ConsoleWriter) Close() error {
|
||||
if closer, ok := w.Out.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFields appends formatted key-value pairs to buf.
|
||||
func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer) {
|
||||
var fields = make([]string, 0, len(evt))
|
||||
for field := range evt {
|
||||
var isExcluded bool
|
||||
for _, excluded := range w.FieldsExclude {
|
||||
if field == excluded {
|
||||
isExcluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(buf, "%s |%s| %s",
|
||||
colorize(formatTime(event[TimestampFieldName]), cDarkGray, !w.NoColor),
|
||||
colorize(level, lvlColor, !w.NoColor),
|
||||
colorize(event[MessageFieldName], cReset, !w.NoColor))
|
||||
fields := make([]string, 0, len(event))
|
||||
for field := range event {
|
||||
if isExcluded {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case LevelFieldName, TimestampFieldName, MessageFieldName:
|
||||
case LevelFieldName, TimestampFieldName, MessageFieldName, CallerFieldName:
|
||||
continue
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
sort.Strings(fields)
|
||||
for _, field := range fields {
|
||||
fmt.Fprintf(buf, " %s=", colorize(field, cCyan, !w.NoColor))
|
||||
switch value := event[field].(type) {
|
||||
case string:
|
||||
if needsQuote(value) {
|
||||
buf.WriteString(strconv.Quote(value))
|
||||
|
||||
if len(w.FieldsOrder) > 0 {
|
||||
w.orderFields(fields)
|
||||
} else {
|
||||
sort.Strings(fields)
|
||||
}
|
||||
|
||||
// Write space only if something has already been written to the buffer, and if there are fields.
|
||||
if buf.Len() > 0 && len(fields) > 0 {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
// Move the "error" field to the front
|
||||
ei := sort.Search(len(fields), func(i int) bool { return fields[i] >= ErrorFieldName })
|
||||
if ei < len(fields) && fields[ei] == ErrorFieldName {
|
||||
fields[ei] = ""
|
||||
fields = append([]string{ErrorFieldName}, fields...)
|
||||
var xfields = make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if field == "" { // Skip empty fields
|
||||
continue
|
||||
}
|
||||
xfields = append(xfields, field)
|
||||
}
|
||||
fields = xfields
|
||||
}
|
||||
|
||||
for i, field := range fields {
|
||||
var fn Formatter
|
||||
var fv Formatter
|
||||
|
||||
if field == ErrorFieldName {
|
||||
if w.FormatErrFieldName == nil {
|
||||
fn = consoleDefaultFormatErrFieldName(w.NoColor)
|
||||
} else {
|
||||
buf.WriteString(value)
|
||||
fn = w.FormatErrFieldName
|
||||
}
|
||||
|
||||
if w.FormatErrFieldValue == nil {
|
||||
fv = consoleDefaultFormatErrFieldValue(w.NoColor)
|
||||
} else {
|
||||
fv = w.FormatErrFieldValue
|
||||
}
|
||||
} else {
|
||||
if w.FormatFieldName == nil {
|
||||
fn = consoleDefaultFormatFieldName(w.NoColor)
|
||||
} else {
|
||||
fn = w.FormatFieldName
|
||||
}
|
||||
|
||||
if w.FormatFieldValue == nil {
|
||||
fv = consoleDefaultFormatFieldValue
|
||||
} else {
|
||||
fv = w.FormatFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(fn(field))
|
||||
|
||||
switch fValue := evt[field].(type) {
|
||||
case string:
|
||||
if needsQuote(fValue) {
|
||||
buf.WriteString(fv(strconv.Quote(fValue)))
|
||||
} else {
|
||||
buf.WriteString(fv(fValue))
|
||||
}
|
||||
case json.Number:
|
||||
fmt.Fprint(buf, value)
|
||||
buf.WriteString(fv(fValue))
|
||||
default:
|
||||
b, err := json.Marshal(value)
|
||||
b, err := InterfaceMarshalFunc(fValue)
|
||||
if err != nil {
|
||||
fmt.Fprintf(buf, "[error: %v]", err)
|
||||
fmt.Fprintf(buf, colorize("[error: %v]", colorRed, w.NoColor), err)
|
||||
} else {
|
||||
fmt.Fprint(buf, string(b))
|
||||
fmt.Fprint(buf, fv(b))
|
||||
}
|
||||
}
|
||||
|
||||
if i < len(fields)-1 { // Skip space for last field
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writePart appends a formatted part to buf.
|
||||
func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{}, p string) {
|
||||
var f Formatter
|
||||
var fvn FormatterByFieldName
|
||||
|
||||
if len(w.PartsExclude) > 0 {
|
||||
for _, exclude := range w.PartsExclude {
|
||||
if exclude == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
buf.WriteTo(w.Out)
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
|
||||
func formatTime(t interface{}) string {
|
||||
switch t := t.(type) {
|
||||
case string:
|
||||
return t
|
||||
case json.Number:
|
||||
u, _ := t.Int64()
|
||||
return time.Unix(u, 0).Format(time.RFC3339)
|
||||
}
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func colorize(s interface{}, color int, enabled bool) string {
|
||||
if !enabled {
|
||||
return fmt.Sprintf("%v", s)
|
||||
}
|
||||
return fmt.Sprintf("\x1b[%dm%v\x1b[0m", color, s)
|
||||
}
|
||||
|
||||
func levelColor(level string) int {
|
||||
switch level {
|
||||
case "debug":
|
||||
return cMagenta
|
||||
case "info":
|
||||
return cGreen
|
||||
case "warn":
|
||||
return cYellow
|
||||
case "error", "fatal", "panic":
|
||||
return cRed
|
||||
switch p {
|
||||
case LevelFieldName:
|
||||
if w.FormatLevel == nil {
|
||||
f = consoleDefaultFormatLevel(w.NoColor)
|
||||
} else {
|
||||
f = w.FormatLevel
|
||||
}
|
||||
case TimestampFieldName:
|
||||
if w.FormatTimestamp == nil {
|
||||
f = consoleDefaultFormatTimestamp(w.TimeFormat, w.TimeLocation, w.NoColor)
|
||||
} else {
|
||||
f = w.FormatTimestamp
|
||||
}
|
||||
case MessageFieldName:
|
||||
if w.FormatMessage == nil {
|
||||
f = consoleDefaultFormatMessage(w.NoColor, evt[LevelFieldName])
|
||||
} else {
|
||||
f = w.FormatMessage
|
||||
}
|
||||
case CallerFieldName:
|
||||
if w.FormatCaller == nil {
|
||||
f = consoleDefaultFormatCaller(w.NoColor)
|
||||
} else {
|
||||
f = w.FormatCaller
|
||||
}
|
||||
default:
|
||||
return cReset
|
||||
if w.FormatPartValueByName != nil {
|
||||
fvn = w.FormatPartValueByName
|
||||
} else if w.FormatFieldValue != nil {
|
||||
f = w.FormatFieldValue
|
||||
} else {
|
||||
f = consoleDefaultFormatFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
var s string
|
||||
if f == nil {
|
||||
s = fvn(evt[p], p)
|
||||
} else {
|
||||
s = f(evt[p])
|
||||
}
|
||||
|
||||
if len(s) > 0 {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte(' ') // Write space only if not the first part
|
||||
}
|
||||
buf.WriteString(s)
|
||||
}
|
||||
}
|
||||
|
||||
// orderFields takes an array of field names and an array representing field order
|
||||
// and returns an array with any ordered fields at the beginning, in order,
|
||||
// and the remaining fields after in their original order.
|
||||
func (w ConsoleWriter) orderFields(fields []string) {
|
||||
if w.fieldIsOrdered == nil {
|
||||
w.fieldIsOrdered = make(map[string]int)
|
||||
for i, fieldName := range w.FieldsOrder {
|
||||
w.fieldIsOrdered[fieldName] = i
|
||||
}
|
||||
}
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
ii, iOrdered := w.fieldIsOrdered[fields[i]]
|
||||
jj, jOrdered := w.fieldIsOrdered[fields[j]]
|
||||
if iOrdered && jOrdered {
|
||||
return ii < jj
|
||||
}
|
||||
if iOrdered {
|
||||
return true
|
||||
}
|
||||
if jOrdered {
|
||||
return false
|
||||
}
|
||||
return fields[i] < fields[j]
|
||||
})
|
||||
}
|
||||
|
||||
// needsQuote returns true when the string s should be quoted in output.
|
||||
func needsQuote(s string) bool {
|
||||
for i := range s {
|
||||
if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' {
|
||||
@@ -148,3 +384,152 @@ func needsQuote(s string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// colorize returns the string s wrapped in ANSI code c, unless disabled is true or c is 0.
|
||||
func colorize(s interface{}, c int, disabled bool) string {
|
||||
e := os.Getenv("NO_COLOR")
|
||||
if e != "" || c == 0 {
|
||||
disabled = true
|
||||
}
|
||||
|
||||
if disabled {
|
||||
return fmt.Sprintf("%s", s)
|
||||
}
|
||||
return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s)
|
||||
}
|
||||
|
||||
// ----- DEFAULT FORMATTERS ---------------------------------------------------
|
||||
|
||||
func consoleDefaultPartsOrder() []string {
|
||||
return []string{
|
||||
TimestampFieldName,
|
||||
LevelFieldName,
|
||||
CallerFieldName,
|
||||
MessageFieldName,
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatTimestamp(timeFormat string, location *time.Location, noColor bool) Formatter {
|
||||
if timeFormat == "" {
|
||||
timeFormat = consoleDefaultTimeFormat
|
||||
}
|
||||
if location == nil {
|
||||
location = time.Local
|
||||
}
|
||||
|
||||
return func(i interface{}) string {
|
||||
t := "<nil>"
|
||||
switch tt := i.(type) {
|
||||
case string:
|
||||
ts, err := time.ParseInLocation(TimeFieldFormat, tt, location)
|
||||
if err != nil {
|
||||
t = tt
|
||||
} else {
|
||||
t = ts.In(location).Format(timeFormat)
|
||||
}
|
||||
case json.Number:
|
||||
i, err := tt.Int64()
|
||||
if err != nil {
|
||||
t = tt.String()
|
||||
} else {
|
||||
var sec, nsec int64
|
||||
|
||||
switch TimeFieldFormat {
|
||||
case TimeFormatUnixNano:
|
||||
sec, nsec = 0, i
|
||||
case TimeFormatUnixMicro:
|
||||
sec, nsec = 0, int64(time.Duration(i)*time.Microsecond)
|
||||
case TimeFormatUnixMs:
|
||||
sec, nsec = 0, int64(time.Duration(i)*time.Millisecond)
|
||||
default:
|
||||
sec, nsec = i, 0
|
||||
}
|
||||
|
||||
ts := time.Unix(sec, nsec)
|
||||
t = ts.In(location).Format(timeFormat)
|
||||
}
|
||||
}
|
||||
return colorize(t, colorDarkGray, noColor)
|
||||
}
|
||||
}
|
||||
|
||||
func stripLevel(ll string) string {
|
||||
if len(ll) == 0 {
|
||||
return unknownLevel
|
||||
}
|
||||
if len(ll) > 3 {
|
||||
ll = ll[:3]
|
||||
}
|
||||
return strings.ToUpper(ll)
|
||||
}
|
||||
|
||||
func consoleDefaultFormatLevel(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
if ll, ok := i.(string); ok {
|
||||
level, _ := ParseLevel(ll)
|
||||
fl, ok := FormattedLevels[level]
|
||||
if ok {
|
||||
return colorize(fl, LevelColors[level], noColor)
|
||||
}
|
||||
return stripLevel(ll)
|
||||
}
|
||||
if i == nil {
|
||||
return unknownLevel
|
||||
}
|
||||
return stripLevel(fmt.Sprintf("%s", i))
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatCaller(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
var c string
|
||||
if cc, ok := i.(string); ok {
|
||||
c = cc
|
||||
}
|
||||
if len(c) > 0 {
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
if rel, err := filepath.Rel(cwd, c); err == nil {
|
||||
c = rel
|
||||
}
|
||||
}
|
||||
c = colorize(c, colorBold, noColor) + colorize(" >", colorCyan, noColor)
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatMessage(noColor bool, level interface{}) Formatter {
|
||||
return func(i interface{}) string {
|
||||
if i == nil || i == "" {
|
||||
return ""
|
||||
}
|
||||
switch level {
|
||||
case LevelInfoValue, LevelWarnValue, LevelErrorValue, LevelFatalValue, LevelPanicValue:
|
||||
return colorize(fmt.Sprintf("%s", i), colorBold, noColor)
|
||||
default:
|
||||
return fmt.Sprintf("%s", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatFieldName(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
return colorize(fmt.Sprintf("%s=", i), colorCyan, noColor)
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatFieldValue(i interface{}) string {
|
||||
return fmt.Sprintf("%s", i)
|
||||
}
|
||||
|
||||
func consoleDefaultFormatErrFieldName(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
return colorize(fmt.Sprintf("%s=", i), colorCyan, noColor)
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatErrFieldValue(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
return colorize(colorize(fmt.Sprintf("%s", i), colorBold, noColor), colorRed, noColor)
|
||||
}
|
||||
}
|
||||
|
||||
+637
-13
@@ -2,29 +2,653 @@ package zerolog_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func ExampleConsoleWriter_Write() {
|
||||
func ExampleConsoleWriter() {
|
||||
log := zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true})
|
||||
|
||||
log.Info().Msg("hello world")
|
||||
// Output: <nil> |INFO| hello world
|
||||
log.Info().Str("foo", "bar").Msg("Hello World")
|
||||
// Output: <nil> INF Hello World foo=bar
|
||||
}
|
||||
|
||||
func TestConsoleWriterNumbers(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
log := zerolog.New(zerolog.ConsoleWriter{Out: buf, NoColor: true})
|
||||
log.Info().
|
||||
Float64("float", 1.23).
|
||||
Uint64("small", 123).
|
||||
Uint64("big", 1152921504606846976).
|
||||
Msg("msg")
|
||||
if got, want := strings.TrimSpace(buf.String()), "<nil> |INFO| msg big=1152921504606846976 float=1.23 small=123"; got != want {
|
||||
t.Errorf("\ngot:\n%s\nwant:\n%s", got, want)
|
||||
func ExampleConsoleWriter_customFormatters() {
|
||||
out := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true}
|
||||
out.FormatLevel = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%-6s|", i)) }
|
||||
out.FormatFieldName = func(i interface{}) string { return fmt.Sprintf("%s:", i) }
|
||||
out.FormatFieldValue = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%s", i)) }
|
||||
log := zerolog.New(out)
|
||||
|
||||
log.Info().Str("foo", "bar").Msg("Hello World")
|
||||
// Output: <nil> INFO | Hello World foo:BAR
|
||||
}
|
||||
|
||||
func ExampleConsoleWriter_partValueFormatter() {
|
||||
out := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true,
|
||||
PartsOrder: []string{"level", "one", "two", "three", "message"},
|
||||
FieldsExclude: []string{"one", "two", "three"}}
|
||||
out.FormatLevel = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%-6s", i)) }
|
||||
out.FormatFieldName = func(i interface{}) string { return fmt.Sprintf("%s:", i) }
|
||||
out.FormatPartValueByName = func(i interface{}, s string) string {
|
||||
var ret string
|
||||
switch s {
|
||||
case "one":
|
||||
ret = strings.ToUpper(fmt.Sprintf("%s", i))
|
||||
case "two":
|
||||
ret = strings.ToLower(fmt.Sprintf("%s", i))
|
||||
case "three":
|
||||
ret = strings.ToLower(fmt.Sprintf("(%s)", i))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
log := zerolog.New(out)
|
||||
|
||||
log.Info().Str("foo", "bar").
|
||||
Str("two", "TEST_TWO").
|
||||
Str("one", "test_one").
|
||||
Str("three", "test_three").
|
||||
Msg("Hello World")
|
||||
// Output: INFO TEST_ONE test_two (test_three) Hello World foo:bar
|
||||
}
|
||||
|
||||
func ExampleNewConsoleWriter() {
|
||||
out := zerolog.NewConsoleWriter()
|
||||
out.NoColor = true // For testing purposes only
|
||||
log := zerolog.New(out)
|
||||
|
||||
log.Debug().Str("foo", "bar").Msg("Hello World")
|
||||
// Output: <nil> DBG Hello World foo=bar
|
||||
}
|
||||
|
||||
func ExampleNewConsoleWriter_customFormatters() {
|
||||
out := zerolog.NewConsoleWriter(
|
||||
func(w *zerolog.ConsoleWriter) {
|
||||
// Customize time format
|
||||
w.TimeFormat = time.RFC822
|
||||
// Customize level formatting
|
||||
w.FormatLevel = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("[%-5s]", i)) }
|
||||
},
|
||||
)
|
||||
out.NoColor = true // For testing purposes only
|
||||
|
||||
log := zerolog.New(out)
|
||||
|
||||
log.Info().Str("foo", "bar").Msg("Hello World")
|
||||
// Output: <nil> [INFO ] Hello World foo=bar
|
||||
}
|
||||
|
||||
func TestConsoleLogger(t *testing.T) {
|
||||
t.Run("Numbers", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
log := zerolog.New(zerolog.ConsoleWriter{Out: buf, NoColor: true})
|
||||
log.Info().
|
||||
Float64("float", 1.23).
|
||||
Uint64("small", 123).
|
||||
Uint64("big", 1152921504606846976).
|
||||
Msg("msg")
|
||||
if got, want := strings.TrimSpace(buf.String()), "<nil> INF msg big=1152921504606846976 float=1.23 small=123"; got != want {
|
||||
t.Errorf("\ngot:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConsoleWriter(t *testing.T) {
|
||||
t.Run("Default field formatter", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true, PartsOrder: []string{"foo"}}
|
||||
|
||||
_, err := w.Write([]byte(`{"foo": "DEFAULT"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "DEFAULT foo=DEFAULT\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Write colorized", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: false}
|
||||
|
||||
_, err := w.Write([]byte(`{"level": "warn", "message": "Foobar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "\x1b[90m<nil>\x1b[0m \x1b[33mWRN\x1b[0m \x1b[1mFoobar\x1b[0m\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NO_COLOR = true", func(t *testing.T) {
|
||||
os.Setenv("NO_COLOR", "anything")
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf}
|
||||
|
||||
_, err := w.Write([]byte(`{"level": "warn", "message": "Foobar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "<nil> WRN Foobar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
os.Unsetenv("NO_COLOR")
|
||||
})
|
||||
|
||||
t.Run("Write fields", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true}
|
||||
|
||||
ts := time.Unix(0, 0)
|
||||
d := ts.UTC().Format(time.RFC3339)
|
||||
_, err := w.Write([]byte(`{"time": "` + d + `", "level": "debug", "message": "Foobar", "foo": "bar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := ts.Format(time.Kitchen) + " DBG Foobar foo=bar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unix timestamp input format", func(t *testing.T) {
|
||||
of := zerolog.TimeFieldFormat
|
||||
defer func() {
|
||||
zerolog.TimeFieldFormat = of
|
||||
}()
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, TimeFormat: time.StampMilli, NoColor: true}
|
||||
|
||||
_, err := w.Write([]byte(`{"time": 1234, "level": "debug", "message": "Foobar", "foo": "bar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := time.Unix(1234, 0).Format(time.StampMilli) + " DBG Foobar foo=bar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unix timestamp ms input format", func(t *testing.T) {
|
||||
of := zerolog.TimeFieldFormat
|
||||
defer func() {
|
||||
zerolog.TimeFieldFormat = of
|
||||
}()
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, TimeFormat: time.StampMilli, NoColor: true}
|
||||
|
||||
_, err := w.Write([]byte(`{"time": 1234567, "level": "debug", "message": "Foobar", "foo": "bar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := time.Unix(1234, 567000000).Format(time.StampMilli) + " DBG Foobar foo=bar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unix timestamp us input format", func(t *testing.T) {
|
||||
of := zerolog.TimeFieldFormat
|
||||
defer func() {
|
||||
zerolog.TimeFieldFormat = of
|
||||
}()
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, TimeFormat: time.StampMicro, NoColor: true}
|
||||
|
||||
_, err := w.Write([]byte(`{"time": 1234567891, "level": "debug", "message": "Foobar", "foo": "bar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := time.Unix(1234, 567891000).Format(time.StampMicro) + " DBG Foobar foo=bar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No message field", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true}
|
||||
|
||||
_, err := w.Write([]byte(`{"level": "debug", "foo": "bar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "<nil> DBG foo=bar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No level field", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true}
|
||||
|
||||
_, err := w.Write([]byte(`{"message": "Foobar", "foo": "bar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "<nil> ??? Foobar foo=bar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Write colorized fields", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: false}
|
||||
|
||||
_, err := w.Write([]byte(`{"level": "warn", "message": "Foobar", "foo": "bar"}`))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "\x1b[90m<nil>\x1b[0m \x1b[33mWRN\x1b[0m \x1b[1mFoobar\x1b[0m \x1b[36mfoo=\x1b[0mbar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Write error field", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true}
|
||||
|
||||
ts := time.Unix(0, 0)
|
||||
d := ts.UTC().Format(time.RFC3339)
|
||||
evt := `{"time": "` + d + `", "level": "error", "message": "Foobar", "aaa": "bbb", "error": "Error"}`
|
||||
// t.Log(evt)
|
||||
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := ts.Format(time.Kitchen) + " ERR Foobar error=Error aaa=bbb\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Write caller field", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot get working directory: %s", err)
|
||||
}
|
||||
|
||||
ts := time.Unix(0, 0)
|
||||
d := ts.UTC().Format(time.RFC3339)
|
||||
|
||||
fields := map[string]interface{}{
|
||||
"time": d,
|
||||
"level": "debug",
|
||||
"message": "Foobar",
|
||||
"foo": "bar",
|
||||
"caller": filepath.Join(cwd, "foo", "bar.go"),
|
||||
}
|
||||
|
||||
evt, err := json.Marshal(fields)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot marshal fields: %s", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(evt)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
// Define the expected output with forward slashes
|
||||
expectedOutput := ts.Format(time.Kitchen) + " DBG foo/bar.go > Foobar foo=bar\n"
|
||||
|
||||
// Get the actual output and normalize path separators to forward slashes
|
||||
actualOutput := buf.String()
|
||||
actualOutput = strings.ReplaceAll(actualOutput, string(os.PathSeparator), "/")
|
||||
|
||||
// Compare the normalized actual output to the expected output
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Write JSON field", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true}
|
||||
|
||||
evt := `{"level": "debug", "message": "Foobar", "foo": [1, 2, 3], "bar": true}`
|
||||
// t.Log(evt)
|
||||
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "<nil> DBG Foobar bar=true foo=[1,2,3]\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("With an extra 'level' field", func(t *testing.T) {
|
||||
t.Run("malformed string", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
field string
|
||||
output string
|
||||
}{
|
||||
{"", "<nil> ??? Hello World foo=bar\n"},
|
||||
{"-", "<nil> - Hello World foo=bar\n"},
|
||||
{"1", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
|
||||
{"a", "<nil> A Hello World foo=bar\n"},
|
||||
{"12", "<nil> 12 Hello World foo=bar\n"},
|
||||
{"a2", "<nil> A2 Hello World foo=bar\n"},
|
||||
{"2a", "<nil> 2A Hello World foo=bar\n"},
|
||||
{"ab", "<nil> AB Hello World foo=bar\n"},
|
||||
{"12a", "<nil> 12A Hello World foo=bar\n"},
|
||||
{"a12", "<nil> A12 Hello World foo=bar\n"},
|
||||
{"abc", "<nil> ABC Hello World foo=bar\n"},
|
||||
{"123", "<nil> 123 Hello World foo=bar\n"},
|
||||
{"abcd", "<nil> ABC Hello World foo=bar\n"},
|
||||
{"1234", "<nil> 123 Hello World foo=bar\n"},
|
||||
{"123d", "<nil> 123 Hello World foo=bar\n"},
|
||||
{"01", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
|
||||
{"001", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
|
||||
{"0001", "<nil> " + zerolog.FormattedLevels[1] + " Hello World foo=bar\n"},
|
||||
}
|
||||
for i, c := range cases {
|
||||
c := c
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
out := zerolog.NewConsoleWriter()
|
||||
out.NoColor = true
|
||||
out.Out = buf
|
||||
log := zerolog.New(out)
|
||||
|
||||
log.Debug().Str("level", c.field).Str("foo", "bar").Msg("Hello World")
|
||||
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != c.output {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, c.output)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("weird value", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
field interface{}
|
||||
output string
|
||||
}{
|
||||
{0, "<nil> 0 Hello World foo=bar\n"},
|
||||
{1, "<nil> 1 Hello World foo=bar\n"},
|
||||
{-1, "<nil> -1 Hello World foo=bar\n"},
|
||||
{-3, "<nil> -3 Hello World foo=bar\n"},
|
||||
{-32, "<nil> -32 Hello World foo=bar\n"},
|
||||
{-321, "<nil> -32 Hello World foo=bar\n"},
|
||||
{12, "<nil> 12 Hello World foo=bar\n"},
|
||||
{123, "<nil> 123 Hello World foo=bar\n"},
|
||||
{1234, "<nil> 123 Hello World foo=bar\n"},
|
||||
}
|
||||
for i, c := range cases {
|
||||
c := c
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
out := zerolog.NewConsoleWriter()
|
||||
out.NoColor = true
|
||||
out.Out = buf
|
||||
log := zerolog.New(out)
|
||||
|
||||
log.Debug().Interface("level", c.field).Str("foo", "bar").Msg("Hello World")
|
||||
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != c.output {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, c.output)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestConsoleWriterConfiguration(t *testing.T) {
|
||||
t.Run("Sets TimeFormat", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true, TimeFormat: time.RFC3339}
|
||||
|
||||
ts := time.Unix(0, 0)
|
||||
d := ts.UTC().Format(time.RFC3339)
|
||||
evt := `{"time": "` + d + `", "level": "info", "message": "Foobar"}`
|
||||
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := ts.Format(time.RFC3339) + " INF Foobar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets TimeFormat and TimeLocation", func(t *testing.T) {
|
||||
locs := []*time.Location{time.Local, time.UTC}
|
||||
|
||||
for _, location := range locs {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{
|
||||
Out: buf,
|
||||
NoColor: true,
|
||||
TimeFormat: time.RFC3339,
|
||||
TimeLocation: location,
|
||||
}
|
||||
|
||||
ts := time.Unix(0, 0)
|
||||
d := ts.UTC().Format(time.RFC3339)
|
||||
evt := `{"time": "` + d + `", "level": "info", "message": "Foobar"}`
|
||||
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := ts.In(location).Format(time.RFC3339) + " INF Foobar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q (location=%s)", actualOutput, expectedOutput, location)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets PartsOrder", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true, PartsOrder: []string{"message", "level"}}
|
||||
|
||||
evt := `{"level": "info", "message": "Foobar"}`
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "Foobar INF\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets PartsExclude", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true, PartsExclude: []string{"time"}}
|
||||
|
||||
d := time.Unix(0, 0).UTC().Format(time.RFC3339)
|
||||
evt := `{"time": "` + d + `", "level": "info", "message": "Foobar"}`
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "INF Foobar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets FieldsOrder", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true, FieldsOrder: []string{"zebra", "aardvark"}}
|
||||
|
||||
evt := `{"level": "info", "message": "Zoo", "aardvark": "Able", "mussel": "Mountain", "zebra": "Zulu"}`
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "<nil> INF Zoo zebra=Zulu aardvark=Able mussel=Mountain\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets FieldsExclude", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true, FieldsExclude: []string{"foo"}}
|
||||
|
||||
evt := `{"level": "info", "message": "Foobar", "foo":"bar", "baz":"quux"}`
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "<nil> INF Foobar baz=quux\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets FormatExtra", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{
|
||||
Out: buf, NoColor: true, PartsOrder: []string{"level", "message"},
|
||||
FormatExtra: func(evt map[string]interface{}, buf *bytes.Buffer) error {
|
||||
buf.WriteString("\nAdditional stacktrace")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
evt := `{"level": "info", "message": "Foobar"}`
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "INF Foobar\nAdditional stacktrace\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets FormatPrepare", func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{
|
||||
Out: buf, NoColor: true, PartsOrder: []string{"level", "message"},
|
||||
FormatPrepare: func(evt map[string]interface{}) error {
|
||||
evt["message"] = fmt.Sprintf("msg=%s", evt["message"])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
evt := `{"level": "info", "message": "Foobar"}`
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
expectedOutput := "INF msg=Foobar\n"
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Uses local time for console writer without time zone", func(t *testing.T) {
|
||||
// Regression test for issue #483 (check there for more details)
|
||||
|
||||
timeFormat := "2006-01-02 15:04:05"
|
||||
expectedOutput := "2022-10-20 20:24:50 INF Foobar\n"
|
||||
evt := `{"time": "2022-10-20 20:24:50", "level": "info", "message": "Foobar"}`
|
||||
|
||||
of := zerolog.TimeFieldFormat
|
||||
defer func() {
|
||||
zerolog.TimeFieldFormat = of
|
||||
}()
|
||||
zerolog.TimeFieldFormat = timeFormat
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w := zerolog.ConsoleWriter{Out: buf, NoColor: true, TimeFormat: timeFormat}
|
||||
_, err := w.Write([]byte(evt))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when writing output: %s", err)
|
||||
}
|
||||
|
||||
actualOutput := buf.String()
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("Unexpected output %q, want: %q", actualOutput, expectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkConsoleWriter(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
var msg = []byte(`{"level": "info", "foo": "bar", "message": "HELLO", "time": "1990-01-01"}`)
|
||||
|
||||
w := zerolog.ConsoleWriter{Out: io.Discard, NoColor: false}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Write(msg)
|
||||
}
|
||||
}
|
||||
|
||||
+205
-51
@@ -1,7 +1,9 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
@@ -16,9 +18,11 @@ func (c Context) Logger() Logger {
|
||||
return c.l
|
||||
}
|
||||
|
||||
// Fields is a helper function to use a map to set fields using type assertion.
|
||||
func (c Context) Fields(fields map[string]interface{}) Context {
|
||||
c.l.context = appendFields(c.l.context, fields)
|
||||
// Fields is a helper function to use a map or slice to set fields using type assertion.
|
||||
// Only map[string]interface{} and []interface{} are accepted. []interface{} must
|
||||
// alternate string keys and arbitrary values, and extraneous ones are ignored.
|
||||
func (c Context) Fields(fields interface{}) Context {
|
||||
c.l.context = appendFields(c.l.context, fields, c.l.stack, c.l.ctx, c.l.hooks)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -30,8 +34,28 @@ func (c Context) Dict(key string, dict *Event) Context {
|
||||
return c
|
||||
}
|
||||
|
||||
// CreateDict creates an Event to be used with the Context.Dict method.
|
||||
// It preserves the stack, hooks, and context from the logger.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the Context.Dict method.
|
||||
func (c Context) CreateDict() *Event {
|
||||
return newEvent(nil, DebugLevel, c.l.stack, c.l.ctx, c.l.hooks)
|
||||
}
|
||||
|
||||
// CreateArray creates an Array to be used with the Context.Array method.
|
||||
// It preserves the stack, hooks, and context from the logger.
|
||||
// Call usual field methods like Str, Int etc to add elements to this
|
||||
// array and give it as argument the Context.Array method.
|
||||
func (c Context) CreateArray() *Array {
|
||||
a := Arr()
|
||||
a.stack = c.l.stack
|
||||
a.ctx = c.l.ctx
|
||||
a.ch = c.l.hooks
|
||||
return a
|
||||
}
|
||||
|
||||
// Array adds the field key with an array to the event context.
|
||||
// Use zerolog.Arr() to create the array or pass a type that
|
||||
// Use c.CreateArray() to create the array or pass a type that
|
||||
// implement the LogArrayMarshaler interface.
|
||||
func (c Context) Array(key string, arr LogArrayMarshaler) Context {
|
||||
c.l.context = enc.AppendKey(c.l.context, key)
|
||||
@@ -39,29 +63,44 @@ func (c Context) Array(key string, arr LogArrayMarshaler) Context {
|
||||
c.l.context = arr.write(c.l.context)
|
||||
return c
|
||||
}
|
||||
var a *Array
|
||||
if aa, ok := arr.(*Array); ok {
|
||||
a = aa
|
||||
} else {
|
||||
a = Arr()
|
||||
arr.MarshalZerologArray(a)
|
||||
}
|
||||
a := c.CreateArray()
|
||||
arr.MarshalZerologArray(a)
|
||||
c.l.context = a.write(c.l.context)
|
||||
return c
|
||||
}
|
||||
|
||||
// Object marshals an object that implement the LogObjectMarshaler interface.
|
||||
func (c Context) Object(key string, obj LogObjectMarshaler) Context {
|
||||
e := newEvent(levelWriterAdapter{ioutil.Discard}, 0)
|
||||
e := c.l.scratchEvent()
|
||||
e.Object(key, obj)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
return c
|
||||
}
|
||||
|
||||
// Objects adds the field key with objs to the logger context as an array of
|
||||
// objects that implement the LogObjectMarshaler interface.
|
||||
//
|
||||
// This is the array version that accepts a slice of LogObjectMarshaler objects.
|
||||
func (c Context) Objects(key string, objs []LogObjectMarshaler) Context {
|
||||
e := c.l.scratchEvent()
|
||||
e.Objects(key, objs)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
return c
|
||||
}
|
||||
|
||||
// ObjectsV adds the field key with objs to the logger context as an array of
|
||||
// objects that implement the LogObjectMarshaler interface.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual LogObjectMarshaler objects.
|
||||
func (c Context) ObjectsV(key string, objs ...LogObjectMarshaler) Context {
|
||||
return c.Objects(key, objs)
|
||||
}
|
||||
|
||||
// EmbedObject marshals and Embeds an object that implement the LogObjectMarshaler interface.
|
||||
func (c Context) EmbedObject(obj LogObjectMarshaler) Context {
|
||||
e := newEvent(levelWriterAdapter{ioutil.Discard}, 0)
|
||||
e := c.l.scratchEvent()
|
||||
e.EmbedObject(obj)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
@@ -75,11 +114,49 @@ func (c Context) Str(key, val string) Context {
|
||||
}
|
||||
|
||||
// Strs adds the field key with val as a string to the logger context.
|
||||
//
|
||||
// This is the array version that accepts a slice of string values.
|
||||
func (c Context) Strs(key string, vals []string) Context {
|
||||
c.l.context = enc.AppendStrings(enc.AppendKey(c.l.context, key), vals)
|
||||
return c
|
||||
}
|
||||
|
||||
// StrsV adds the field key with vals as a []string to the logger context.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual strings.
|
||||
func (c Context) StrsV(key string, vals ...string) Context {
|
||||
return c.Strs(key, vals)
|
||||
}
|
||||
|
||||
// Stringer adds the field key with val.String() (or null if val is nil) to the logger context.
|
||||
func (c Context) Stringer(key string, val fmt.Stringer) Context {
|
||||
if val != nil {
|
||||
c.l.context = enc.AppendString(enc.AppendKey(c.l.context, key), val.String())
|
||||
return c
|
||||
}
|
||||
|
||||
c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), nil)
|
||||
return c
|
||||
}
|
||||
|
||||
// Stringers adds the field key with vals to the logger context where each
|
||||
// individual val is added by calling val.String().
|
||||
//
|
||||
// This is the array version that accepts a slice of fmt.Stringer values.
|
||||
func (c Context) Stringers(key string, vals []fmt.Stringer) Context {
|
||||
c.l.context = enc.AppendStringers(enc.AppendKey(c.l.context, key), vals)
|
||||
return c
|
||||
}
|
||||
|
||||
// StringersV adds the field key with vals to the logger context where each
|
||||
// individual val is added by calling val.String().
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual
|
||||
// fmt.Stringer values.
|
||||
func (c Context) StringersV(key string, vals ...fmt.Stringer) Context {
|
||||
return c.Stringers(key, vals)
|
||||
}
|
||||
|
||||
// Bytes adds the field key with val as a []byte to the logger context.
|
||||
func (c Context) Bytes(key string, val []byte) Context {
|
||||
c.l.context = enc.AppendBytes(enc.AppendKey(c.l.context, key), val)
|
||||
@@ -102,14 +179,17 @@ func (c Context) RawJSON(key string, b []byte) Context {
|
||||
}
|
||||
|
||||
// AnErr adds the field key with serialized err to the logger context.
|
||||
// If err is nil, no field is added.
|
||||
func (c Context) AnErr(key string, err error) Context {
|
||||
marshaled := ErrorMarshalFunc(err)
|
||||
switch m := marshaled.(type) {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
return c
|
||||
case LogObjectMarshaler:
|
||||
return c.Object(key, m)
|
||||
case error:
|
||||
if isNilValue(m) {
|
||||
return c
|
||||
}
|
||||
return c.Str(key, m.Error())
|
||||
case string:
|
||||
return c.Str(key, m)
|
||||
@@ -121,29 +201,39 @@ func (c Context) AnErr(key string, err error) Context {
|
||||
// Errs adds the field key with errs as an array of serialized errors to the
|
||||
// logger context.
|
||||
func (c Context) Errs(key string, errs []error) Context {
|
||||
arr := Arr()
|
||||
for _, err := range errs {
|
||||
marshaled := ErrorMarshalFunc(err)
|
||||
switch m := marshaled.(type) {
|
||||
case LogObjectMarshaler:
|
||||
arr = arr.Object(m)
|
||||
case error:
|
||||
arr = arr.Str(m.Error())
|
||||
case string:
|
||||
arr = arr.Str(m)
|
||||
default:
|
||||
arr = arr.Interface(m)
|
||||
}
|
||||
}
|
||||
|
||||
arr := c.CreateArray().Errs(errs)
|
||||
return c.Array(key, arr)
|
||||
}
|
||||
|
||||
// Err adds the field "error" with serialized err to the logger context.
|
||||
func (c Context) Err(err error) Context {
|
||||
if c.l.stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(err).(type) {
|
||||
case nil:
|
||||
return c // do nothing with nil errors
|
||||
case LogObjectMarshaler:
|
||||
c = c.Object(ErrorStackFieldName, m)
|
||||
case error:
|
||||
c = c.Str(ErrorStackFieldName, m.Error())
|
||||
case string:
|
||||
c = c.Str(ErrorStackFieldName, m)
|
||||
default:
|
||||
c = c.Interface(ErrorStackFieldName, m)
|
||||
}
|
||||
}
|
||||
|
||||
return c.AnErr(ErrorFieldName, err)
|
||||
}
|
||||
|
||||
// Ctx adds the context.Context to the logger context. The context.Context is
|
||||
// not rendered in the error message, but is made available for hooks to use.
|
||||
// A typical use case is to extract tracing information from the
|
||||
// context.Context.
|
||||
func (c Context) Ctx(ctx context.Context) Context {
|
||||
c.l.ctx = ctx
|
||||
return c
|
||||
}
|
||||
|
||||
// Bool adds the field key with val as a bool to the logger context.
|
||||
func (c Context) Bool(key string, b bool) Context {
|
||||
c.l.context = enc.AppendBool(enc.AppendKey(c.l.context, key), b)
|
||||
@@ -278,25 +368,25 @@ func (c Context) Uints64(key string, i []uint64) Context {
|
||||
|
||||
// Float32 adds the field key with f as a float32 to the logger context.
|
||||
func (c Context) Float32(key string, f float32) Context {
|
||||
c.l.context = enc.AppendFloat32(enc.AppendKey(c.l.context, key), f)
|
||||
c.l.context = enc.AppendFloat32(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Floats32 adds the field key with f as a []float32 to the logger context.
|
||||
func (c Context) Floats32(key string, f []float32) Context {
|
||||
c.l.context = enc.AppendFloats32(enc.AppendKey(c.l.context, key), f)
|
||||
c.l.context = enc.AppendFloats32(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Float64 adds the field key with f as a float64 to the logger context.
|
||||
func (c Context) Float64(key string, f float64) Context {
|
||||
c.l.context = enc.AppendFloat64(enc.AppendKey(c.l.context, key), f)
|
||||
c.l.context = enc.AppendFloat64(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Floats64 adds the field key with f as a []float64 to the logger context.
|
||||
func (c Context) Floats64(key string, f []float64) Context {
|
||||
c.l.context = enc.AppendFloats64(enc.AppendKey(c.l.context, key), f)
|
||||
c.l.context = enc.AppendFloats64(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -308,8 +398,9 @@ func (ts timestampHook) Run(e *Event, level Level, msg string) {
|
||||
|
||||
var th = timestampHook{}
|
||||
|
||||
// Timestamp adds the current local time as UNIX timestamp to the logger context with the "time" key.
|
||||
// Timestamp adds the current local time to the logger context with the "time" key, formatted using zerolog.TimeFieldFormat.
|
||||
// To customize the key name, change zerolog.TimestampFieldName.
|
||||
// To customize the time format, change zerolog.TimeFieldFormat.
|
||||
//
|
||||
// NOTE: It won't dedupe the "time" key if the *Context has one already.
|
||||
func (c Context) Timestamp() Context {
|
||||
@@ -317,44 +408,81 @@ func (c Context) Timestamp() Context {
|
||||
return c
|
||||
}
|
||||
|
||||
// Time adds the field key with t formated as string using zerolog.TimeFieldFormat.
|
||||
// Time adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (c Context) Time(key string, t time.Time) Context {
|
||||
c.l.context = enc.AppendTime(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
|
||||
return c
|
||||
}
|
||||
|
||||
// Times adds the field key with t formated as string using zerolog.TimeFieldFormat.
|
||||
// Times adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (c Context) Times(key string, t []time.Time) Context {
|
||||
c.l.context = enc.AppendTimes(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
|
||||
return c
|
||||
}
|
||||
|
||||
// Dur adds the fields key with d divided by unit and stored as a float.
|
||||
// Dur adds the field key with d divided by unit and stored as a float.
|
||||
func (c Context) Dur(key string, d time.Duration) Context {
|
||||
c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger)
|
||||
c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Durs adds the fields key with d divided by unit and stored as a float.
|
||||
// Durs adds the field key with d divided by unit and stored as a float.
|
||||
func (c Context) Durs(key string, d []time.Duration) Context {
|
||||
c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldInteger)
|
||||
c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Interface adds the field key with obj marshaled using reflection.
|
||||
func (c Context) Interface(key string, i interface{}) Context {
|
||||
if obj, ok := i.(LogObjectMarshaler); ok {
|
||||
return c.Object(key, obj)
|
||||
}
|
||||
c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
type callerHook struct{}
|
||||
|
||||
func (ch callerHook) Run(e *Event, level Level, msg string) {
|
||||
// Three extra frames to skip (added by hook infra).
|
||||
e.caller(CallerSkipFrameCount + 3)
|
||||
// Type adds the field key with val's type using reflection.
|
||||
func (c Context) Type(key string, val interface{}) Context {
|
||||
c.l.context = enc.AppendType(enc.AppendKey(c.l.context, key), val)
|
||||
return c
|
||||
}
|
||||
|
||||
var ch = callerHook{}
|
||||
// Any is a wrapper around Context.Interface.
|
||||
func (c Context) Any(key string, i interface{}) Context {
|
||||
return c.Interface(key, i)
|
||||
}
|
||||
|
||||
// Reset removes all the context fields.
|
||||
func (c Context) Reset() Context {
|
||||
c.l.context = enc.AppendBeginMarker(make([]byte, 0, 500))
|
||||
return c
|
||||
}
|
||||
|
||||
type callerHook struct {
|
||||
callerSkipFrameCount int
|
||||
}
|
||||
|
||||
func newCallerHook(skipFrameCount int) callerHook {
|
||||
return callerHook{callerSkipFrameCount: skipFrameCount}
|
||||
}
|
||||
|
||||
func (ch callerHook) Run(e *Event, level Level, msg string) {
|
||||
switch ch.callerSkipFrameCount {
|
||||
case useGlobalSkipFrameCount:
|
||||
// Extra frames to skip (added by hook infra).
|
||||
e.caller(CallerSkipFrameCount + contextCallerSkipFrameCount)
|
||||
default:
|
||||
// Extra frames to skip (added by hook infra).
|
||||
e.caller(ch.callerSkipFrameCount + contextCallerSkipFrameCount)
|
||||
}
|
||||
}
|
||||
|
||||
// useGlobalSkipFrameCount acts as a flag to informat callerHook.Run
|
||||
// to use the global CallerSkipFrameCount.
|
||||
const useGlobalSkipFrameCount = math.MinInt32
|
||||
|
||||
// ch is the default caller hook using the global CallerSkipFrameCount.
|
||||
var ch = newCallerHook(useGlobalSkipFrameCount)
|
||||
|
||||
// Caller adds the file:line of the caller with the zerolog.CallerFieldName key.
|
||||
func (c Context) Caller() Context {
|
||||
@@ -362,19 +490,45 @@ func (c Context) Caller() Context {
|
||||
return c
|
||||
}
|
||||
|
||||
// IPAddr adds IPv4 or IPv6 Address to the context
|
||||
// CallerWithSkipFrameCount adds the file:line of the caller with the zerolog.CallerFieldName key.
|
||||
// The specified skipFrameCount int will override the global CallerSkipFrameCount for this context's respective logger.
|
||||
// If set to -1 the global CallerSkipFrameCount will be used.
|
||||
func (c Context) CallerWithSkipFrameCount(skipFrameCount int) Context {
|
||||
c.l = c.l.Hook(newCallerHook(skipFrameCount))
|
||||
return c
|
||||
}
|
||||
|
||||
// Stack enables stack trace printing for the error passed to Err().
|
||||
func (c Context) Stack() Context {
|
||||
c.l.stack = true
|
||||
return c
|
||||
}
|
||||
|
||||
// IPAddr adds adds the field key with ip as a net.IP IPv4 or IPv6 Address to the context
|
||||
func (c Context) IPAddr(key string, ip net.IP) Context {
|
||||
c.l.context = enc.AppendIPAddr(enc.AppendKey(c.l.context, key), ip)
|
||||
return c
|
||||
}
|
||||
|
||||
// IPPrefix adds IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
// IPAddrs adds the field key with ip as a []net.IP array of IPv4 or IPv6 Address to the context
|
||||
func (c Context) IPAddrs(key string, ip []net.IP) Context {
|
||||
c.l.context = enc.AppendIPAddrs(enc.AppendKey(c.l.context, key), ip)
|
||||
return c
|
||||
}
|
||||
|
||||
// IPPrefix adds adds the field key with pfx as a []net.IPNet IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
func (c Context) IPPrefix(key string, pfx net.IPNet) Context {
|
||||
c.l.context = enc.AppendIPPrefix(enc.AppendKey(c.l.context, key), pfx)
|
||||
return c
|
||||
}
|
||||
|
||||
// MACAddr adds MAC address to the context
|
||||
// IPPrefix adds adds the field key with pfx as a []net.IPNet array of IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
func (c Context) IPPrefixes(key string, pfx []net.IPNet) Context {
|
||||
c.l.context = enc.AppendIPPrefixes(enc.AppendKey(c.l.context, key), pfx)
|
||||
return c
|
||||
}
|
||||
|
||||
// MACAddr adds adds the field key with ha as a net.HardwareAddr MAC address to the context
|
||||
func (c Context) MACAddr(key string, ha net.HardwareAddr) Context {
|
||||
c.l.context = enc.AppendMACAddr(enc.AppendKey(c.l.context, key), ha)
|
||||
return c
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type myError struct{}
|
||||
|
||||
func (e *myError) Error() string { return "test" }
|
||||
|
||||
func TestContext_ErrWithStackMarshaler(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return "stack-trace"
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf).With().Stack().Err(errors.New("test error")).Logger()
|
||||
|
||||
log.Info().Msg("test message")
|
||||
|
||||
got := decodeIfBinaryToString(buf.Bytes())
|
||||
want := `{"level":"info","stack":"stack-trace","error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Context.Err() with stack marshaler = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_AnErrWithNilErrorMarshal(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorMarshalFunc
|
||||
defer func() { ErrorMarshalFunc = original }()
|
||||
|
||||
// Set marshaler to return a nil error pointer
|
||||
ErrorMarshalFunc = func(err error) interface{} {
|
||||
return (*myError)(nil) // nil pointer of error type
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf).With().AnErr("test", errors.New("some error")).Logger()
|
||||
|
||||
log.Info().Msg("test message")
|
||||
|
||||
got := decodeIfBinaryToString(buf.Bytes())
|
||||
want := `{"level":"info","message":"test message"}` + "\n" // No "test" field because isNilValue returned true
|
||||
if got != want {
|
||||
t.Errorf("Context.AnErr() with nil error marshal = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_ErrWithNilStackMarshaler(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set marshaler to return nil
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf).With().Stack().Err(errors.New("test error")).Logger()
|
||||
|
||||
log.Info().Msg("test message")
|
||||
|
||||
got := decodeIfBinaryToString(buf.Bytes())
|
||||
want := `{"level":"info","message":"test message"}` + "\n" // No stack or error field because stack marshaler returned nil
|
||||
if got != want {
|
||||
t.Errorf("Context.Err() with nil stack marshaler = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_ErrWithStackMarshalerObject(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns LogObjectMarshaler
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return logObjectMarshalerImpl{name: "user", age: 30}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf).With().Stack().Err(errors.New("test error")).Logger()
|
||||
|
||||
log.Info().Msg("test message")
|
||||
|
||||
got := decodeIfBinaryToString(buf.Bytes())
|
||||
want := `{"level":"info","stack":{"name":"user","age":-30},"error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Context.Err() with stack marshaler object = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_ErrWithStackMarshalerError(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns an error
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return errors.New("stack error")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf).With().Stack().Err(errors.New("test error")).Logger()
|
||||
|
||||
log.Info().Msg("test message")
|
||||
|
||||
got := decodeIfBinaryToString(buf.Bytes())
|
||||
want := `{"level":"info","stack":"stack error","error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Context.Err() with stack marshaler error = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_ErrWithStackMarshalerInterface(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns an int
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return 42
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf).With().Stack().Err(errors.New("test error")).Logger()
|
||||
|
||||
log.Info().Msg("test message")
|
||||
|
||||
got := decodeIfBinaryToString(buf.Bytes())
|
||||
want := `{"level":"info","stack":42,"error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Context.Err() with stack marshaler interface = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -7,41 +7,45 @@ import (
|
||||
var disabledLogger *Logger
|
||||
|
||||
func init() {
|
||||
SetGlobalLevel(TraceLevel)
|
||||
l := Nop()
|
||||
disabledLogger = &l
|
||||
}
|
||||
|
||||
type ctxKey struct{}
|
||||
|
||||
// WithContext returns a copy of ctx with l associated. If an instance of Logger
|
||||
// is already in the context, the context is not updated.
|
||||
// WithContext returns a copy of ctx with the receiver attached. The Logger
|
||||
// attached to the provided Context (if any) will not be effected. If the
|
||||
// receiver's log level is Disabled it will only be attached to the returned
|
||||
// Context if the provided Context has a previously attached Logger. If the
|
||||
// provided Context has no attached Logger, a Disabled Logger will not be
|
||||
// attached.
|
||||
//
|
||||
// For instance, to add a field to an existing logger in the context, use this
|
||||
// Note: to modify the existing Logger attached to a Context (instead of
|
||||
// replacing it in a new Context), use UpdateContext with the following
|
||||
// notation:
|
||||
//
|
||||
// ctx := r.Context()
|
||||
// l := zerolog.Ctx(ctx)
|
||||
// l.UpdateContext(func(c Context) Context {
|
||||
// return c.Str("bar", "baz")
|
||||
// })
|
||||
func (l *Logger) WithContext(ctx context.Context) context.Context {
|
||||
if lp, ok := ctx.Value(ctxKey{}).(*Logger); ok {
|
||||
if lp == l {
|
||||
// Do not store same logger.
|
||||
return ctx
|
||||
}
|
||||
} else if l.level == Disabled {
|
||||
// ctx := r.Context()
|
||||
// l := zerolog.Ctx(ctx)
|
||||
// l.UpdateContext(func(c Context) Context {
|
||||
// return c.Str("bar", "baz")
|
||||
// })
|
||||
func (l Logger) WithContext(ctx context.Context) context.Context {
|
||||
if _, ok := ctx.Value(ctxKey{}).(*Logger); !ok && l.level == Disabled {
|
||||
// Do not store disabled logger.
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, ctxKey{}, l)
|
||||
return context.WithValue(ctx, ctxKey{}, &l)
|
||||
}
|
||||
|
||||
// Ctx returns the Logger associated with the ctx. If no logger
|
||||
// is associated, a disabled logger is returned.
|
||||
// is associated, DefaultContextLogger is returned, unless DefaultContextLogger
|
||||
// is nil, in which case a disabled logger is returned.
|
||||
func Ctx(ctx context.Context) *Logger {
|
||||
if l, ok := ctx.Value(ctxKey{}).(*Logger); ok {
|
||||
return l
|
||||
} else if l = DefaultContextLogger; l != nil {
|
||||
return l
|
||||
}
|
||||
return disabledLogger
|
||||
}
|
||||
|
||||
+48
-9
@@ -1,14 +1,18 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/internal/cbor"
|
||||
)
|
||||
|
||||
func TestCtx(t *testing.T) {
|
||||
log := New(ioutil.Discard)
|
||||
log := New(io.Discard)
|
||||
ctx := log.WithContext(context.Background())
|
||||
log2 := Ctx(ctx)
|
||||
if !reflect.DeepEqual(log, *log2) {
|
||||
@@ -27,18 +31,25 @@ func TestCtx(t *testing.T) {
|
||||
if log2 != disabledLogger {
|
||||
t.Error("Ctx did not return the expected logger")
|
||||
}
|
||||
|
||||
DefaultContextLogger = &log
|
||||
t.Cleanup(func() { DefaultContextLogger = nil })
|
||||
log2 = Ctx(context.Background())
|
||||
if log2 != &log {
|
||||
t.Error("Ctx did not return the expected logger")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCtxDisabled(t *testing.T) {
|
||||
dl := New(ioutil.Discard).Level(Disabled)
|
||||
dl := New(io.Discard).Level(Disabled)
|
||||
ctx := dl.WithContext(context.Background())
|
||||
if ctx != context.Background() {
|
||||
t.Error("WithContext stored a disabled logger")
|
||||
}
|
||||
|
||||
l := New(ioutil.Discard).With().Str("foo", "bar").Logger()
|
||||
l := New(io.Discard).With().Str("foo", "bar").Logger()
|
||||
ctx = l.WithContext(ctx)
|
||||
if Ctx(ctx) != &l {
|
||||
if !reflect.DeepEqual(Ctx(ctx), &l) {
|
||||
t.Error("WithContext did not store logger")
|
||||
}
|
||||
|
||||
@@ -46,18 +57,46 @@ func TestCtxDisabled(t *testing.T) {
|
||||
return c.Str("bar", "baz")
|
||||
})
|
||||
ctx = l.WithContext(ctx)
|
||||
if Ctx(ctx) != &l {
|
||||
if !reflect.DeepEqual(Ctx(ctx), &l) {
|
||||
t.Error("WithContext did not store updated logger")
|
||||
}
|
||||
|
||||
l = l.Level(DebugLevel)
|
||||
ctx = l.WithContext(ctx)
|
||||
if Ctx(ctx) != &l {
|
||||
if !reflect.DeepEqual(Ctx(ctx), &l) {
|
||||
t.Error("WithContext did not store copied logger")
|
||||
}
|
||||
|
||||
ctx = dl.WithContext(ctx)
|
||||
if Ctx(ctx) != &dl {
|
||||
t.Error("WithContext did not overide logger with a disabled logger")
|
||||
if !reflect.DeepEqual(Ctx(ctx), &dl) {
|
||||
t.Error("WithContext did not override logger with a disabled logger")
|
||||
}
|
||||
}
|
||||
|
||||
type logObjectMarshalerImpl struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
func (t logObjectMarshalerImpl) MarshalZerologObject(e *Event) {
|
||||
e.Str("name", strings.ToLower(t.name)).Int("age", -t.age)
|
||||
}
|
||||
|
||||
func Test_InterfaceLogObjectMarshaler(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
ctx := log.WithContext(context.Background())
|
||||
|
||||
log2 := Ctx(ctx)
|
||||
|
||||
withLog := log2.With().Interface("obj", &logObjectMarshalerImpl{
|
||||
name: "FOO",
|
||||
age: 29,
|
||||
}).Logger()
|
||||
|
||||
withLog.Info().Msg("test")
|
||||
|
||||
if got, want := cbor.DecodeIfBinaryToString(buf.Bytes()), `{"level":"info","obj":{"name":"foo","age":-29},"message":"test"}`+"\n"; got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
+24
-11
@@ -19,12 +19,16 @@ var bufPool = &sync.Pool{
|
||||
|
||||
type Alerter func(missed int)
|
||||
|
||||
type diodeFetcher interface {
|
||||
diodes.Diode
|
||||
Next() diodes.GenericDataType
|
||||
}
|
||||
|
||||
// Writer is a io.Writer wrapper that uses a diode to make Write lock-free,
|
||||
// non-blocking and thread safe.
|
||||
type Writer struct {
|
||||
w io.Writer
|
||||
d *diodes.ManyToOne
|
||||
p *diodes.Poller
|
||||
d diodeFetcher
|
||||
c context.CancelFunc
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -35,25 +39,34 @@ type Writer struct {
|
||||
//
|
||||
// Use a diode.Writer when
|
||||
//
|
||||
// wr := diode.NewWriter(w, 1000, 10 * time.Millisecond, func(missed int) {
|
||||
// wr := diode.NewWriter(w, 1000, 0, func(missed int) {
|
||||
// log.Printf("Dropped %d messages", missed)
|
||||
// })
|
||||
// log := zerolog.New(wr)
|
||||
//
|
||||
// If pollInterval is greater than 0, a poller is used otherwise a waiter is
|
||||
// used.
|
||||
//
|
||||
// See code.cloudfoundry.org/go-diodes for more info on diode.
|
||||
func NewWriter(w io.Writer, size int, poolInterval time.Duration, f Alerter) Writer {
|
||||
func NewWriter(w io.Writer, size int, pollInterval time.Duration, f Alerter) Writer {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
d := diodes.NewManyToOne(size, diodes.AlertFunc(f))
|
||||
dw := Writer{
|
||||
w: w,
|
||||
d: d,
|
||||
p: diodes.NewPoller(d,
|
||||
diodes.WithPollingInterval(poolInterval),
|
||||
diodes.WithPollingContext(ctx)),
|
||||
w: w,
|
||||
c: cancel,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
if f == nil {
|
||||
f = func(int) {}
|
||||
}
|
||||
d := diodes.NewManyToOne(size, diodes.AlertFunc(f))
|
||||
if pollInterval > 0 {
|
||||
dw.d = diodes.NewPoller(d,
|
||||
diodes.WithPollingInterval(pollInterval),
|
||||
diodes.WithPollingContext(ctx))
|
||||
} else {
|
||||
dw.d = diodes.NewWaiter(d,
|
||||
diodes.WithWaiterContext(ctx))
|
||||
}
|
||||
go dw.poll()
|
||||
return dw
|
||||
}
|
||||
@@ -80,7 +93,7 @@ func (dw Writer) Close() error {
|
||||
func (dw Writer) poll() {
|
||||
defer close(dw.done)
|
||||
for {
|
||||
d := dw.p.Next()
|
||||
d := dw.d.Next()
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ package diode_test
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/diode"
|
||||
)
|
||||
|
||||
func ExampleNewWriter() {
|
||||
w := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
|
||||
w := diode.NewWriter(os.Stdout, 1000, 0, func(missed int) {
|
||||
fmt.Printf("Dropped %d messages\n", missed)
|
||||
})
|
||||
log := zerolog.New(w)
|
||||
|
||||
+139
-15
@@ -3,9 +3,11 @@ package diode_test
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -16,7 +18,7 @@ import (
|
||||
|
||||
func TestNewWriter(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
w := diode.NewWriter(&buf, 1000, 10*time.Millisecond, func(missed int) {
|
||||
w := diode.NewWriter(&buf, 1000, 0, func(missed int) {
|
||||
fmt.Printf("Dropped %d messages\n", missed)
|
||||
})
|
||||
log := zerolog.New(w)
|
||||
@@ -30,18 +32,140 @@ func TestNewWriter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark(b *testing.B) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
defer log.SetOutput(os.Stderr)
|
||||
w := diode.NewWriter(ioutil.Discard, 100000, 10*time.Millisecond, nil)
|
||||
func TestClose(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
w := diode.NewWriter(&buf, 1000, 0, func(missed int) {})
|
||||
log := zerolog.New(w)
|
||||
defer w.Close()
|
||||
|
||||
b.SetParallelism(1000)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
log.Print("test")
|
||||
}
|
||||
})
|
||||
|
||||
log.Print("test")
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func TestFatal(t *testing.T) {
|
||||
if os.Getenv("TEST_FATAL") == "1" {
|
||||
w := diode.NewWriter(os.Stderr, 1000, 0, func(missed int) {
|
||||
fmt.Printf("Dropped %d messages\n", missed)
|
||||
})
|
||||
defer w.Close()
|
||||
log := zerolog.New(w)
|
||||
log.Fatal().Msg("test")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestFatal")
|
||||
cmd.Env = append(os.Environ(), "TEST_FATAL=1")
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var stderrBuf bytes.Buffer
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if _, err := io.Copy(&stderrBuf, stderr); err != nil {
|
||||
t.Errorf("failed to copy stderr: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
if err == nil {
|
||||
t.Error("Expected log.Fatal to exit with non-zero status")
|
||||
}
|
||||
|
||||
wg.Wait() // Wait for the goroutine to finish copying
|
||||
slurp := stderrBuf.Bytes()
|
||||
|
||||
want := "{\"level\":\"fatal\",\"message\":\"test\"}\n"
|
||||
got := cbor.DecodeIfBinaryToString(slurp)
|
||||
if got != want {
|
||||
t.Errorf("Diode Fatal Test failed. got:%s, want:%s!", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type SlowWriter struct{}
|
||||
|
||||
func (rw *SlowWriter) Write(p []byte) (n int, err error) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
fmt.Print(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestFatalWithFilteredLevelWriter(t *testing.T) {
|
||||
if os.Getenv("TEST_FATAL_SLOW") == "1" {
|
||||
slowWriter := SlowWriter{}
|
||||
diodeWriter := diode.NewWriter(&slowWriter, 500, 0, func(missed int) {
|
||||
fmt.Printf("Missed %d logs\n", missed)
|
||||
})
|
||||
leveledDiodeWriter := zerolog.LevelWriterAdapter{
|
||||
Writer: &diodeWriter,
|
||||
}
|
||||
filteredDiodeWriter := zerolog.FilteredLevelWriter{
|
||||
Writer: &leveledDiodeWriter,
|
||||
Level: zerolog.InfoLevel,
|
||||
}
|
||||
logger := zerolog.New(&filteredDiodeWriter)
|
||||
logger.Fatal().Msg("test")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestFatalWithFilteredLevelWriter")
|
||||
cmd.Env = append(os.Environ(), "TEST_FATAL_SLOW=1")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = io.Copy(&stdoutBuf, stdout)
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
if err == nil {
|
||||
t.Error("Expected log.Fatal to exit with non-zero status")
|
||||
}
|
||||
|
||||
wg.Wait() // Wait for the goroutine to finish copying
|
||||
slurp := stdoutBuf.Bytes()
|
||||
|
||||
got := cbor.DecodeIfBinaryToString(slurp)
|
||||
want := "{\"level\":\"fatal\",\"message\":\"test\"}\n"
|
||||
if got != want {
|
||||
t.Errorf("Expected output %q, got: %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark(b *testing.B) {
|
||||
log.SetOutput(io.Discard)
|
||||
defer log.SetOutput(os.Stderr)
|
||||
benchs := map[string]time.Duration{
|
||||
"Waiter": 0,
|
||||
"Pooler": 10 * time.Millisecond,
|
||||
}
|
||||
for name, interval := range benchs {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
w := diode.NewWriter(io.Discard, 100000, interval, nil)
|
||||
log := zerolog.New(w)
|
||||
defer w.Close()
|
||||
|
||||
b.SetParallelism(1000)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
log.Print("test")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
// ManyToOne diode is optimal for many writers (go-routines B-n) and a single
|
||||
// reader (go-routine A). It is not thread safe for multiple readers.
|
||||
type ManyToOne struct {
|
||||
buffer []unsafe.Pointer
|
||||
writeIndex uint64
|
||||
readIndex uint64
|
||||
buffer []unsafe.Pointer
|
||||
alerter Alerter
|
||||
}
|
||||
|
||||
// NewManyToOne creates a new diode (ring buffer). The ManyToOne diode
|
||||
// is optimzed for many writers (on go-routines B-n) and a single reader
|
||||
// is optimized for many writers (on go-routines B-n) and a single reader
|
||||
// (on go-routine A). The alerter is invoked on the read's go-routine. It is
|
||||
// called when it notices that the writer go-routine has passed it and wrote
|
||||
// over data. A nil can be used to ignore alerts.
|
||||
@@ -66,7 +66,7 @@ func (d *ManyToOne) Set(data GenericDataType) {
|
||||
}
|
||||
|
||||
// TryNext will attempt to read from the next slot of the ring buffer.
|
||||
// If there is not data available, it will return (nil, false).
|
||||
// If there is no data available, it will return (nil, false).
|
||||
func (d *ManyToOne) TryNext() (data GenericDataType, ok bool) {
|
||||
// Read a value from the ring buffer based on the readIndex.
|
||||
idx := d.readIndex % uint64(len(d.buffer))
|
||||
@@ -80,7 +80,7 @@ func (d *ManyToOne) TryNext() (data GenericDataType, ok bool) {
|
||||
}
|
||||
|
||||
// When the seq value is less than the current read index that means a
|
||||
// value was read from idx that was previously written but has since has
|
||||
// value was read from idx that was previously written but since has
|
||||
// been dropped. This value must be ignored and the read head must not
|
||||
// increment.
|
||||
//
|
||||
|
||||
@@ -31,9 +31,9 @@ type bucket struct {
|
||||
// OneToOne diode is meant to be used by a single reader and a single writer.
|
||||
// It is not thread safe if used otherwise.
|
||||
type OneToOne struct {
|
||||
buffer []unsafe.Pointer
|
||||
writeIndex uint64
|
||||
readIndex uint64
|
||||
buffer []unsafe.Pointer
|
||||
alerter Alerter
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (d *OneToOne) TryNext() (data GenericDataType, ok bool) {
|
||||
}
|
||||
|
||||
// When the seq value is less than the current read index that means a
|
||||
// value was read from idx that was previously written but has since has
|
||||
// value was read from idx that was previously written but since has
|
||||
// been dropped. This value must be ignored and the read head must not
|
||||
// increment.
|
||||
//
|
||||
|
||||
@@ -24,18 +24,18 @@ type PollerConfigOption func(*Poller)
|
||||
// WithPollingInterval sets the interval at which the diode is queried
|
||||
// for new data. The default is 10ms.
|
||||
func WithPollingInterval(interval time.Duration) PollerConfigOption {
|
||||
return PollerConfigOption(func(c *Poller) {
|
||||
return func(c *Poller) {
|
||||
c.interval = interval
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithPollingContext sets the context to cancel any retrieval (Next()). It
|
||||
// will not change any results for adding data (Set()). Default is
|
||||
// context.Background().
|
||||
func WithPollingContext(ctx context.Context) PollerConfigOption {
|
||||
return PollerConfigOption(func(c *Poller) {
|
||||
return func(c *Poller) {
|
||||
c.ctx = ctx
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// NewPoller returns a new Poller that wraps the given diode.
|
||||
|
||||
@@ -21,9 +21,9 @@ type WaiterConfigOption func(*Waiter)
|
||||
// will not change any results for adding data (Set()). Default is
|
||||
// context.Background().
|
||||
func WithWaiterContext(ctx context.Context) WaiterConfigOption {
|
||||
return WaiterConfigOption(func(c *Waiter) {
|
||||
return func(c *Waiter) {
|
||||
c.ctx = ctx
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// NewWaiter returns a new Waiter that wraps the given diode.
|
||||
@@ -39,7 +39,12 @@ func NewWaiter(d Diode, opts ...WaiterConfigOption) *Waiter {
|
||||
|
||||
go func() {
|
||||
<-w.ctx.Done()
|
||||
|
||||
// Mutex is strictly necessary here to avoid a race in Next() (between
|
||||
// w.isDone() and w.c.Wait()) and w.c.Broadcast() here.
|
||||
w.mu.Lock()
|
||||
w.c.Broadcast()
|
||||
w.mu.Unlock()
|
||||
}()
|
||||
|
||||
return w
|
||||
|
||||
+6
-6
@@ -13,13 +13,13 @@ type encoder interface {
|
||||
AppendBool(dst []byte, val bool) []byte
|
||||
AppendBools(dst []byte, vals []bool) []byte
|
||||
AppendBytes(dst, s []byte) []byte
|
||||
AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte
|
||||
AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte
|
||||
AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte
|
||||
AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte
|
||||
AppendEndMarker(dst []byte) []byte
|
||||
AppendFloat32(dst []byte, val float32) []byte
|
||||
AppendFloat64(dst []byte, val float64) []byte
|
||||
AppendFloats32(dst []byte, vals []float32) []byte
|
||||
AppendFloats64(dst []byte, vals []float64) []byte
|
||||
AppendFloat32(dst []byte, val float32, precision int) []byte
|
||||
AppendFloat64(dst []byte, val float64, precision int) []byte
|
||||
AppendFloats32(dst []byte, vals []float32, precision int) []byte
|
||||
AppendFloats64(dst []byte, vals []float64, precision int) []byte
|
||||
AppendHex(dst, s []byte) []byte
|
||||
AppendIPAddr(dst []byte, ip net.IP) []byte
|
||||
AppendIPPrefix(dst []byte, pfx net.IPNet) []byte
|
||||
|
||||
@@ -14,9 +14,19 @@ var (
|
||||
enc = cbor.Encoder{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// using closure to reflect the changes at runtime.
|
||||
cbor.JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return InterfaceMarshalFunc(v)
|
||||
}
|
||||
}
|
||||
|
||||
func appendJSON(dst []byte, j []byte) []byte {
|
||||
return cbor.AppendEmbeddedJSON(dst, j)
|
||||
}
|
||||
func appendCBOR(dst []byte, c []byte) []byte {
|
||||
return cbor.AppendEmbeddedCBOR(dst, c)
|
||||
}
|
||||
|
||||
// decodeIfBinaryToString - converts a binary formatted log msg to a
|
||||
// JSON formatted String Log message.
|
||||
|
||||
@@ -6,6 +6,7 @@ package zerolog
|
||||
// JSON encoded byte stream.
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/rs/zerolog/internal/json"
|
||||
)
|
||||
|
||||
@@ -15,9 +16,27 @@ var (
|
||||
enc = json.Encoder{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// using closure to reflect the changes at runtime.
|
||||
json.JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return InterfaceMarshalFunc(v)
|
||||
}
|
||||
}
|
||||
|
||||
func appendJSON(dst []byte, j []byte) []byte {
|
||||
return append(dst, j...)
|
||||
}
|
||||
func appendCBOR(dst []byte, cbor []byte) []byte {
|
||||
dst = append(dst, []byte("\"data:application/cbor;base64,")...)
|
||||
l := len(dst)
|
||||
enc := base64.StdEncoding
|
||||
n := enc.EncodedLen(len(cbor))
|
||||
for i := 0; i < n; i++ {
|
||||
dst = append(dst, '.')
|
||||
}
|
||||
enc.Encode(dst[l:], cbor)
|
||||
return append(dst, '"')
|
||||
}
|
||||
|
||||
func decodeIfBinaryToString(in []byte) string {
|
||||
return string(in)
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type loggableError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (l loggableError) MarshalZerologObject(e *Event) {
|
||||
if l.error == nil {
|
||||
return
|
||||
}
|
||||
e.Str("l", strings.ToUpper(l.error.Error()))
|
||||
}
|
||||
|
||||
type nonLoggableError struct {
|
||||
error
|
||||
line int
|
||||
}
|
||||
|
||||
type wrappedError struct {
|
||||
error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (w wrappedError) Error() string {
|
||||
if w.error == nil {
|
||||
return w.msg
|
||||
}
|
||||
return w.error.Error() + ": " + w.msg
|
||||
}
|
||||
|
||||
type interfaceError struct {
|
||||
val string
|
||||
}
|
||||
|
||||
func TestArrayErrorMarshalFunc(t *testing.T) {
|
||||
prefixed := func(s, prefix string) string {
|
||||
if s == "null" {
|
||||
return ""
|
||||
}
|
||||
return prefix + s + `,`
|
||||
}
|
||||
errs := []error{
|
||||
nil,
|
||||
fmt.Errorf("failure"),
|
||||
loggableError{fmt.Errorf("whoops")},
|
||||
nonLoggableError{fmt.Errorf("oops"), 402},
|
||||
}
|
||||
type testCase struct {
|
||||
name string
|
||||
marshal func(err error) interface{}
|
||||
want []string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "default",
|
||||
marshal: nil,
|
||||
want: []string{`null`, `"failure"`, `{"l":"WHOOPS"}`, `"oops"`},
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
marshal: func(err error) interface{} {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return err.Error()
|
||||
},
|
||||
want: []string{`null`, `"failure"`, `"whoops"`, `"oops"`},
|
||||
},
|
||||
{
|
||||
name: "loggable",
|
||||
marshal: func(err error) interface{} {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return loggableError{err}
|
||||
},
|
||||
want: []string{`null`, `{"l":"FAILURE"}`, `{"l":"WHOOPS"}`, `{"l":"OOPS"}`},
|
||||
},
|
||||
{
|
||||
name: "non-loggable",
|
||||
marshal: func(err error) interface{} {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return nonLoggableError{err, 404}
|
||||
},
|
||||
want: []string{`null`, `"failure"`, `"whoops"`, `"oops"`},
|
||||
},
|
||||
{
|
||||
name: "interface",
|
||||
marshal: func(err error) interface{} {
|
||||
var some interfaceError
|
||||
if err != nil {
|
||||
some.val = err.Error()
|
||||
}
|
||||
var interfaceErr interface{} = some
|
||||
return interfaceErr
|
||||
},
|
||||
want: []string{`{}`, `{}`, `{}`, `{}`},
|
||||
},
|
||||
{
|
||||
name: "nilError",
|
||||
marshal: func(err error) interface{} {
|
||||
var errNil error = nil
|
||||
return errNil
|
||||
},
|
||||
want: []string{`null`, `null`, `null`, `null`},
|
||||
},
|
||||
{
|
||||
name: "wrapped error",
|
||||
marshal: func(err error) interface{} {
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if we, ok := err.(wrappedError); ok {
|
||||
return we
|
||||
} else {
|
||||
return wrappedError{err, "addendum"}
|
||||
}
|
||||
},
|
||||
want: []string{`null`, `"failure: addendum"`, `"whoops: addendum"`, `"oops: addendum"`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
originalErrorMarshalFunc := ErrorMarshalFunc
|
||||
defer func() {
|
||||
ErrorMarshalFunc = originalErrorMarshalFunc
|
||||
}()
|
||||
|
||||
if tc.marshal != nil {
|
||||
ErrorMarshalFunc = tc.marshal
|
||||
}
|
||||
|
||||
t.Run("Err", func(t *testing.T) {
|
||||
for i, err := range errs {
|
||||
want := tc.want[i]
|
||||
t.Run("Arr", func(t *testing.T) {
|
||||
wants := `[` + want + `]`
|
||||
a := Arr().Err(err)
|
||||
if got := decodeObjectToStr(a.write([]byte{})); got != wants {
|
||||
t.Errorf("%s %d Array.Err(%v)\ngot: %s\nwant: %s", tc.name, i, err, got, wants)
|
||||
}
|
||||
})
|
||||
t.Run("Ctx", func(t *testing.T) {
|
||||
wants := `{` + prefixed(want, `"error":`) + `"message":"msg"}` + "\n"
|
||||
out := &bytes.Buffer{}
|
||||
logger := New(out).With().Err(err).Logger()
|
||||
logger.Log().Msg("msg")
|
||||
if got := decodeIfBinaryToString(out.Bytes()); got != wants {
|
||||
t.Errorf("%s %d Ctx.Err(%v)\ngot: %v\nwant: %v", tc.name, i, err, got, wants)
|
||||
}
|
||||
})
|
||||
t.Run("Event", func(t *testing.T) {
|
||||
wants := `{` + prefixed(want, `"error":`) + `"message":"msg"}` + "\n"
|
||||
out := &bytes.Buffer{}
|
||||
logger := New(out)
|
||||
logger.Log().Err(err).Msg("msg")
|
||||
if got := decodeIfBinaryToString(out.Bytes()); got != wants {
|
||||
t.Errorf("%s %d Event.Err(%v)\ngot: %v\nwant: %v", tc.name, i, err, got, wants)
|
||||
}
|
||||
})
|
||||
t.Run("Fields", func(t *testing.T) {
|
||||
if i == 0 && tc.want[i] == "{}" {
|
||||
want = `null`
|
||||
}
|
||||
wants := `{"err":` + want + `,"message":"msg"}` + "\n"
|
||||
out := &bytes.Buffer{}
|
||||
logger := New(out)
|
||||
logger.Log().Fields(map[string]interface{}{"err": err}).Msg("msg")
|
||||
if got := decodeIfBinaryToString(out.Bytes()); got != wants {
|
||||
t.Errorf("%s %d Event.Fields(%v)\ngot: %v\nwant: %v", tc.name, i, err, got, wants)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Errs", func(t *testing.T) {
|
||||
want := `[` + strings.Join(tc.want, ",") + `]`
|
||||
t.Run("Arr", func(t *testing.T) {
|
||||
a := Arr().Errs(errs)
|
||||
if got := decodeObjectToStr(a.write([]byte{})); got != want {
|
||||
t.Errorf("%s Array.Errs()\ngot: %s\nwant: %s", tc.name, got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Ctx", func(t *testing.T) {
|
||||
wants := `{"e":` + want + `,"message":"msg"}` + "\n"
|
||||
out := &bytes.Buffer{}
|
||||
logger := New(out).With().Errs("e", errs).Logger()
|
||||
logger.Log().Msg("msg")
|
||||
if got := decodeIfBinaryToString(out.Bytes()); got != wants {
|
||||
t.Errorf("%s Ctx.Errs()\ngot: %v\nwant: %v", tc.name, got, wants)
|
||||
}
|
||||
})
|
||||
t.Run("Event", func(t *testing.T) {
|
||||
wants := `{"e":` + want + `,"message":"msg"}` + "\n"
|
||||
out := &bytes.Buffer{}
|
||||
logger := New(out)
|
||||
logger.Log().Errs("e", errs).Msg("msg")
|
||||
if got := decodeIfBinaryToString(out.Bytes()); got != wants {
|
||||
t.Errorf("%s Ctx.Errs()\ngot: %v\nwant: %v", tc.name, got, wants)
|
||||
}
|
||||
})
|
||||
t.Run("Fields", func(t *testing.T) {
|
||||
wants := `{"e":` + want + `,"message":"msg"}` + "\n"
|
||||
out := &bytes.Buffer{}
|
||||
logger := New(out)
|
||||
logger.Log().Fields(map[string]interface{}{"e": errs}).Msg("msg")
|
||||
if got := decodeIfBinaryToString(out.Bytes()); got != wants {
|
||||
t.Errorf("%s Ctx.Errs()\ngot: %v\nwant: %v", tc.name, got, wants)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -18,22 +18,29 @@ var eventPool = &sync.Pool{
|
||||
},
|
||||
}
|
||||
|
||||
// ErrorMarshalFunc allows customization of global error marshaling
|
||||
var ErrorMarshalFunc = func(err error) interface{} {
|
||||
return err
|
||||
}
|
||||
|
||||
// Event represents a log event. It is instanced by one of the level method of
|
||||
// Logger and finalized by the Msg or Msgf method.
|
||||
type Event struct {
|
||||
buf []byte
|
||||
w LevelWriter
|
||||
level Level
|
||||
done func(msg string)
|
||||
ch []Hook // hooks from context
|
||||
buf []byte
|
||||
w LevelWriter
|
||||
level Level
|
||||
done func(msg string)
|
||||
stack bool // enable error stack trace
|
||||
ch []Hook // hooks from context
|
||||
skipFrame int // The number of additional frames to skip when printing the caller.
|
||||
ctx context.Context // Optional Go context for event
|
||||
}
|
||||
|
||||
func putEvent(e *Event) {
|
||||
// prevent any subsequent use of the Event contextual state and truncate the buffer
|
||||
e.w = nil
|
||||
e.done = nil
|
||||
e.stack = false
|
||||
e.ch = nil
|
||||
e.skipFrame = 0
|
||||
e.ctx = nil
|
||||
e.buf = e.buf[:0]
|
||||
|
||||
// Proper usage of a sync.Pool requires each entry to have approximately
|
||||
// the same memory cost. To obtain this property when the stored type
|
||||
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
|
||||
@@ -41,10 +48,9 @@ func putEvent(e *Event) {
|
||||
//
|
||||
// See https://golang.org/issue/23199
|
||||
const maxSize = 1 << 16 // 64KiB
|
||||
if cap(e.buf) > maxSize {
|
||||
return
|
||||
if cap(e.buf) <= maxSize {
|
||||
eventPool.Put(e)
|
||||
}
|
||||
eventPool.Put(e)
|
||||
}
|
||||
|
||||
// LogObjectMarshaler provides a strongly-typed and encoding-agnostic interface
|
||||
@@ -59,13 +65,16 @@ type LogArrayMarshaler interface {
|
||||
MarshalZerologArray(a *Array)
|
||||
}
|
||||
|
||||
func newEvent(w LevelWriter, level Level) *Event {
|
||||
func newEvent(w LevelWriter, level Level, stack bool, ctx context.Context, hooks []Hook) *Event {
|
||||
e := eventPool.Get().(*Event)
|
||||
e.buf = e.buf[:0]
|
||||
e.ch = nil
|
||||
e.stack = stack
|
||||
e.ctx = ctx
|
||||
e.ch = hooks
|
||||
e.buf = enc.AppendBeginMarker(e.buf)
|
||||
e.w = w
|
||||
e.level = level
|
||||
e.skipFrame = 0
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -92,6 +101,9 @@ func (e *Event) Enabled() bool {
|
||||
|
||||
// Discard disables the event so Msg(f) won't print it.
|
||||
func (e *Event) Discard() *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.level = Disabled
|
||||
return nil
|
||||
}
|
||||
@@ -107,10 +119,20 @@ func (e *Event) Msg(msg string) {
|
||||
e.msg(msg)
|
||||
}
|
||||
|
||||
// Msgf sends the event with formated msg added as the message field if not empty.
|
||||
// Send is equivalent to calling Msg("").
|
||||
//
|
||||
// NOTICE: once this methid is called, the *Event should be disposed.
|
||||
// Calling Msg twice can have unexpected result.
|
||||
// NOTICE: once this method is called, the *Event should be disposed.
|
||||
func (e *Event) Send() {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.msg("")
|
||||
}
|
||||
|
||||
// Msgf sends the event with formatted msg added as the message field if not empty.
|
||||
//
|
||||
// NOTICE: once this method is called, the *Event should be disposed.
|
||||
// Calling Msgf twice can have unexpected result.
|
||||
func (e *Event) Msgf(format string, v ...interface{}) {
|
||||
if e == nil {
|
||||
return
|
||||
@@ -118,14 +140,16 @@ func (e *Event) Msgf(format string, v ...interface{}) {
|
||||
e.msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (e *Event) MsgFunc(createMsg func() string) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.msg(createMsg())
|
||||
}
|
||||
|
||||
func (e *Event) msg(msg string) {
|
||||
if len(e.ch) > 0 {
|
||||
e.ch[0].Run(e, e.level, msg)
|
||||
if len(e.ch) > 1 {
|
||||
for _, hook := range e.ch[1:] {
|
||||
hook.Run(e, e.level, msg)
|
||||
}
|
||||
}
|
||||
for _, hook := range e.ch {
|
||||
hook.Run(e, e.level, msg)
|
||||
}
|
||||
if msg != "" {
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, MessageFieldName), msg)
|
||||
@@ -134,40 +158,73 @@ func (e *Event) msg(msg string) {
|
||||
defer e.done(msg)
|
||||
}
|
||||
if err := e.write(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "zerolog: could not write event: %v", err)
|
||||
if ErrorHandler != nil {
|
||||
ErrorHandler(err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "zerolog: could not write event: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fields is a helper function to use a map to set fields using type assertion.
|
||||
func (e *Event) Fields(fields map[string]interface{}) *Event {
|
||||
// Fields is a helper function to use a map or slice to set fields using type assertion.
|
||||
// Only map[string]interface{} and []interface{} are accepted. []interface{} must
|
||||
// alternate string keys and arbitrary values, and extraneous ones are ignored.
|
||||
func (e *Event) Fields(fields interface{}) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = appendFields(e.buf, fields)
|
||||
e.buf = appendFields(e.buf, fields, e.stack, e.ctx, e.ch)
|
||||
return e
|
||||
}
|
||||
|
||||
// Dict adds the field key with a dict to the event context.
|
||||
// Use zerolog.Dict() to create the dictionary.
|
||||
// Use e.CreateDict() to create the dictionary.
|
||||
func (e *Event) Dict(key string, dict *Event) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
if e != nil {
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
e.buf = append(enc.AppendKey(e.buf, key), dict.buf...)
|
||||
}
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
e.buf = append(enc.AppendKey(e.buf, key), dict.buf...)
|
||||
putEvent(e)
|
||||
putEvent(dict)
|
||||
return e
|
||||
}
|
||||
|
||||
// CreateDict creates an Event to be used with the *Event.Dict method.
|
||||
// It preserves the stack, hooks, and context from the parent event.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the *Event.Dict method.
|
||||
func (e *Event) CreateDict() *Event {
|
||||
if e == nil {
|
||||
return newEvent(nil, DebugLevel, false, nil, nil)
|
||||
}
|
||||
return newEvent(nil, DebugLevel, e.stack, e.ctx, e.ch)
|
||||
}
|
||||
|
||||
// Dict creates an Event to be used with the *Event.Dict method.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the *Event.Dict method.
|
||||
// NOTE: This function is deprecated because it does not preserve
|
||||
// the stack, hooks, and context from the parent event.
|
||||
// Deprecated: Use Event.CreateDict instead.
|
||||
func Dict() *Event {
|
||||
return newEvent(nil, 0)
|
||||
return newEvent(nil, DebugLevel, false, nil, nil)
|
||||
}
|
||||
|
||||
// CreateArray creates an Array to be used with the *Event.Array method.
|
||||
// It preserves the stack, hooks, and context from the parent event.
|
||||
// Call usual field methods like Str, Int etc to add elements to this
|
||||
// array and give it as argument the *Event.Array method.
|
||||
func (e *Event) CreateArray() *Array {
|
||||
a := Arr()
|
||||
if e != nil {
|
||||
a.stack = e.stack
|
||||
a.ctx = e.ctx
|
||||
a.ch = e.ch
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Array adds the field key with an array to the event context.
|
||||
// Use zerolog.Arr() to create the array or pass a type that
|
||||
// Use e.CreateArray() to create the array or pass a type that
|
||||
// implement the LogArrayMarshaler interface.
|
||||
func (e *Event) Array(key string, arr LogArrayMarshaler) *Event {
|
||||
if e == nil {
|
||||
@@ -178,7 +235,7 @@ func (e *Event) Array(key string, arr LogArrayMarshaler) *Event {
|
||||
if aa, ok := arr.(*Array); ok {
|
||||
a = aa
|
||||
} else {
|
||||
a = Arr()
|
||||
a = e.CreateArray()
|
||||
arr.MarshalZerologArray(a)
|
||||
}
|
||||
e.buf = a.write(e.buf)
|
||||
@@ -197,15 +254,59 @@ func (e *Event) Object(key string, obj LogObjectMarshaler) *Event {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendKey(e.buf, key)
|
||||
if obj == nil {
|
||||
e.buf = enc.AppendNil(e.buf)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
e.appendObject(obj)
|
||||
return e
|
||||
}
|
||||
|
||||
// Object marshals an object that implement the LogObjectMarshaler interface.
|
||||
// Objects adds the field key with objs as an array of objects that
|
||||
// implement the LogObjectMarshaler interface to the event.
|
||||
//
|
||||
// This is the array version that accepts a slice of LogObjectMarshaler objects.
|
||||
func (e *Event) Objects(key string, objs []LogObjectMarshaler) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendArrayStart(enc.AppendKey(e.buf, key))
|
||||
for i, obj := range objs {
|
||||
e.buf = appendObject(e.buf, obj, e.stack, e.ctx, e.ch)
|
||||
if i < (len(objs) - 1) {
|
||||
e.buf = enc.AppendArrayDelim(e.buf)
|
||||
}
|
||||
}
|
||||
e.buf = enc.AppendArrayEnd(e.buf)
|
||||
return e
|
||||
}
|
||||
|
||||
// ObjectsV adds the field key with objs as an array of objects that
|
||||
// implement the LogObjectMarshaler interface to the event.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual LogObjectMarshaler objects.
|
||||
func (e *Event) ObjectsV(key string, objs ...LogObjectMarshaler) *Event {
|
||||
return e.Objects(key, objs)
|
||||
}
|
||||
|
||||
// Func allows an anonymous func to run only if the event is enabled.
|
||||
func (e *Event) Func(f func(e *Event)) *Event {
|
||||
if e != nil && e.Enabled() {
|
||||
f(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// EmbedObject marshals an object that implement the LogObjectMarshaler interface.
|
||||
func (e *Event) EmbedObject(obj LogObjectMarshaler) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
if obj == nil {
|
||||
return e
|
||||
}
|
||||
obj.MarshalZerologObject(e)
|
||||
return e
|
||||
}
|
||||
@@ -220,6 +321,8 @@ func (e *Event) Str(key, val string) *Event {
|
||||
}
|
||||
|
||||
// Strs adds the field key with vals as a []string to the *Event context.
|
||||
//
|
||||
// This is the array version that accepts a slice of string values.
|
||||
func (e *Event) Strs(key string, vals []string) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -228,6 +331,47 @@ func (e *Event) Strs(key string, vals []string) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// StrsV adds the field key with vals as a []string to the *Event context.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual strings.
|
||||
func (e *Event) StrsV(key string, vals ...string) *Event {
|
||||
return e.Strs(key, vals)
|
||||
}
|
||||
|
||||
// Stringer adds the field key and a val to the *Event context.
|
||||
// If val is not nil, it is added by calling val.String().
|
||||
// If val is nil, it is encoded as null without calling String().
|
||||
func (e *Event) Stringer(key string, val fmt.Stringer) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendStringer(enc.AppendKey(e.buf, key), val)
|
||||
return e
|
||||
}
|
||||
|
||||
// Stringers adds the field key with vals to the *Event context.
|
||||
// If a val is not nil, it is added by calling val.String().
|
||||
// If a val is nil, it is encoded as null without calling String().
|
||||
//
|
||||
// This is the array version that accepts a slice of fmt.Stringer values.
|
||||
func (e *Event) Stringers(key string, vals []fmt.Stringer) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendStringers(enc.AppendKey(e.buf, key), vals)
|
||||
return e
|
||||
}
|
||||
|
||||
// StringersV adds the field key with vals to the *Event context.
|
||||
// If a val is not nil, it is added by calling val.String().
|
||||
// If a val is nil, it is encoded as null without calling String().
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual
|
||||
// fmt.Stringer values.
|
||||
func (e *Event) StringersV(key string, vals ...fmt.Stringer) *Event {
|
||||
return e.Stringers(key, vals)
|
||||
}
|
||||
|
||||
// Bytes adds the field key with val as a string to the *Event context.
|
||||
//
|
||||
// Runes outside of normal ASCII ranges will be hex-encoded in the resulting
|
||||
@@ -261,16 +405,33 @@ func (e *Event) RawJSON(key string, b []byte) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// RawCBOR adds already encoded CBOR to the log line under key.
|
||||
//
|
||||
// No sanity check is performed on b
|
||||
// Note: The full featureset of CBOR is supported as data will not be mapped to json but stored as data-url
|
||||
func (e *Event) RawCBOR(key string, b []byte) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = appendCBOR(enc.AppendKey(e.buf, key), b)
|
||||
return e
|
||||
}
|
||||
|
||||
// AnErr adds the field key with serialized err to the *Event context.
|
||||
// If err is nil, no field is added.
|
||||
func (e *Event) AnErr(key string, err error) *Event {
|
||||
marshaled := ErrorMarshalFunc(err)
|
||||
switch m := marshaled.(type) {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
return e
|
||||
case LogObjectMarshaler:
|
||||
return e.Object(key, m)
|
||||
case error:
|
||||
if isNilValue(m) {
|
||||
return e
|
||||
}
|
||||
return e.Str(key, m.Error())
|
||||
case string:
|
||||
return e.Str(key, m)
|
||||
@@ -285,32 +446,73 @@ func (e *Event) Errs(key string, errs []error) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
|
||||
arr := Arr()
|
||||
for _, err := range errs {
|
||||
marshaled := ErrorMarshalFunc(err)
|
||||
switch m := marshaled.(type) {
|
||||
case LogObjectMarshaler:
|
||||
arr = arr.Object(m)
|
||||
case error:
|
||||
arr = arr.Err(m)
|
||||
case string:
|
||||
arr = arr.Str(m)
|
||||
default:
|
||||
arr = arr.Interface(m)
|
||||
}
|
||||
}
|
||||
|
||||
arr := e.CreateArray().Errs(errs)
|
||||
return e.Array(key, arr)
|
||||
}
|
||||
|
||||
// Err adds the field "error" with serialized err to the *Event context.
|
||||
// If err is nil, no field is added.
|
||||
//
|
||||
// To customize the key name, change zerolog.ErrorFieldName.
|
||||
//
|
||||
// If Stack() has been called before and zerolog.ErrorStackMarshaler is defined,
|
||||
// the err is passed to ErrorStackMarshaler and the result is appended to the
|
||||
// zerolog.ErrorStackFieldName.
|
||||
func (e *Event) Err(err error) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if e.stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(err).(type) {
|
||||
case nil:
|
||||
return e
|
||||
case LogObjectMarshaler:
|
||||
e = e.Object(ErrorStackFieldName, m)
|
||||
case error:
|
||||
e = e.Str(ErrorStackFieldName, m.Error())
|
||||
case string:
|
||||
e = e.Str(ErrorStackFieldName, m)
|
||||
default:
|
||||
e = e.Interface(ErrorStackFieldName, m)
|
||||
}
|
||||
}
|
||||
|
||||
return e.AnErr(ErrorFieldName, err)
|
||||
}
|
||||
|
||||
// Stack enables stack trace printing for the error passed to Err().
|
||||
//
|
||||
// ErrorStackMarshaler must be set for this method to do something.
|
||||
func (e *Event) Stack() *Event {
|
||||
if e != nil {
|
||||
e.stack = true
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Ctx adds the Go Context to the *Event context. The context is not rendered
|
||||
// in the output message, but is available to hooks and to Func() calls via the
|
||||
// GetCtx() accessor. A typical use case is to extract tracing information from
|
||||
// the Go Ctx.
|
||||
func (e *Event) Ctx(ctx context.Context) *Event {
|
||||
if e != nil {
|
||||
e.ctx = ctx
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// GetCtx retrieves the Go context.Context which is optionally stored in the
|
||||
// Event. This allows Hooks and functions passed to Func() to retrieve values
|
||||
// which are stored in the context.Context. This can be useful in tracing,
|
||||
// where span information is commonly propagated in the context.Context.
|
||||
func (e *Event) GetCtx() context.Context {
|
||||
if e == nil || e.ctx == nil {
|
||||
return context.Background()
|
||||
}
|
||||
return e.ctx
|
||||
}
|
||||
|
||||
// Bool adds the field key with val as a bool to the *Event context.
|
||||
func (e *Event) Bool(key string, b bool) *Event {
|
||||
if e == nil {
|
||||
@@ -514,7 +716,7 @@ func (e *Event) Float32(key string, f float32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloat32(enc.AppendKey(e.buf, key), f)
|
||||
e.buf = enc.AppendFloat32(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -523,7 +725,7 @@ func (e *Event) Floats32(key string, f []float32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloats32(enc.AppendKey(e.buf, key), f)
|
||||
e.buf = enc.AppendFloats32(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -532,7 +734,7 @@ func (e *Event) Float64(key string, f float64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloat64(enc.AppendKey(e.buf, key), f)
|
||||
e.buf = enc.AppendFloat64(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -541,7 +743,7 @@ func (e *Event) Floats64(key string, f []float64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloats64(enc.AppendKey(e.buf, key), f)
|
||||
e.buf = enc.AppendFloats64(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -558,7 +760,7 @@ func (e *Event) Timestamp() *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// Time adds the field key with t formated as string using zerolog.TimeFieldFormat.
|
||||
// Time adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (e *Event) Time(key string, t time.Time) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -567,7 +769,7 @@ func (e *Event) Time(key string, t time.Time) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// Times adds the field key with t formated as string using zerolog.TimeFieldFormat.
|
||||
// Times adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (e *Event) Times(key string, t []time.Time) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -583,7 +785,7 @@ func (e *Event) Dur(key string, d time.Duration) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger)
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -594,7 +796,7 @@ func (e *Event) Durs(key string, d []time.Duration) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger)
|
||||
e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -609,10 +811,15 @@ func (e *Event) TimeDiff(key string, t time.Time, start time.Time) *Event {
|
||||
if t.After(start) {
|
||||
d = t.Sub(start)
|
||||
}
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldInteger)
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// Any is a wrapper around Event.Interface.
|
||||
func (e *Event) Any(key string, i interface{}) *Event {
|
||||
return e.Interface(key, i)
|
||||
}
|
||||
|
||||
// Interface adds the field key with i marshaled using reflection.
|
||||
func (e *Event) Interface(key string, i interface{}) *Event {
|
||||
if e == nil {
|
||||
@@ -625,24 +832,47 @@ func (e *Event) Interface(key string, i interface{}) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// Type adds the field key with val's type using reflection.
|
||||
func (e *Event) Type(key string, val interface{}) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendType(enc.AppendKey(e.buf, key), val)
|
||||
return e
|
||||
}
|
||||
|
||||
// CallerSkipFrame instructs any future Caller calls to skip the specified number of frames.
|
||||
// This includes those added via hooks from the context.
|
||||
func (e *Event) CallerSkipFrame(skip int) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.skipFrame += skip
|
||||
return e
|
||||
}
|
||||
|
||||
// Caller adds the file:line of the caller with the zerolog.CallerFieldName key.
|
||||
func (e *Event) Caller() *Event {
|
||||
return e.caller(CallerSkipFrameCount)
|
||||
// The argument skip is the number of stack frames to ascend
|
||||
// Skip If not passed, use the global variable CallerSkipFrameCount
|
||||
func (e *Event) Caller(skip ...int) *Event {
|
||||
sk := CallerSkipFrameCount
|
||||
if len(skip) > 0 {
|
||||
sk = skip[0] + CallerSkipFrameCount
|
||||
}
|
||||
return e.caller(sk)
|
||||
}
|
||||
|
||||
func (e *Event) caller(skip int) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
_, file, line, ok := runtime.Caller(skip)
|
||||
if !ok {
|
||||
return e
|
||||
if pc, file, line, ok := runtime.Caller(skip + e.skipFrame); ok {
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(pc, file, line))
|
||||
}
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), file+":"+strconv.Itoa(line))
|
||||
return e
|
||||
}
|
||||
|
||||
// IPAddr adds IPv4 or IPv6 Address to the event
|
||||
// IPAddr adds the field key with ip as a net.IP IPv4 or IPv6 Address to the event
|
||||
func (e *Event) IPAddr(key string, ip net.IP) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -651,7 +881,16 @@ func (e *Event) IPAddr(key string, ip net.IP) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// IPPrefix adds IPv4 or IPv6 Prefix (address and mask) to the event
|
||||
// IPAddrs adds the field key with ip as a net.IP array of IPv4 or IPv6 Address to the event
|
||||
func (e *Event) IPAddrs(key string, ip []net.IP) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPAddrs(enc.AppendKey(e.buf, key), ip)
|
||||
return e
|
||||
}
|
||||
|
||||
// IPPrefix adds the field key with pfx as a net.IPNet IPv4 or IPv6 Prefix (address and mask) to the event
|
||||
func (e *Event) IPPrefix(key string, pfx net.IPNet) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
@@ -660,7 +899,16 @@ func (e *Event) IPPrefix(key string, pfx net.IPNet) *Event {
|
||||
return e
|
||||
}
|
||||
|
||||
// MACAddr adds MAC address to the event
|
||||
// IPPrefixes the field key with pfx as a net.IPNet array of IPv4 or IPv6 Prefixes (address and mask) to the event
|
||||
func (e *Event) IPPrefixes(key string, pfx []net.IPNet) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPPrefixes(enc.AppendKey(e.buf, key), pfx)
|
||||
return e
|
||||
}
|
||||
|
||||
// MACAddr the field key with ha as a net.HardwareAddr MAC address to the event
|
||||
func (e *Event) MACAddr(key string, ha net.HardwareAddr) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
|
||||
+727
@@ -0,0 +1,727 @@
|
||||
//go:build !binary_log
|
||||
// +build !binary_log
|
||||
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type nilError struct{}
|
||||
|
||||
func (nilError) Error() string {
|
||||
return "nope"
|
||||
}
|
||||
|
||||
func TestEvent_AnErr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, `{}`},
|
||||
{"error", errors.New("test"), `{"err":"test"}`},
|
||||
{"nil interface", func() *nilError { return nil }(), `{}`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil)
|
||||
e = e.AnErr("err", tt.err)
|
||||
err := e.write()
|
||||
if err != nil {
|
||||
t.Errorf("Event.AnErr() error: %v", err)
|
||||
}
|
||||
|
||||
if got, want := strings.TrimSpace(buf.String()), tt.want; got != want {
|
||||
t.Errorf("Event.AnErr() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_writeWithNil(t *testing.T) {
|
||||
var e *Event = nil
|
||||
got := e.write()
|
||||
|
||||
var want *Event = nil
|
||||
if got != nil {
|
||||
t.Errorf("Event.write() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type loggableObject struct {
|
||||
member string
|
||||
}
|
||||
|
||||
func (o loggableObject) MarshalZerologObject(e *Event) {
|
||||
e.Str("member", o.member)
|
||||
}
|
||||
|
||||
func TestEvent_Object(t *testing.T) {
|
||||
t.Run("ObjectWithNil", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil)
|
||||
e = e.Object("obj", nil)
|
||||
err := e.write()
|
||||
if err != nil {
|
||||
t.Errorf("Event.Object() error: %v", err)
|
||||
}
|
||||
|
||||
want := `{"obj":null}`
|
||||
got := strings.TrimSpace(buf.String())
|
||||
if got != want {
|
||||
t.Errorf("Event.Object()\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmbedObjectWithNil", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil)
|
||||
e = e.EmbedObject(nil)
|
||||
err := e.write()
|
||||
if err != nil {
|
||||
t.Errorf("Event.EmbedObject() error: %v", err)
|
||||
}
|
||||
|
||||
want := "{}"
|
||||
got := strings.TrimSpace(buf.String())
|
||||
if got != want {
|
||||
t.Errorf("Event.EmbedObject()\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
type contextKeyType struct{}
|
||||
var contextKey = contextKeyType{}
|
||||
|
||||
called := false
|
||||
ctxHook := HookFunc(func(e *Event, level Level, message string) {
|
||||
called = true
|
||||
ctx := e.GetCtx()
|
||||
if ctx == nil {
|
||||
t.Errorf("expected context to be set in Event")
|
||||
}
|
||||
val := ctx.Value(contextKey)
|
||||
if val == nil {
|
||||
t.Errorf("expected context value, got %v", val)
|
||||
}
|
||||
e.Str("ctxValue", val.(string))
|
||||
e.Bool("stackValue", e.stack)
|
||||
})
|
||||
|
||||
t.Run("ObjectWithFullContext", func(t *testing.T) {
|
||||
called = false
|
||||
ctx := context.WithValue(context.Background(), contextKey, "ctx-object")
|
||||
|
||||
var buf bytes.Buffer
|
||||
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, true, ctx, []Hook{ctxHook})
|
||||
e = e.Object("obj", loggableObject{member: "object-value"})
|
||||
e.Msg("hello")
|
||||
|
||||
if !called {
|
||||
t.Errorf("hook was not called")
|
||||
}
|
||||
|
||||
want := `{"obj":{"member":"object-value"},"ctxValue":"ctx-object","stackValue":true,"message":"hello"}`
|
||||
got := strings.TrimSpace(buf.String())
|
||||
if got != want {
|
||||
t.Errorf("Event.EmbedObject()\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmbedObjectWithFullContext", func(t *testing.T) {
|
||||
called = false
|
||||
ctx := context.WithValue(context.Background(), contextKey, "ctx-embed")
|
||||
|
||||
var buf bytes.Buffer
|
||||
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, ctx, []Hook{ctxHook})
|
||||
e = e.EmbedObject(loggableObject{member: "embedded-value"})
|
||||
e.Msg("hello")
|
||||
|
||||
if !called {
|
||||
t.Errorf("hook was not called")
|
||||
}
|
||||
|
||||
want := `{"member":"embedded-value","ctxValue":"ctx-embed","stackValue":false,"message":"hello"}`
|
||||
got := strings.TrimSpace(buf.String())
|
||||
if got != want {
|
||||
t.Errorf("Event.EmbedObject()\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEvent_WithNilEvent(t *testing.T) {
|
||||
// coverage for nil Event receiver for all types
|
||||
var e *Event = nil
|
||||
|
||||
fixtures := makeFieldFixtures()
|
||||
types := map[string]func() *Event{
|
||||
"Array": func() *Event {
|
||||
arr := e.CreateArray()
|
||||
return e.Array("k", arr)
|
||||
},
|
||||
"Bool": func() *Event {
|
||||
return e.Bool("k", fixtures.Bools[0])
|
||||
},
|
||||
"Bools": func() *Event {
|
||||
return e.Bools("k", fixtures.Bools)
|
||||
},
|
||||
"Fields": func() *Event {
|
||||
return e.Fields(fixtures)
|
||||
},
|
||||
"Int": func() *Event {
|
||||
return e.Int("k", fixtures.Ints[0])
|
||||
},
|
||||
"Ints": func() *Event {
|
||||
return e.Ints("k", fixtures.Ints)
|
||||
},
|
||||
"Int8": func() *Event {
|
||||
return e.Int8("k", fixtures.Ints8[0])
|
||||
},
|
||||
"Ints8": func() *Event {
|
||||
return e.Ints8("k", fixtures.Ints8)
|
||||
},
|
||||
"Int16": func() *Event {
|
||||
return e.Int16("k", fixtures.Ints16[0])
|
||||
},
|
||||
"Ints16": func() *Event {
|
||||
return e.Ints16("k", fixtures.Ints16)
|
||||
},
|
||||
"Int32": func() *Event {
|
||||
return e.Int32("k", fixtures.Ints32[0])
|
||||
},
|
||||
"Ints32": func() *Event {
|
||||
return e.Ints32("k", fixtures.Ints32)
|
||||
},
|
||||
"Int64": func() *Event {
|
||||
return e.Int64("k", fixtures.Ints64[0])
|
||||
},
|
||||
"Ints64": func() *Event {
|
||||
return e.Ints64("k", fixtures.Ints64)
|
||||
},
|
||||
"Uint": func() *Event {
|
||||
return e.Uint("k", fixtures.Uints[0])
|
||||
},
|
||||
"Uints": func() *Event {
|
||||
return e.Uints("k", fixtures.Uints)
|
||||
},
|
||||
"Uint8": func() *Event {
|
||||
return e.Uint8("k", fixtures.Uints8[0])
|
||||
},
|
||||
"Uints8": func() *Event {
|
||||
return e.Uints8("k", fixtures.Uints8)
|
||||
},
|
||||
"Uint16": func() *Event {
|
||||
return e.Uint16("k", fixtures.Uints16[0])
|
||||
},
|
||||
"Uints16": func() *Event {
|
||||
return e.Uints16("k", fixtures.Uints16)
|
||||
},
|
||||
"Uint32": func() *Event {
|
||||
return e.Uint32("k", fixtures.Uints32[0])
|
||||
},
|
||||
"Uints32": func() *Event {
|
||||
return e.Uints32("k", fixtures.Uints32)
|
||||
},
|
||||
"Uint64": func() *Event {
|
||||
return e.Uint64("k", fixtures.Uints64[0])
|
||||
},
|
||||
"Uints64": func() *Event {
|
||||
return e.Uints64("k", fixtures.Uints64)
|
||||
},
|
||||
"Float64": func() *Event {
|
||||
return e.Float64("k", fixtures.Floats64[0])
|
||||
},
|
||||
"Floats64": func() *Event {
|
||||
return e.Floats64("k", fixtures.Floats64)
|
||||
},
|
||||
"Float32": func() *Event {
|
||||
return e.Float32("k", fixtures.Floats32[0])
|
||||
},
|
||||
"Floats32": func() *Event {
|
||||
return e.Floats32("k", fixtures.Floats32)
|
||||
},
|
||||
"RawCBOR": func() *Event {
|
||||
return e.RawCBOR("k", fixtures.RawCBOR)
|
||||
},
|
||||
"RawJSON": func() *Event {
|
||||
return e.RawJSON("k", fixtures.RawJSONs[0])
|
||||
},
|
||||
"Str": func() *Event {
|
||||
return e.Str("k", fixtures.Strings[0])
|
||||
},
|
||||
"Strs": func() *Event {
|
||||
return e.Strs("k", fixtures.Strings)
|
||||
},
|
||||
"StrsV": func() *Event {
|
||||
return e.StrsV("k", fixtures.Strings...)
|
||||
},
|
||||
"Stringers": func() *Event {
|
||||
return e.Stringers("k", fixtures.Stringers)
|
||||
},
|
||||
"StringersV": func() *Event {
|
||||
return e.StringersV("k", fixtures.Stringers...)
|
||||
},
|
||||
"Err": func() *Event {
|
||||
return e.Err(fixtures.Errs[0])
|
||||
},
|
||||
"Errs": func() *Event {
|
||||
return e.Errs("k", fixtures.Errs)
|
||||
},
|
||||
"Ctx": func() *Event {
|
||||
return e.Ctx(fixtures.Ctx)
|
||||
},
|
||||
"Time": func() *Event {
|
||||
return e.Time("k", fixtures.Times[0])
|
||||
},
|
||||
"Times": func() *Event {
|
||||
return e.Times("k", fixtures.Times)
|
||||
},
|
||||
"Dict": func() *Event {
|
||||
d := e.CreateDict()
|
||||
d.Str("greeting", "hello")
|
||||
return e.Dict("k", d)
|
||||
},
|
||||
"Dur": func() *Event {
|
||||
return e.Dur("k", fixtures.Durations[0])
|
||||
},
|
||||
"Durs": func() *Event {
|
||||
return e.Durs("k", fixtures.Durations)
|
||||
},
|
||||
"Interface": func() *Event {
|
||||
return e.Interface("k", fixtures.Interfaces[0])
|
||||
},
|
||||
"Interfaces": func() *Event {
|
||||
return e.Interface("k", fixtures.Interfaces)
|
||||
},
|
||||
"Interface(Object)": func() *Event {
|
||||
return e.Interface("k", fixtures.Objects[0])
|
||||
},
|
||||
"Interface(Objects)": func() *Event {
|
||||
return e.Interface("k", fixtures.Objects)
|
||||
},
|
||||
"Object": func() *Event {
|
||||
return e.Object("k", fixtures.Objects[0])
|
||||
},
|
||||
"Objects": func() *Event {
|
||||
return e.Objects("k", fixtures.Objects)
|
||||
},
|
||||
"ObjectsV": func() *Event {
|
||||
return e.ObjectsV("k", fixtures.Objects...)
|
||||
},
|
||||
"EmbedObject": func() *Event {
|
||||
return e.EmbedObject(fixtures.Objects[0])
|
||||
},
|
||||
"Timestamp": func() *Event {
|
||||
return e.Timestamp()
|
||||
},
|
||||
"IPAddr": func() *Event {
|
||||
return e.IPAddr("k", fixtures.IPAddrs[0])
|
||||
},
|
||||
"IPAddrs": func() *Event {
|
||||
return e.IPAddrs("k", fixtures.IPAddrs)
|
||||
},
|
||||
"IPPrefix": func() *Event {
|
||||
return e.IPPrefix("k", fixtures.IPPfxs[0])
|
||||
},
|
||||
"IPPrefixes": func() *Event {
|
||||
return e.IPPrefixes("k", fixtures.IPPfxs)
|
||||
},
|
||||
"MACAddr": func() *Event {
|
||||
return e.MACAddr("k", fixtures.MACAddr)
|
||||
},
|
||||
"Type": func() *Event {
|
||||
return e.Type("k", fixtures.Type)
|
||||
},
|
||||
"Caller": func() *Event {
|
||||
return e.Caller(1)
|
||||
},
|
||||
"CallerSkip": func() *Event {
|
||||
return e.CallerSkipFrame(2)
|
||||
},
|
||||
"Stack": func() *Event {
|
||||
return e.Stack()
|
||||
},
|
||||
}
|
||||
|
||||
for name := range types {
|
||||
f := types[name]
|
||||
if got := f(); got != nil {
|
||||
t.Errorf("Event.Bool() = %v, want %v", got, nil)
|
||||
}
|
||||
}
|
||||
|
||||
e.Send()
|
||||
e.Msg("nothing")
|
||||
e.Msgf("what %s", "nothing")
|
||||
|
||||
got := e.write()
|
||||
if got != nil {
|
||||
t.Errorf("Event.write() = %v, want %v", got, e)
|
||||
}
|
||||
|
||||
called := false
|
||||
e.MsgFunc(func() string {
|
||||
called = true
|
||||
return "called"
|
||||
})
|
||||
if called {
|
||||
t.Errorf("Event.MsgFunc() should not be called on nil Event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_MsgFunc(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil)
|
||||
|
||||
called := false
|
||||
e.MsgFunc(func() string {
|
||||
called = true
|
||||
return "called"
|
||||
})
|
||||
if !called {
|
||||
t.Errorf("Event.MsgFunc() was not called on non-nil Event")
|
||||
}
|
||||
|
||||
want := `{"message":"called"}`
|
||||
got := strings.TrimSpace(buf.String())
|
||||
if got != want {
|
||||
t.Errorf("Event.MsgFunc() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_CallerRuntimeFail(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel, false, nil, nil)
|
||||
|
||||
// Set a very large skipFrame to make runtime.Caller fail
|
||||
e.CallerSkipFrame(1000)
|
||||
e.Caller()
|
||||
|
||||
e.Msg("test")
|
||||
|
||||
got := strings.TrimSpace(buf.String())
|
||||
want := `{"message":"test"}` // No caller field because runtime.Caller failed
|
||||
if got != want {
|
||||
t.Errorf("Event.Caller() with failed runtime.Caller = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_DoneHandler(t *testing.T) {
|
||||
e := newEvent(nil, InfoLevel, false, nil, nil)
|
||||
|
||||
// Set up a done handler to capture calls
|
||||
var called bool
|
||||
var capturedMsg string
|
||||
e.done = func(msg string) {
|
||||
called = true
|
||||
capturedMsg = msg
|
||||
}
|
||||
|
||||
// Trigger msg via Msg
|
||||
e.Msg("test message")
|
||||
|
||||
// Assert the handler was called with the correct message
|
||||
if !called {
|
||||
t.Error("Done handler was not called")
|
||||
}
|
||||
if capturedMsg != "test message" {
|
||||
t.Errorf("Expected message 'test message', got '%s'", capturedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
type badLevelWriter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *badLevelWriter) WriteLevel(level Level, p []byte) (n int, err error) {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
func (w *badLevelWriter) Write(p []byte) (n int, err error) {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
func TestEvent_Msg_ErrorHandlerNil(t *testing.T) {
|
||||
// Save original ErrorHandler and restore after test
|
||||
originalErrorHandler := ErrorHandler
|
||||
ErrorHandler = nil
|
||||
defer func() { ErrorHandler = originalErrorHandler }()
|
||||
|
||||
// Create a LevelWriter that always returns an error
|
||||
mockWriter := &badLevelWriter{err: errors.New("write error")}
|
||||
|
||||
e := newEvent(mockWriter, InfoLevel, false, nil, nil)
|
||||
if e == nil {
|
||||
t.Fatal("Event should not be nil")
|
||||
}
|
||||
|
||||
// Capture stderr
|
||||
oldStderr := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stderr = w
|
||||
|
||||
// Call Msg to trigger write error
|
||||
e.Msg("test message")
|
||||
|
||||
// Restore stderr and read captured output
|
||||
w.Close()
|
||||
os.Stderr = oldStderr
|
||||
captured, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert the error message was printed to stderr
|
||||
expected := "zerolog: could not write event: write error\n"
|
||||
if string(captured) != expected {
|
||||
t.Errorf("Expected stderr output %q, got %q", expected, string(captured))
|
||||
}
|
||||
}
|
||||
|
||||
type mockLogObjectMarshaler struct {
|
||||
data string
|
||||
}
|
||||
|
||||
func (m mockLogObjectMarshaler) MarshalZerologObject(e *Event) {
|
||||
e.Str("stack_func", m.data)
|
||||
}
|
||||
|
||||
func TestEvent_ErrWithStackMarshaler(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return "stack-trace"
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Err(err).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"stack":"stack-trace","error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Err() with stack marshaler = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_FieldsWithErrorAndStackMarshaler(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return "stack-trace"
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Fields([]interface{}{"error", err}).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"error":"test error","stack":"stack-trace","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Fields() with error and stack marshaler = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_FieldsWithErrorAndStackMarshalerObject(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns LogObjectMarshaler
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return mockLogObjectMarshaler{data: "stack-data"}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Fields([]interface{}{"error", err}).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"error":"test error","stack":{"stack_func":"stack-data"},"message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Fields() with error and stack marshaler object = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_FieldsWithErrorAndStackMarshalerError(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns an error
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return errors.New("stack error")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Fields([]interface{}{"error", err}).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"error":"test error","stack":"stack error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Fields() with error and stack marshaler error = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_FieldsWithErrorAndStackMarshalerInterface(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns an int
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return 42
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Fields([]interface{}{"error", err}).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"error":"test error","stack":42,"message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Fields() with error and stack marshaler interface = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_FieldsWithErrorAndStackMarshalerNil(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set marshaler to return nil
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Fields([]interface{}{"error", err}).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"error":"test error","message":"test message"}` + "\n" // No stack field because marshaler returned nil
|
||||
if got != want {
|
||||
t.Errorf("Event.Fields() with error and nil stack marshaler = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_ErrWithStackMarshalerObject(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns LogObjectMarshaler
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return mockLogObjectMarshaler{data: "stack-data"}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Err(err).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"stack":{"stack_func":"stack-data"},"error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Err() with stack marshaler object = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_ErrWithStackMarshalerError(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns an error
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return errors.New("stack error")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Err(err).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"stack":"stack error","error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Err() with stack marshaler error = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_ErrWithStackMarshalerInterface(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set a mock marshaler that returns an int
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return 42
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Err(err).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"stack":42,"error":"test error","message":"test message"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("Event.Err() with stack marshaler interface = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_ErrWithStackMarshalerNil(t *testing.T) {
|
||||
// Save original
|
||||
original := ErrorStackMarshaler
|
||||
defer func() { ErrorStackMarshaler = original }()
|
||||
|
||||
// Set marshaler to return nil
|
||||
ErrorStackMarshaler = func(err error) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := New(&buf)
|
||||
|
||||
err := errors.New("test error")
|
||||
log.Log().Stack().Err(err).Msg("test message")
|
||||
|
||||
got := buf.String()
|
||||
want := `{"message":"test message"}` + "\n" // No fields because stack marshaler returned nil
|
||||
if got != want {
|
||||
t.Errorf("Event.Err() with nil stack marshaler = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"time":"5:41PM","level":"info","message":"Starting listener","listen":":8080","pid":37556}
|
||||
{"time":"5:41PM","level":"debug","message":"Access","database":"myapp","host":"localhost:4962","pid":37556}
|
||||
{"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":23}
|
||||
{"time":"5:41PM","level":"info","message":"Access","method":"POST","path":"/posts","pid":37556,"resp_time":532}
|
||||
{"time":"5:41PM","level":"warn","message":"Slow request","method":"POST","path":"/posts","pid":37556,"resp_time":532}
|
||||
{"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":10}
|
||||
{"time":"5:41PM","level":"error","message":"Database connection lost","database":"myapp","pid":37556,"error":"connection reset by peer"}
|
||||
@@ -1,26 +1,61 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func appendFields(dst []byte, fields map[string]interface{}) []byte {
|
||||
keys := make([]string, 0, len(fields))
|
||||
for key := range fields {
|
||||
keys = append(keys, key)
|
||||
func isNilValue(e error) bool {
|
||||
switch reflect.TypeOf(e).Kind() {
|
||||
case reflect.Ptr:
|
||||
return reflect.ValueOf(e).IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
dst = enc.AppendKey(dst, key)
|
||||
val := fields[key]
|
||||
if val, ok := val.(LogObjectMarshaler); ok {
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(val)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
}
|
||||
|
||||
func appendFields(dst []byte, fields interface{}, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
switch fields := fields.(type) {
|
||||
case []interface{}:
|
||||
if n := len(fields); n&0x1 == 1 { // odd number
|
||||
fields = fields[:n-1]
|
||||
}
|
||||
dst = appendFieldList(dst, fields, stack, ctx, hooks)
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(fields))
|
||||
for key := range fields {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
kv := make([]interface{}, 2)
|
||||
for _, key := range keys {
|
||||
kv[0], kv[1] = key, fields[key]
|
||||
dst = appendFieldList(dst, kv, stack, ctx, hooks)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendObject(dst []byte, obj LogObjectMarshaler, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
e := newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, stack, ctx, hooks)
|
||||
e.buf = e.buf[:0] // discard the beginning marker added by newEvent
|
||||
e.appendObject(obj)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendFieldList(dst []byte, kvList []interface{}, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
for i, n := 0, len(kvList); i < n; i += 2 {
|
||||
key, val := kvList[i], kvList[i+1]
|
||||
if key, ok := key.(string); ok {
|
||||
dst = enc.AppendKey(dst, key)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
switch val := val.(type) {
|
||||
@@ -29,34 +64,51 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
|
||||
case []byte:
|
||||
dst = enc.AppendBytes(dst, val)
|
||||
case error:
|
||||
marshaled := ErrorMarshalFunc(val)
|
||||
switch m := marshaled.(type) {
|
||||
switch m := ErrorMarshalFunc(val).(type) {
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case LogObjectMarshaler:
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(m)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
if !isNilValue(m) {
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
}
|
||||
case string:
|
||||
dst = enc.AppendString(dst, m)
|
||||
default:
|
||||
dst = enc.AppendInterface(dst, m)
|
||||
}
|
||||
|
||||
if stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(val).(type) {
|
||||
case nil:
|
||||
return dst // do nothing with nil errors
|
||||
case LogObjectMarshaler:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
case string:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendString(dst, m)
|
||||
default:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendInterface(dst, m)
|
||||
}
|
||||
}
|
||||
case []error:
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
for i, err := range val {
|
||||
marshaled := ErrorMarshalFunc(err)
|
||||
switch m := marshaled.(type) {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case LogObjectMarshaler:
|
||||
e := newEvent(nil, 0)
|
||||
e.buf = e.buf[:0]
|
||||
e.appendObject(m)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
if !isNilValue(m) {
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
}
|
||||
case string:
|
||||
dst = enc.AppendString(dst, m)
|
||||
default:
|
||||
@@ -64,7 +116,16 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
|
||||
}
|
||||
|
||||
if i < (len(val) - 1) {
|
||||
enc.AppendArrayDelim(dst)
|
||||
dst = enc.AppendArrayDelim(dst)
|
||||
}
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
case []LogObjectMarshaler:
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
for i, obj := range val {
|
||||
dst = appendObject(dst, obj, stack, ctx, hooks)
|
||||
if i < (len(val) - 1) {
|
||||
dst = enc.AppendArrayDelim(dst)
|
||||
}
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
@@ -91,45 +152,109 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
|
||||
case uint64:
|
||||
dst = enc.AppendUint64(dst, val)
|
||||
case float32:
|
||||
dst = enc.AppendFloat32(dst, val)
|
||||
dst = enc.AppendFloat32(dst, val, FloatingPointPrecision)
|
||||
case float64:
|
||||
dst = enc.AppendFloat64(dst, val)
|
||||
dst = enc.AppendFloat64(dst, val, FloatingPointPrecision)
|
||||
case time.Time:
|
||||
dst = enc.AppendTime(dst, val, TimeFieldFormat)
|
||||
case time.Duration:
|
||||
dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldInteger)
|
||||
dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
case *string:
|
||||
dst = enc.AppendString(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendString(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *bool:
|
||||
dst = enc.AppendBool(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendBool(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int:
|
||||
dst = enc.AppendInt(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendInt(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int8:
|
||||
dst = enc.AppendInt8(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendInt8(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int16:
|
||||
dst = enc.AppendInt16(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendInt16(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int32:
|
||||
dst = enc.AppendInt32(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendInt32(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int64:
|
||||
dst = enc.AppendInt64(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendInt64(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint:
|
||||
dst = enc.AppendUint(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendUint(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint8:
|
||||
dst = enc.AppendUint8(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendUint8(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint16:
|
||||
dst = enc.AppendUint16(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendUint16(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint32:
|
||||
dst = enc.AppendUint32(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendUint32(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint64:
|
||||
dst = enc.AppendUint64(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendUint64(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *float32:
|
||||
dst = enc.AppendFloat32(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendFloat32(dst, *val, FloatingPointPrecision)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *float64:
|
||||
dst = enc.AppendFloat64(dst, *val)
|
||||
if val != nil {
|
||||
dst = enc.AppendFloat64(dst, *val, FloatingPointPrecision)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *time.Time:
|
||||
dst = enc.AppendTime(dst, *val, TimeFieldFormat)
|
||||
if val != nil {
|
||||
dst = enc.AppendTime(dst, *val, TimeFieldFormat)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *time.Duration:
|
||||
dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldInteger)
|
||||
if val != nil {
|
||||
dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case []string:
|
||||
dst = enc.AppendStrings(dst, val)
|
||||
case []bool:
|
||||
@@ -146,8 +271,7 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
|
||||
dst = enc.AppendInts64(dst, val)
|
||||
case []uint:
|
||||
dst = enc.AppendUints(dst, val)
|
||||
// case []uint8:
|
||||
// dst = enc.AppendUints8(dst, val)
|
||||
// case []uint8: is handled as []byte above
|
||||
case []uint16:
|
||||
dst = enc.AppendUints16(dst, val)
|
||||
case []uint32:
|
||||
@@ -155,23 +279,33 @@ func appendFields(dst []byte, fields map[string]interface{}) []byte {
|
||||
case []uint64:
|
||||
dst = enc.AppendUints64(dst, val)
|
||||
case []float32:
|
||||
dst = enc.AppendFloats32(dst, val)
|
||||
dst = enc.AppendFloats32(dst, val, FloatingPointPrecision)
|
||||
case []float64:
|
||||
dst = enc.AppendFloats64(dst, val)
|
||||
dst = enc.AppendFloats64(dst, val, FloatingPointPrecision)
|
||||
case []time.Time:
|
||||
dst = enc.AppendTimes(dst, val, TimeFieldFormat)
|
||||
case []time.Duration:
|
||||
dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldInteger)
|
||||
dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case net.IP:
|
||||
dst = enc.AppendIPAddr(dst, val)
|
||||
case []net.IP:
|
||||
dst = enc.AppendIPAddrs(dst, val)
|
||||
case net.IPNet:
|
||||
dst = enc.AppendIPPrefix(dst, val)
|
||||
case []net.IPNet:
|
||||
dst = enc.AppendIPPrefixes(dst, val)
|
||||
case net.HardwareAddr:
|
||||
dst = enc.AppendMACAddr(dst, val)
|
||||
case json.RawMessage:
|
||||
dst = appendJSON(dst, val)
|
||||
default:
|
||||
dst = enc.AppendInterface(dst, val)
|
||||
if lom, ok := val.(LogObjectMarshaler); ok {
|
||||
dst = appendObject(dst, lom, stack, ctx, hooks)
|
||||
} else {
|
||||
dst = enc.AppendInterface(dst, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fixtureObj struct {
|
||||
Pub string
|
||||
Tag string `json:"tag"`
|
||||
priv int
|
||||
}
|
||||
|
||||
func (o fixtureObj) MarshalZerologObject(e *Event) {
|
||||
e.Str("Pub", o.Pub).
|
||||
Str("Tag", o.Tag).
|
||||
Int("priv", o.priv)
|
||||
}
|
||||
|
||||
type fieldFixtures struct {
|
||||
Bools []bool
|
||||
Bytes []byte
|
||||
Ctx context.Context
|
||||
Durations []time.Duration
|
||||
Errs []error
|
||||
Floats32 []float32
|
||||
Floats64 []float64
|
||||
Interfaces []struct {
|
||||
Pub string
|
||||
Tag string `json:"tag"`
|
||||
priv int
|
||||
}
|
||||
Ints []int
|
||||
Ints8 []int8
|
||||
Ints16 []int16
|
||||
Ints32 []int32
|
||||
Ints64 []int64
|
||||
Uints []uint
|
||||
Uints8 []uint8
|
||||
Uints16 []uint16
|
||||
Uints32 []uint32
|
||||
Uints64 []uint64
|
||||
IPAddrs []net.IP
|
||||
IPPfxs []net.IPNet
|
||||
MACAddr net.HardwareAddr
|
||||
Objects []LogObjectMarshaler
|
||||
RawCBOR []byte
|
||||
RawJSONs [][]byte
|
||||
Stringers []fmt.Stringer
|
||||
Strings []string
|
||||
Times []time.Time
|
||||
Type reflect.Type
|
||||
}
|
||||
|
||||
func makeFieldFixtures() *fieldFixtures {
|
||||
bools := []bool{true, false, true, false, true, false, true, false, true, false}
|
||||
bytes := []byte(`abcdef`)
|
||||
ints := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
ints8 := []int8{-8, 8}
|
||||
ints16 := []int16{-16, 16}
|
||||
ints32 := []int32{-32, 32}
|
||||
ints64 := []int64{-64, 64}
|
||||
uints := []uint{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
uints8 := []uint8{8, uint8(^uint8(0))}
|
||||
uints16 := []uint16{16, uint16(^uint16(0))}
|
||||
uints32 := []uint32{32, uint32(^uint32(0))}
|
||||
uints64 := []uint64{64, uint64(^uint64(0))}
|
||||
floats32 := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
floats64 := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
strings := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
|
||||
durations := []time.Duration{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
times := []time.Time{
|
||||
time.Unix(0, 0),
|
||||
time.Unix(1, 0),
|
||||
time.Unix(2, 0),
|
||||
time.Unix(3, 0),
|
||||
time.Unix(4, 0),
|
||||
time.Unix(5, 0),
|
||||
time.Unix(6, 0),
|
||||
time.Unix(7, 0),
|
||||
time.Unix(8, 0),
|
||||
time.Unix(9, 0),
|
||||
}
|
||||
interfaces := []struct {
|
||||
Pub string
|
||||
Tag string `json:"tag"`
|
||||
priv int
|
||||
}{
|
||||
{"A", "j", -5},
|
||||
{"B", "i", -4},
|
||||
{"C", "h", -3},
|
||||
{"D", "g", -2},
|
||||
{"E", "f", -1},
|
||||
{"F", "e", 0},
|
||||
{"G", "d", 1},
|
||||
{"H", "c", 2},
|
||||
{"I", "b", 3},
|
||||
{"J", "a", 4},
|
||||
}
|
||||
objects := []LogObjectMarshaler{
|
||||
fixtureObj{"a", "z", 1},
|
||||
fixtureObj{"b", "y", 2},
|
||||
fixtureObj{"c", "x", 3},
|
||||
fixtureObj{"d", "w", 4},
|
||||
fixtureObj{"e", "v", 5},
|
||||
fixtureObj{"f", "u", 6},
|
||||
fixtureObj{"g", "t", 7},
|
||||
fixtureObj{"h", "s", 8},
|
||||
fixtureObj{"i", "r", 9},
|
||||
fixtureObj{"j", "q", 10},
|
||||
}
|
||||
ipAddrV4 := net.IP{192, 168, 0, 1}
|
||||
ipAddrV6 := net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}
|
||||
ipAddrs := []net.IP{ipAddrV4, ipAddrV6, ipAddrV4, ipAddrV6, ipAddrV4, ipAddrV6, ipAddrV4, ipAddrV6, ipAddrV4, ipAddrV6}
|
||||
ipPfxV4 := net.IPNet{IP: net.IP{192, 168, 0, 0}, Mask: net.CIDRMask(24, 32)}
|
||||
ipPfxV6 := net.IPNet{IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x00}, Mask: net.CIDRMask(64, 128)}
|
||||
ipPfxs := []net.IPNet{ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6, ipPfxV4, ipPfxV6}
|
||||
macAddr := net.HardwareAddr{0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E}
|
||||
errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e"), nil, loggableError{fmt.Errorf("oops")}}
|
||||
ctx := context.Background()
|
||||
stringers := []fmt.Stringer{ipAddrs[0], durations[0]}
|
||||
rawJSONs := [][]byte{[]byte(`{"some":"json"}`), []byte(`{"longer":[1111,2222,3333,4444,5555]}`)}
|
||||
rawCBOR := []byte{0xA1, 0x64, 0x73, 0x6F, 0x6D, 0x65, 0x64, 0x61, 0x74, 0x61} // {"some":"data"}
|
||||
|
||||
return &fieldFixtures{
|
||||
Bools: bools,
|
||||
Bytes: bytes,
|
||||
Ctx: ctx,
|
||||
Durations: durations,
|
||||
Errs: errs,
|
||||
Floats32: floats32,
|
||||
Floats64: floats64,
|
||||
Interfaces: interfaces,
|
||||
Ints: ints,
|
||||
Ints8: ints8,
|
||||
Ints16: ints16,
|
||||
Ints32: ints32,
|
||||
Ints64: ints64,
|
||||
Uints: uints,
|
||||
Uints8: uints8,
|
||||
Uints16: uints16,
|
||||
Uints32: uints32,
|
||||
Uints64: uints64,
|
||||
IPAddrs: ipAddrs,
|
||||
IPPfxs: ipPfxs,
|
||||
MACAddr: macAddr,
|
||||
Objects: objects,
|
||||
RawCBOR: rawCBOR,
|
||||
RawJSONs: rawJSONs,
|
||||
Stringers: stringers,
|
||||
Strings: strings,
|
||||
Times: times,
|
||||
Type: reflect.TypeOf(12345),
|
||||
}
|
||||
}
|
||||
+149
-12
@@ -1,7 +1,40 @@
|
||||
package zerolog
|
||||
|
||||
import "time"
|
||||
import "sync/atomic"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// TimeFormatUnix defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers.
|
||||
TimeFormatUnix = ""
|
||||
|
||||
// TimeFormatUnixMs defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers in milliseconds.
|
||||
TimeFormatUnixMs = "UNIXMS"
|
||||
|
||||
// TimeFormatUnixMicro defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers in microseconds.
|
||||
TimeFormatUnixMicro = "UNIXMICRO"
|
||||
|
||||
// TimeFormatUnixNano defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers in nanoseconds.
|
||||
TimeFormatUnixNano = "UNIXNANO"
|
||||
|
||||
// DurationFormatFloat defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as floating point numbers.
|
||||
DurationFormatFloat = "float"
|
||||
// DurationFormatInt defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as integers.
|
||||
DurationFormatInt = "int"
|
||||
// DurationFormatString defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as string.
|
||||
DurationFormatString = "string"
|
||||
)
|
||||
|
||||
var (
|
||||
// TimestampFieldName is the field name used for the timestamp field.
|
||||
@@ -10,6 +43,26 @@ var (
|
||||
// LevelFieldName is the field name used for the level field.
|
||||
LevelFieldName = "level"
|
||||
|
||||
// LevelTraceValue is the value used for the trace level field.
|
||||
LevelTraceValue = "trace"
|
||||
// LevelDebugValue is the value used for the debug level field.
|
||||
LevelDebugValue = "debug"
|
||||
// LevelInfoValue is the value used for the info level field.
|
||||
LevelInfoValue = "info"
|
||||
// LevelWarnValue is the value used for the warn level field.
|
||||
LevelWarnValue = "warn"
|
||||
// LevelErrorValue is the value used for the error level field.
|
||||
LevelErrorValue = "error"
|
||||
// LevelFatalValue is the value used for the fatal level field.
|
||||
LevelFatalValue = "fatal"
|
||||
// LevelPanicValue is the value used for the panic level field.
|
||||
LevelPanicValue = "panic"
|
||||
|
||||
// LevelFieldMarshalFunc allows customization of global level field marshaling.
|
||||
LevelFieldMarshalFunc = func(l Level) string {
|
||||
return l.String()
|
||||
}
|
||||
|
||||
// MessageFieldName is the field name used for the message field.
|
||||
MessageFieldName = "message"
|
||||
|
||||
@@ -22,26 +75,110 @@ var (
|
||||
// CallerSkipFrameCount is the number of stack frames to skip to find the caller.
|
||||
CallerSkipFrameCount = 2
|
||||
|
||||
// TimeFieldFormat defines the time format of the Time field type.
|
||||
// If set to an empty string, the time is formatted as an UNIX timestamp
|
||||
// as integer.
|
||||
// CallerMarshalFunc allows customization of global caller marshaling
|
||||
CallerMarshalFunc = func(pc uintptr, file string, line int) string {
|
||||
return file + ":" + strconv.Itoa(line)
|
||||
}
|
||||
|
||||
// ErrorStackFieldName is the field name used for error stacks.
|
||||
ErrorStackFieldName = "stack"
|
||||
|
||||
// ErrorStackMarshaler extract the stack from err if any.
|
||||
ErrorStackMarshaler func(err error) interface{}
|
||||
|
||||
// ErrorMarshalFunc allows customization of global error marshaling
|
||||
ErrorMarshalFunc = func(err error) interface{} {
|
||||
return err
|
||||
}
|
||||
|
||||
// InterfaceMarshalFunc allows customization of interface marshaling.
|
||||
// Default: "encoding/json.Marshal" with disabled HTML escaping
|
||||
InterfaceMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := buf.Bytes()
|
||||
if len(b) > 0 {
|
||||
// Remove trailing \n which is added by Encode.
|
||||
return b[:len(b)-1], nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// TimeFieldFormat defines the time format of the Time field type. If set to
|
||||
// TimeFormatUnix, TimeFormatUnixMs, TimeFormatUnixMicro or TimeFormatUnixNano, the time is formatted as a UNIX
|
||||
// timestamp as integer.
|
||||
TimeFieldFormat = time.RFC3339
|
||||
|
||||
// TimestampFunc defines the function called to generate a timestamp.
|
||||
TimestampFunc = time.Now
|
||||
|
||||
// DurationFieldFormat defines the format of the Duration field type.
|
||||
DurationFieldFormat = DurationFormatFloat
|
||||
|
||||
// DurationFieldUnit defines the unit for time.Duration type fields added
|
||||
// using the Dur method.
|
||||
DurationFieldUnit = time.Millisecond
|
||||
|
||||
// DurationFieldInteger renders Dur fields as integer instead of float if
|
||||
// set to true.
|
||||
// Deprecated: use DurationFieldFormat with DurationFormatInt instead.
|
||||
DurationFieldInteger = false
|
||||
|
||||
// ErrorHandler is called whenever zerolog fails to write an event on its
|
||||
// output. If not set, an error is printed on the stderr. This handler must
|
||||
// be thread safe and non-blocking.
|
||||
ErrorHandler func(err error)
|
||||
|
||||
// FatalExitFunc is called by log.Fatal() instead of os.Exit(1). If not set,
|
||||
// os.Exit(1) is called.
|
||||
FatalExitFunc func()
|
||||
|
||||
// DefaultContextLogger is returned from Ctx() if there is no logger associated
|
||||
// with the context.
|
||||
DefaultContextLogger *Logger
|
||||
|
||||
// LevelColors are used by ConsoleWriter's consoleDefaultFormatLevel to color
|
||||
// log levels.
|
||||
LevelColors = map[Level]int{
|
||||
TraceLevel: colorBlue,
|
||||
DebugLevel: 0,
|
||||
InfoLevel: colorGreen,
|
||||
WarnLevel: colorYellow,
|
||||
ErrorLevel: colorRed,
|
||||
FatalLevel: colorRed,
|
||||
PanicLevel: colorRed,
|
||||
}
|
||||
|
||||
// FormattedLevels are used by ConsoleWriter's consoleDefaultFormatLevel
|
||||
// for a short level name.
|
||||
FormattedLevels = map[Level]string{
|
||||
TraceLevel: "TRC",
|
||||
DebugLevel: "DBG",
|
||||
InfoLevel: "INF",
|
||||
WarnLevel: "WRN",
|
||||
ErrorLevel: "ERR",
|
||||
FatalLevel: "FTL",
|
||||
PanicLevel: "PNC",
|
||||
}
|
||||
|
||||
// TriggerLevelWriterBufferReuseLimit is a limit in bytes that a buffer is dropped
|
||||
// from the TriggerLevelWriter buffer pool if the buffer grows above the limit.
|
||||
TriggerLevelWriterBufferReuseLimit = 64 * 1024
|
||||
|
||||
// FloatingPointPrecision, if set to a value other than -1, controls the number
|
||||
// of digits when formatting float numbers in JSON. See strconv.FormatFloat for
|
||||
// more details.
|
||||
FloatingPointPrecision = -1
|
||||
)
|
||||
|
||||
var (
|
||||
gLevel = new(uint32)
|
||||
disableSampling = new(uint32)
|
||||
gLevel = new(int32)
|
||||
disableSampling = new(int32)
|
||||
)
|
||||
|
||||
// SetGlobalLevel sets the global override for log level. If this
|
||||
@@ -49,23 +186,23 @@ var (
|
||||
//
|
||||
// To globally disable logs, set GlobalLevel to Disabled.
|
||||
func SetGlobalLevel(l Level) {
|
||||
atomic.StoreUint32(gLevel, uint32(l))
|
||||
atomic.StoreInt32(gLevel, int32(l))
|
||||
}
|
||||
|
||||
// GlobalLevel returns the current global log level
|
||||
func GlobalLevel() Level {
|
||||
return Level(atomic.LoadUint32(gLevel))
|
||||
return Level(atomic.LoadInt32(gLevel))
|
||||
}
|
||||
|
||||
// DisableSampling will disable sampling in all Loggers if true.
|
||||
func DisableSampling(v bool) {
|
||||
var i uint32
|
||||
var i int32
|
||||
if v {
|
||||
i = 1
|
||||
}
|
||||
atomic.StoreUint32(disableSampling, i)
|
||||
atomic.StoreInt32(disableSampling, i)
|
||||
}
|
||||
|
||||
func samplingDisabled() bool {
|
||||
return atomic.LoadUint32(disableSampling) == 1
|
||||
return atomic.LoadInt32(disableSampling) == 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func AsLogObjectMarshalers[T LogObjectMarshaler](objs []T) []LogObjectMarshaler {
|
||||
if objs == nil {
|
||||
return nil
|
||||
}
|
||||
s := make([]LogObjectMarshaler, len(objs))
|
||||
for i, v := range objs {
|
||||
s[i] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func AsStringers[T fmt.Stringer](objs []T) []fmt.Stringer {
|
||||
if objs == nil {
|
||||
return nil
|
||||
}
|
||||
s := make([]fmt.Stringer, len(objs))
|
||||
for i, v := range objs {
|
||||
s[i] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1 +1,15 @@
|
||||
module github.com/rs/zerolog
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/xid v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -0,0 +1,7 @@
|
||||
// +build go1.12
|
||||
|
||||
package zerolog
|
||||
|
||||
// Since go 1.12, some auto generated init functions are hidden from
|
||||
// runtime.Caller.
|
||||
const contextCallerSkipFrameCount = 2
|
||||
+147
-5
@@ -5,12 +5,13 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/hlog/internal/mutil"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/zenazn/goji/web/mutil"
|
||||
)
|
||||
|
||||
// FromRequest gets the logger in the request's context.
|
||||
@@ -79,10 +80,39 @@ func RequestHandler(fieldKey string) func(next http.Handler) http.Handler {
|
||||
func RemoteAddrHandler(fieldKey string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
if r.RemoteAddr != "" {
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, host)
|
||||
return c.Str(fieldKey, r.RemoteAddr)
|
||||
})
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getHost(hostPort string) string {
|
||||
if hostPort == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(hostPort)
|
||||
if err != nil {
|
||||
return hostPort
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// RemoteIPHandler is similar to RemoteAddrHandler, but logs only
|
||||
// an IP, not a port.
|
||||
func RemoteIPHandler(fieldKey string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := getHost(r.RemoteAddr)
|
||||
if ip != "" {
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, ip)
|
||||
})
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
@@ -122,6 +152,35 @@ func RefererHandler(fieldKey string) func(next http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// ProtoHandler adds the requests protocol version as a field to the context logger
|
||||
// using fieldKey as field Key.
|
||||
func ProtoHandler(fieldKey string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, r.Proto)
|
||||
})
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPVersionHandler is similar to ProtoHandler, but it does not store the "HTTP/"
|
||||
// prefix in the protocol name.
|
||||
func HTTPVersionHandler(fieldKey string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proto := strings.TrimPrefix(r.Proto, "HTTP/")
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, proto)
|
||||
})
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type idKey struct{}
|
||||
|
||||
// IDFromRequest returns the unique id associated to the request if any.
|
||||
@@ -138,6 +197,11 @@ func IDFromCtx(ctx context.Context) (id xid.ID, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// CtxWithID adds the given xid.ID to the context
|
||||
func CtxWithID(ctx context.Context, id xid.ID) context.Context {
|
||||
return context.WithValue(ctx, idKey{}, id)
|
||||
}
|
||||
|
||||
// RequestIDHandler returns a handler setting a unique id to the request which can
|
||||
// be gathered using IDFromRequest(req). This generated id is added as a field to the
|
||||
// logger using the passed fieldKey as field name. The id is also added as a response
|
||||
@@ -154,7 +218,7 @@ func RequestIDHandler(fieldKey, headerName string) func(next http.Handler) http.
|
||||
id, ok := IDFromRequest(r)
|
||||
if !ok {
|
||||
id = xid.New()
|
||||
ctx = context.WithValue(ctx, idKey{}, id)
|
||||
ctx = CtxWithID(ctx, id)
|
||||
r = r.WithContext(ctx)
|
||||
}
|
||||
if fieldKey != "" {
|
||||
@@ -171,14 +235,92 @@ func RequestIDHandler(fieldKey, headerName string) func(next http.Handler) http.
|
||||
}
|
||||
}
|
||||
|
||||
// CustomHeaderHandler adds given header from request's header as a field to
|
||||
// the context's logger using fieldKey as field key.
|
||||
func CustomHeaderHandler(fieldKey, header string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if val := r.Header.Get(header); val != "" {
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, val)
|
||||
})
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// EtagHandler adds Etag header from response's header as a field to
|
||||
// the context's logger using fieldKey as field key.
|
||||
func EtagHandler(fieldKey string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
etag := w.Header().Get("Etag")
|
||||
if etag != "" {
|
||||
etag = strings.ReplaceAll(etag, `"`, "")
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, etag)
|
||||
})
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ResponseHeaderHandler(fieldKey, headerName string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
value := w.Header().Get(headerName)
|
||||
if value != "" {
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, value)
|
||||
})
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AccessHandler returns a handler that call f after each request.
|
||||
func AccessHandler(f func(r *http.Request, status, size int, duration time.Duration)) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
lw := mutil.WrapWriter(w)
|
||||
defer func() {
|
||||
f(r, lw.Status(), lw.BytesWritten(), time.Since(start))
|
||||
}()
|
||||
next.ServeHTTP(lw, r)
|
||||
f(r, lw.Status(), lw.BytesWritten(), time.Since(start))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// HostHandler adds the request's host as a field to the context's logger
|
||||
// using fieldKey as field key. If trimPort is set to true, then port is
|
||||
// removed from the host.
|
||||
func HostHandler(fieldKey string, trimPort ...bool) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var host string
|
||||
if len(trimPort) > 0 && trimPort[0] {
|
||||
host = getHost(r.Host)
|
||||
} else {
|
||||
host = r.Host
|
||||
}
|
||||
if host != "" {
|
||||
log := zerolog.Ctx(r.Context())
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str(fieldKey, host)
|
||||
})
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ func Example_handler() {
|
||||
// Install the logger handler with default output on the console
|
||||
c = c.Append(hlog.NewHandler(log))
|
||||
|
||||
// Install some provided extra handler to set some request's context fields.
|
||||
// Thanks to those handler, all our logs will come with some pre-populated fields.
|
||||
// Install some provided extra handlers to set some request's context fields.
|
||||
// Thanks to those handlers, all our logs will come with some pre-populated fields.
|
||||
c = c.Append(hlog.RemoteAddrHandler("ip"))
|
||||
c = c.Append(hlog.UserAgentHandler("user_agent"))
|
||||
c = c.Append(hlog.RefererHandler("referer"))
|
||||
@@ -63,11 +63,11 @@ func Example_handler() {
|
||||
hlog.FromRequest(r).Info().
|
||||
Str("user", "current user").
|
||||
Str("status", "ok").
|
||||
Msg("Something happend")
|
||||
Msg("Something happened")
|
||||
}))
|
||||
http.Handle("/", h)
|
||||
|
||||
h.ServeHTTP(httptest.NewRecorder(), &http.Request{})
|
||||
|
||||
// Output: {"level":"info","role":"my-service","host":"local-hostname","user":"current user","status":"ok","time":"2001-02-03T04:05:06Z","message":"Something happend"}
|
||||
// Output: {"level":"info","role":"my-service","host":"local-hostname","user":"current user","status":"ok","time":"2001-02-03T04:05:06Z","message":"Something happened"}
|
||||
}
|
||||
|
||||
+199
-10
@@ -1,19 +1,20 @@
|
||||
//go:build go1.7
|
||||
// +build go1.7
|
||||
|
||||
package hlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"reflect"
|
||||
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/internal/cbor"
|
||||
)
|
||||
@@ -100,7 +101,7 @@ func TestRemoteAddrHandler(t *testing.T) {
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"ip":"1.2.3.4"}`+"\n", decodeIfBinary(out); want != got {
|
||||
if want, got := `{"ip":"1.2.3.4:1234"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
@@ -116,6 +117,38 @@ func TestRemoteAddrHandlerIPv6(t *testing.T) {
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"ip":"[2001:db8:a0b:12f0::1]:1234"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteIPHandler(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{
|
||||
RemoteAddr: "1.2.3.4:1234",
|
||||
}
|
||||
h := RemoteIPHandler("ip")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"ip":"1.2.3.4"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteIPHandlerIPv6(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{
|
||||
RemoteAddr: "[2001:db8:a0b:12f0::1]:1234",
|
||||
}
|
||||
h := RemoteIPHandler("ip")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"ip":"2001:db8:a0b:12f0::1"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
@@ -182,6 +215,96 @@ func TestRequestIDHandler(t *testing.T) {
|
||||
h.ServeHTTP(httptest.NewRecorder(), r)
|
||||
}
|
||||
|
||||
func TestCustomHeaderHandler(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{
|
||||
Header: http.Header{
|
||||
"X-Request-Id": []string{"514bbe5bb5251c92bd07a9846f4a1ab6"},
|
||||
},
|
||||
}
|
||||
h := CustomHeaderHandler("reqID", "X-Request-Id")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"reqID":"514bbe5bb5251c92bd07a9846f4a1ab6"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtagHandler(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
w := httptest.NewRecorder()
|
||||
r := &http.Request{}
|
||||
h := EtagHandler("etag")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Etag", `"abcdef"`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
h2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.ServeHTTP(w, r)
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
})
|
||||
h3 := NewHandler(zerolog.New(out))(h2)
|
||||
h3.ServeHTTP(w, r)
|
||||
if want, got := `{"etag":"abcdef"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseHeaderHandler(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
w := httptest.NewRecorder()
|
||||
r := &http.Request{}
|
||||
h := ResponseHeaderHandler("encoding", "Content-Encoding")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Encoding", `gzip`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
h2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.ServeHTTP(w, r)
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
})
|
||||
h3 := NewHandler(zerolog.New(out))(h2)
|
||||
h3.ServeHTTP(w, r)
|
||||
if want, got := `{"encoding":"gzip"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtoHandler(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{
|
||||
Proto: "test",
|
||||
}
|
||||
h := ProtoHandler("proto")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"proto":"test"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPVersionHandler(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{
|
||||
Proto: "HTTP/1.1",
|
||||
}
|
||||
h := HTTPVersionHandler("proto")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"proto":"1.1"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombinedHandlers(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{
|
||||
@@ -210,10 +333,10 @@ func BenchmarkHandlers(b *testing.B) {
|
||||
}))
|
||||
h2 := MethodHandler("method")(RequestHandler("request")(h1))
|
||||
handlers := map[string]http.Handler{
|
||||
"Single": NewHandler(zerolog.New(ioutil.Discard))(h1),
|
||||
"Combined": NewHandler(zerolog.New(ioutil.Discard))(h2),
|
||||
"SingleDisabled": NewHandler(zerolog.New(ioutil.Discard).Level(zerolog.Disabled))(h1),
|
||||
"CombinedDisabled": NewHandler(zerolog.New(ioutil.Discard).Level(zerolog.Disabled))(h2),
|
||||
"Single": NewHandler(zerolog.New(io.Discard))(h1),
|
||||
"Combined": NewHandler(zerolog.New(io.Discard))(h2),
|
||||
"SingleDisabled": NewHandler(zerolog.New(io.Discard).Level(zerolog.Disabled))(h1),
|
||||
"CombinedDisabled": NewHandler(zerolog.New(io.Discard).Level(zerolog.Disabled))(h2),
|
||||
}
|
||||
for name := range handlers {
|
||||
h := handlers[name]
|
||||
@@ -244,3 +367,69 @@ func BenchmarkDataRace(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCtxWithID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
id, _ := xid.FromString(`c0umremcie6smuu506pg`)
|
||||
|
||||
want := context.Background()
|
||||
want = context.WithValue(want, idKey{}, id)
|
||||
|
||||
if got := CtxWithID(ctx, id); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("CtxWithID() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHandler(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{Host: "example.com:8080"}
|
||||
h := HostHandler("host")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"host":"example.com:8080"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHandlerWithoutPort(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
r := &http.Request{Host: "example.com:8080"}
|
||||
h := HostHandler("host", true)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := FromRequest(r)
|
||||
l.Log().Msg("")
|
||||
}))
|
||||
h = NewHandler(zerolog.New(out))(h)
|
||||
h.ServeHTTP(nil, r)
|
||||
if want, got := `{"host":"example.com"}`+"\n", decodeIfBinary(out); want != got {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHost(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"example.com:8080", "example.com"},
|
||||
{"example.com", "example.com"},
|
||||
{"invalid", "invalid"},
|
||||
{"192.168.0.1:8080", "192.168.0.1"},
|
||||
{"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
|
||||
{"こんにちは.com:8080", "こんにちは.com"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := getHost(tt.input)
|
||||
if tt.expected != result {
|
||||
t.Errorf("Invalid log output, got: %s, want: %s", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2014, 2015, 2016 Carl Jackson (carl@avtok.com)
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,6 @@
|
||||
// Package mutil contains various functions that are helpful when writing http
|
||||
// middleware.
|
||||
//
|
||||
// It has been vendored from Goji v1.0, with the exception of the code for Go 1.8:
|
||||
// https://github.com/zenazn/goji/
|
||||
package mutil
|
||||
@@ -0,0 +1,154 @@
|
||||
package mutil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// WriterProxy is a proxy around an http.ResponseWriter that allows you to hook
|
||||
// into various parts of the response process.
|
||||
type WriterProxy interface {
|
||||
http.ResponseWriter
|
||||
// Status returns the HTTP status of the request, or 0 if one has not
|
||||
// yet been sent.
|
||||
Status() int
|
||||
// BytesWritten returns the total number of bytes sent to the client.
|
||||
BytesWritten() int
|
||||
// Tee causes the response body to be written to the given io.Writer in
|
||||
// addition to proxying the writes through. Only one io.Writer can be
|
||||
// tee'd to at once: setting a second one will overwrite the first.
|
||||
// Writes will be sent to the proxy before being written to this
|
||||
// io.Writer. It is illegal for the tee'd writer to be modified
|
||||
// concurrently with writes.
|
||||
Tee(io.Writer)
|
||||
// Unwrap returns the original proxied target.
|
||||
Unwrap() http.ResponseWriter
|
||||
}
|
||||
|
||||
// WrapWriter wraps an http.ResponseWriter, returning a proxy that allows you to
|
||||
// hook into various parts of the response process.
|
||||
func WrapWriter(w http.ResponseWriter) WriterProxy {
|
||||
_, cn := w.(http.CloseNotifier)
|
||||
_, fl := w.(http.Flusher)
|
||||
_, hj := w.(http.Hijacker)
|
||||
_, rf := w.(io.ReaderFrom)
|
||||
|
||||
bw := basicWriter{ResponseWriter: w}
|
||||
if cn && fl && hj && rf {
|
||||
return &fancyWriter{bw}
|
||||
}
|
||||
if fl {
|
||||
return &flushWriter{bw}
|
||||
}
|
||||
return &bw
|
||||
}
|
||||
|
||||
// basicWriter wraps a http.ResponseWriter that implements the minimal
|
||||
// http.ResponseWriter interface.
|
||||
type basicWriter struct {
|
||||
http.ResponseWriter
|
||||
wroteHeader bool
|
||||
code int
|
||||
bytes int
|
||||
tee io.Writer
|
||||
}
|
||||
|
||||
func (b *basicWriter) WriteHeader(code int) {
|
||||
if !b.wroteHeader {
|
||||
b.code = code
|
||||
b.wroteHeader = true
|
||||
b.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *basicWriter) Write(buf []byte) (int, error) {
|
||||
b.WriteHeader(http.StatusOK)
|
||||
n, err := b.ResponseWriter.Write(buf)
|
||||
if b.tee != nil {
|
||||
_, err2 := b.tee.Write(buf[:n])
|
||||
// Prefer errors generated by the proxied writer.
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
}
|
||||
b.bytes += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *basicWriter) maybeWriteHeader() {
|
||||
if !b.wroteHeader {
|
||||
b.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *basicWriter) Status() int {
|
||||
return b.code
|
||||
}
|
||||
|
||||
func (b *basicWriter) BytesWritten() int {
|
||||
return b.bytes
|
||||
}
|
||||
|
||||
func (b *basicWriter) Tee(w io.Writer) {
|
||||
b.tee = w
|
||||
}
|
||||
|
||||
func (b *basicWriter) Unwrap() http.ResponseWriter {
|
||||
return b.ResponseWriter
|
||||
}
|
||||
|
||||
// fancyWriter is a writer that additionally satisfies http.CloseNotifier,
|
||||
// http.Flusher, http.Hijacker, and io.ReaderFrom. It exists for the common case
|
||||
// of wrapping the http.ResponseWriter that package http gives you, in order to
|
||||
// make the proxied object support the full method set of the proxied object.
|
||||
type fancyWriter struct {
|
||||
basicWriter
|
||||
}
|
||||
|
||||
func (f *fancyWriter) CloseNotify() <-chan bool {
|
||||
cn := f.basicWriter.ResponseWriter.(http.CloseNotifier)
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
|
||||
func (f *fancyWriter) Flush() {
|
||||
fl := f.basicWriter.ResponseWriter.(http.Flusher)
|
||||
fl.Flush()
|
||||
}
|
||||
|
||||
func (f *fancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj := f.basicWriter.ResponseWriter.(http.Hijacker)
|
||||
return hj.Hijack()
|
||||
}
|
||||
|
||||
func (f *fancyWriter) ReadFrom(r io.Reader) (int64, error) {
|
||||
if f.basicWriter.tee != nil {
|
||||
n, err := io.Copy(&f.basicWriter, r)
|
||||
f.bytes += int(n)
|
||||
return n, err
|
||||
}
|
||||
rf := f.basicWriter.ResponseWriter.(io.ReaderFrom)
|
||||
f.basicWriter.maybeWriteHeader()
|
||||
|
||||
n, err := rf.ReadFrom(r)
|
||||
f.bytes += int(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
type flushWriter struct {
|
||||
basicWriter
|
||||
}
|
||||
|
||||
func (f *flushWriter) Flush() {
|
||||
fl := f.basicWriter.ResponseWriter.(http.Flusher)
|
||||
fl.Flush()
|
||||
}
|
||||
|
||||
var (
|
||||
_ http.CloseNotifier = &fancyWriter{}
|
||||
_ http.Flusher = &fancyWriter{}
|
||||
_ http.Hijacker = &fancyWriter{}
|
||||
_ io.ReaderFrom = &fancyWriter{}
|
||||
_ http.Flusher = &flushWriter{}
|
||||
)
|
||||
@@ -17,12 +17,16 @@ func (h HookFunc) Run(e *Event, level Level, message string) {
|
||||
|
||||
// LevelHook applies a different hook for each level.
|
||||
type LevelHook struct {
|
||||
NoLevelHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook
|
||||
NoLevelHook, TraceHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook
|
||||
}
|
||||
|
||||
// Run implements the Hook interface.
|
||||
func (h LevelHook) Run(e *Event, level Level, message string) {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
if h.TraceHook != nil {
|
||||
h.TraceHook.Run(e, level, message)
|
||||
}
|
||||
case DebugLevel:
|
||||
if h.DebugHook != nil {
|
||||
h.DebugHook.Run(e, level, message)
|
||||
|
||||
+133
-2
@@ -2,10 +2,15 @@ package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type contextKeyType int
|
||||
|
||||
var contextKey contextKeyType
|
||||
|
||||
var (
|
||||
levelNameHook = HookFunc(func(e *Event, level Level, msg string) {
|
||||
levelName := level.String()
|
||||
@@ -31,6 +36,12 @@ var (
|
||||
discardHook = HookFunc(func(e *Event, level Level, message string) {
|
||||
e.Discard()
|
||||
})
|
||||
contextHook = HookFunc(func(e *Event, level Level, message string) {
|
||||
contextData, ok := e.GetCtx().Value(contextKey).(string)
|
||||
if ok {
|
||||
e.Str("context-data", contextData)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
func TestHook(t *testing.T) {
|
||||
@@ -39,6 +50,10 @@ func TestHook(t *testing.T) {
|
||||
want string
|
||||
test func(log Logger)
|
||||
}{
|
||||
{"Message", `{"message":"test message"}` + "\n", func(log Logger) {
|
||||
log = log.Hook()
|
||||
log.Log().Msg("test message")
|
||||
}},
|
||||
{"Message", `{"level_name":"nolevel","message":"test message"}` + "\n", func(log Logger) {
|
||||
log = log.Hook(levelNameHook)
|
||||
log.Log().Msg("test message")
|
||||
@@ -120,6 +135,29 @@ func TestHook(t *testing.T) {
|
||||
log = log.Hook(discardHook)
|
||||
log.Log().Msg("test message")
|
||||
}},
|
||||
{"Context/Background", `{"level":"info","message":"test message"}` + "\n", func(log Logger) {
|
||||
log = log.Hook(contextHook)
|
||||
log.Info().Ctx(context.Background()).Msg("test message")
|
||||
}},
|
||||
{"Context/nil", `{"level":"info","message":"test message"}` + "\n", func(log Logger) {
|
||||
// passing `nil` where a context is wanted is against
|
||||
// the rules, but people still do it.
|
||||
log = log.Hook(contextHook)
|
||||
log.Info().Ctx(nil).Msg("test message") // nolint
|
||||
}},
|
||||
{"Context/valid", `{"level":"info","context-data":"12345abcdef","message":"test message"}` + "\n", func(log Logger) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, contextKey, "12345abcdef")
|
||||
log = log.Hook(contextHook)
|
||||
log.Info().Ctx(ctx).Msg("test message")
|
||||
}},
|
||||
{"Context/With/valid", `{"level":"info","context-data":"12345abcdef","message":"test message"}` + "\n", func(log Logger) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, contextKey, "12345abcdef")
|
||||
log = log.Hook(contextHook)
|
||||
log = log.With().Ctx(ctx).Logger()
|
||||
log.Info().Msg("test message")
|
||||
}},
|
||||
{"None", `{"level":"error"}` + "\n", func(log Logger) {
|
||||
log.Error().Msg("")
|
||||
}},
|
||||
@@ -137,8 +175,101 @@ func TestHook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelHook(t *testing.T) {
|
||||
var called []string
|
||||
|
||||
traceHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "trace")
|
||||
})
|
||||
debugHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "debug")
|
||||
})
|
||||
infoHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "info")
|
||||
})
|
||||
warnHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "warn")
|
||||
})
|
||||
errorHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "error")
|
||||
})
|
||||
fatalHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "fatal")
|
||||
})
|
||||
panicHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "panic")
|
||||
})
|
||||
noLevelHook := HookFunc(func(e *Event, level Level, msg string) {
|
||||
called = append(called, "nolevel")
|
||||
})
|
||||
|
||||
hook := LevelHook{
|
||||
TraceHook: traceHook,
|
||||
DebugHook: debugHook,
|
||||
InfoHook: infoHook,
|
||||
WarnHook: warnHook,
|
||||
ErrorHook: errorHook,
|
||||
FatalHook: fatalHook,
|
||||
PanicHook: panicHook,
|
||||
NoLevelHook: noLevelHook,
|
||||
}
|
||||
|
||||
e := &Event{}
|
||||
|
||||
// Test each level
|
||||
hook.Run(e, TraceLevel, "")
|
||||
if len(called) != 1 || called[0] != "trace" {
|
||||
t.Errorf("TraceLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
called = nil
|
||||
hook.Run(e, DebugLevel, "")
|
||||
if len(called) != 1 || called[0] != "debug" {
|
||||
t.Errorf("DebugLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
called = nil
|
||||
hook.Run(e, InfoLevel, "")
|
||||
if len(called) != 1 || called[0] != "info" {
|
||||
t.Errorf("InfoLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
called = nil
|
||||
hook.Run(e, WarnLevel, "")
|
||||
if len(called) != 1 || called[0] != "warn" {
|
||||
t.Errorf("WarnLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
called = nil
|
||||
hook.Run(e, ErrorLevel, "")
|
||||
if len(called) != 1 || called[0] != "error" {
|
||||
t.Errorf("ErrorLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
called = nil
|
||||
hook.Run(e, FatalLevel, "")
|
||||
if len(called) != 1 || called[0] != "fatal" {
|
||||
t.Errorf("FatalLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
called = nil
|
||||
hook.Run(e, PanicLevel, "")
|
||||
if len(called) != 1 || called[0] != "panic" {
|
||||
t.Errorf("PanicLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
called = nil
|
||||
hook.Run(e, NoLevel, "")
|
||||
if len(called) != 1 || called[0] != "nolevel" {
|
||||
t.Errorf("NoLevel hook not called correctly: %v", called)
|
||||
}
|
||||
|
||||
// Test NewLevelHook
|
||||
_ = NewLevelHook()
|
||||
}
|
||||
|
||||
func BenchmarkHooks(b *testing.B) {
|
||||
logger := New(ioutil.Discard)
|
||||
logger := New(io.Discard)
|
||||
b.ResetTimer()
|
||||
b.Run("Nop/Single", func(b *testing.B) {
|
||||
log := logger.Hook(nopHook)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package cbor
|
||||
|
||||
// JSONMarshalFunc is used to marshal interface to JSON encoded byte slice.
|
||||
// Making it package level instead of embedded in Encoder brings
|
||||
// some extra efforts at importing, but avoids value copy when the functions
|
||||
// of Encoder being invoked.
|
||||
// DO REMEMBER to set this variable at importing, or
|
||||
// you might get a nil pointer dereference panic at runtime.
|
||||
var JSONMarshalFunc func(v interface{}) ([]byte, error)
|
||||
|
||||
type Encoder struct{}
|
||||
|
||||
// AppendKey adds a key (string) to the binary encoded log message
|
||||
@@ -8,4 +16,4 @@ func (e Encoder) AppendKey(dst []byte, key string) []byte {
|
||||
dst = e.AppendBeginMarker(dst)
|
||||
}
|
||||
return e.AppendString(dst, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package cbor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendKey(t *testing.T) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, 0xbf) // start string
|
||||
want = append(want, 0x63) // length 3
|
||||
want = append(want, []byte("key")...)
|
||||
|
||||
got := enc.AppendKey([]byte{}, "key")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendKey(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
"key",
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -26,7 +26,8 @@ const (
|
||||
additionalTypeBreak byte = 31
|
||||
|
||||
// Tag Sub-types.
|
||||
additionalTypeTimestamp byte = 01
|
||||
additionalTypeTimestamp byte = 01
|
||||
additionalTypeEmbeddedCBOR byte = 63
|
||||
|
||||
// Extended Tags - from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml
|
||||
additionalTypeTagNetworkAddr uint16 = 260
|
||||
@@ -54,12 +55,12 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
float32Nan = "\xfa\x7f\xc0\x00\x00"
|
||||
float32PosInfinity = "\xfa\x7f\x80\x00\x00"
|
||||
float32NegInfinity = "\xfa\xff\x80\x00\x00"
|
||||
float64Nan = "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00"
|
||||
float64PosInfinity = "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00"
|
||||
float64NegInfinity = "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00"
|
||||
float32Nan = "\x7f\xc0\x00\x00"
|
||||
float32PosInfinity = "\x7f\x80\x00\x00"
|
||||
float32NegInfinity = "\xff\x80\x00\x00"
|
||||
float64Nan = "\x7f\xf8\x00\x00\x00\x00\x00\x00"
|
||||
float64PosInfinity = "\x7f\xf0\x00\x00\x00\x00\x00\x00"
|
||||
float64NegInfinity = "\xff\xf0\x00\x00\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// IntegerTimeFieldFormat indicates the format of timestamp decoded
|
||||
@@ -67,11 +68,11 @@ const (
|
||||
var IntegerTimeFieldFormat = time.RFC3339
|
||||
|
||||
// NanoTimeFieldFormat indicates the format of timestamp decoded
|
||||
// from a float value (time in seconds and nano seconds).
|
||||
// from a float value (time in seconds and nanoseconds).
|
||||
var NanoTimeFieldFormat = time.RFC3339Nano
|
||||
|
||||
func appendCborTypePrefix(dst []byte, major byte, number uint64) []byte {
|
||||
byteCount := 8
|
||||
var byteCount int
|
||||
var minor byte
|
||||
switch {
|
||||
case number < 256:
|
||||
@@ -91,7 +92,8 @@ func appendCborTypePrefix(dst []byte, major byte, number uint64) []byte {
|
||||
minor = additionalTypeIntUint64
|
||||
|
||||
}
|
||||
dst = append(dst, byte(major|minor))
|
||||
|
||||
dst = append(dst, major|minor)
|
||||
byteCount--
|
||||
for ; byteCount >= 0; byteCount-- {
|
||||
dst = append(dst, byte(number>>(uint(byteCount)*8)))
|
||||
|
||||
@@ -5,6 +5,7 @@ package cbor
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
@@ -43,7 +44,7 @@ func readByte(src *bufio.Reader) byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func decodeIntAdditonalType(src *bufio.Reader, minor byte) int64 {
|
||||
func decodeIntAdditionalType(src *bufio.Reader, minor byte) int64 {
|
||||
val := int64(0)
|
||||
if minor <= 23 {
|
||||
val = int64(minor)
|
||||
@@ -77,7 +78,7 @@ func decodeInteger(src *bufio.Reader) int64 {
|
||||
if major != majorTypeUnsignedInt && major != majorTypeNegativeInt {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeInteger!! (expected 0 or 1)", major))
|
||||
}
|
||||
val := decodeIntAdditonalType(src, minor)
|
||||
val := decodeIntAdditionalType(src, minor)
|
||||
if major == 0 {
|
||||
return val
|
||||
}
|
||||
@@ -94,7 +95,7 @@ func decodeFloat(src *bufio.Reader) (float64, int) {
|
||||
|
||||
switch minor {
|
||||
case additionalTypeFloat16:
|
||||
panic(fmt.Errorf("float16 is not suppported in decodeFloat"))
|
||||
panic(fmt.Errorf("float16 is not supported in decodeFloat"))
|
||||
|
||||
case additionalTypeFloat32:
|
||||
pb := readNBytes(src, 4)
|
||||
@@ -204,7 +205,7 @@ func decodeString(src *bufio.Reader, noQuotes bool) []byte {
|
||||
if !noQuotes {
|
||||
result = append(result, '"')
|
||||
}
|
||||
length := decodeIntAdditonalType(src, minor)
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len := int(length)
|
||||
pbs := readNBytes(src, len)
|
||||
result = append(result, pbs...)
|
||||
@@ -213,6 +214,31 @@ func decodeString(src *bufio.Reader, noQuotes bool) []byte {
|
||||
}
|
||||
return append(result, '"')
|
||||
}
|
||||
func decodeStringToDataUrl(src *bufio.Reader, mimeType string) []byte {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeByteString {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeString", major))
|
||||
}
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
l := int(length)
|
||||
enc := base64.StdEncoding
|
||||
lEnc := enc.EncodedLen(l)
|
||||
result := make([]byte, len("\"data:;base64,\"")+len(mimeType)+lEnc)
|
||||
dest := result
|
||||
u := copy(dest, "\"data:")
|
||||
dest = dest[u:]
|
||||
u = copy(dest, mimeType)
|
||||
dest = dest[u:]
|
||||
u = copy(dest, ";base64,")
|
||||
dest = dest[u:]
|
||||
pbs := readNBytes(src, l)
|
||||
enc.Encode(dest, pbs)
|
||||
dest = dest[lEnc:]
|
||||
dest[0] = '"'
|
||||
return result
|
||||
}
|
||||
|
||||
func decodeUTF8String(src *bufio.Reader) []byte {
|
||||
pb := readByte(src)
|
||||
@@ -222,7 +248,7 @@ func decodeUTF8String(src *bufio.Reader) []byte {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeUTF8String", major))
|
||||
}
|
||||
result := []byte{'"'}
|
||||
length := decodeIntAdditonalType(src, minor)
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len := int(length)
|
||||
pbs := readNBytes(src, len)
|
||||
|
||||
@@ -238,7 +264,7 @@ func decodeUTF8String(src *bufio.Reader) []byte {
|
||||
return append(dst, '"')
|
||||
}
|
||||
}
|
||||
// The string has no need for encoding an therefore is directly
|
||||
// The string has no need for encoding and therefore is directly
|
||||
// appended to the byte slice.
|
||||
result = append(result, pbs...)
|
||||
return append(result, '"')
|
||||
@@ -257,7 +283,7 @@ func array2Json(src *bufio.Reader, dst io.Writer) {
|
||||
if minor == additionalTypeInfiniteCount {
|
||||
unSpecifiedCount = true
|
||||
} else {
|
||||
length := decodeIntAdditonalType(src, minor)
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len = int(length)
|
||||
}
|
||||
for i := 0; unSpecifiedCount || i < len; i++ {
|
||||
@@ -266,7 +292,7 @@ func array2Json(src *bufio.Reader, dst io.Writer) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == byte(majorTypeSimpleAndFloat|additionalTypeBreak) {
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
@@ -277,7 +303,7 @@ func array2Json(src *bufio.Reader, dst io.Writer) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == byte(majorTypeSimpleAndFloat|additionalTypeBreak) {
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
@@ -301,7 +327,7 @@ func map2Json(src *bufio.Reader, dst io.Writer) {
|
||||
if minor == additionalTypeInfiniteCount {
|
||||
unSpecifiedCount = true
|
||||
} else {
|
||||
length := decodeIntAdditonalType(src, minor)
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len = int(length)
|
||||
}
|
||||
dst.Write([]byte{'{'})
|
||||
@@ -311,7 +337,7 @@ func map2Json(src *bufio.Reader, dst io.Writer) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == byte(majorTypeSimpleAndFloat|additionalTypeBreak) {
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
@@ -326,7 +352,7 @@ func map2Json(src *bufio.Reader, dst io.Writer) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == byte(majorTypeSimpleAndFloat|additionalTypeBreak) {
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
@@ -349,10 +375,24 @@ func decodeTagData(src *bufio.Reader) []byte {
|
||||
switch minor {
|
||||
case additionalTypeTimestamp:
|
||||
return decodeTimeStamp(src)
|
||||
case additionalTypeIntUint8:
|
||||
val := decodeIntAdditionalType(src, minor)
|
||||
switch byte(val) {
|
||||
case additionalTypeEmbeddedCBOR:
|
||||
pb := readByte(src)
|
||||
dataMajor := pb & maskOutAdditionalType
|
||||
if dataMajor != majorTypeByteString {
|
||||
panic(fmt.Errorf("Unsupported embedded Type: %d in decodeEmbeddedCBOR", dataMajor))
|
||||
}
|
||||
src.UnreadByte()
|
||||
return decodeStringToDataUrl(src, "application/cbor")
|
||||
default:
|
||||
panic(fmt.Errorf("Unsupported Additional Tag Type: %d in decodeTagData", val))
|
||||
}
|
||||
|
||||
// Tag value is larger than 256 (so uint16).
|
||||
case additionalTypeIntUint16:
|
||||
val := decodeIntAdditonalType(src, minor)
|
||||
val := decodeIntAdditionalType(src, minor)
|
||||
|
||||
switch uint16(val) {
|
||||
case additionalTypeEmbeddedJSON:
|
||||
@@ -383,7 +423,7 @@ func decodeTagData(src *bufio.Reader) []byte {
|
||||
|
||||
case additionalTypeTagNetworkPrefix:
|
||||
pb := readByte(src)
|
||||
if pb != byte(majorTypeMap|0x1) {
|
||||
if pb != majorTypeMap|0x1 {
|
||||
panic(fmt.Errorf("IP Prefix is NOT of MAP of 1 elements as expected"))
|
||||
}
|
||||
octets := decodeString(src, true)
|
||||
@@ -450,7 +490,7 @@ func decodeTimeStamp(src *bufio.Reader) []byte {
|
||||
tsb = append(tsb, '"')
|
||||
return tsb
|
||||
}
|
||||
panic(fmt.Errorf("TS format is neigther int nor float: %d", tsMajor))
|
||||
panic(fmt.Errorf("TS format is neither int nor float: %d", tsMajor))
|
||||
}
|
||||
|
||||
func decodeSimpleFloat(src *bufio.Reader) []byte {
|
||||
|
||||
+341
-57
@@ -3,16 +3,19 @@ package cbor
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
func TestDecodeInteger(t *testing.T) {
|
||||
for _, tc := range integerTestCases {
|
||||
gotv := decodeInteger(getReader(tc.binary))
|
||||
if gotv != int64(tc.val) {
|
||||
for _, tc := range internal.IntegerTestCases {
|
||||
gotv := decodeInteger(getReader(tc.Binary))
|
||||
if gotv != int64(tc.Val) {
|
||||
t.Errorf("decodeInteger(0x%s)=0x%d, want: 0x%d",
|
||||
hex.EncodeToString([]byte(tc.binary)), gotv, tc.val)
|
||||
hex.EncodeToString([]byte(tc.Binary)), gotv, tc.Val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,11 +31,11 @@ func TestDecodeString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDecodeArray(t *testing.T) {
|
||||
for _, tc := range integerArrayTestCases {
|
||||
for _, tc := range internal.IntegerArrayTestCases {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
array2Json(getReader(tc.binary), buf)
|
||||
if buf.String() != tc.json {
|
||||
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.binary)), buf.String(), tc.json)
|
||||
array2Json(getReader(tc.Binary), buf)
|
||||
if buf.String() != tc.Json {
|
||||
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.Binary)), buf.String(), tc.Json)
|
||||
}
|
||||
}
|
||||
//Unspecified Length Array
|
||||
@@ -53,136 +56,199 @@ func TestDecodeArray(t *testing.T) {
|
||||
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.out)), buf.String(), tc.out)
|
||||
}
|
||||
}
|
||||
for _, tc := range booleanArrayTestCases {
|
||||
for _, tc := range internal.BooleanArrayTestCases {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
array2Json(getReader(tc.binary), buf)
|
||||
if buf.String() != tc.json {
|
||||
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.binary)), buf.String(), tc.json)
|
||||
array2Json(getReader(tc.Binary), buf)
|
||||
if buf.String() != tc.Json {
|
||||
t.Errorf("array2Json(0x%s)=%s, want: %s", hex.EncodeToString([]byte(tc.Binary)), buf.String(), tc.Json)
|
||||
}
|
||||
}
|
||||
//TODO add cases for arrays of other types
|
||||
}
|
||||
|
||||
var infiniteMapDecodeTestCases = []struct {
|
||||
bin []byte
|
||||
json string
|
||||
Bin []byte
|
||||
Json string
|
||||
}{
|
||||
{[]byte("\xbf\x64IETF\x20\xff"), "{\"IETF\":-1}"},
|
||||
{[]byte("\xbf\x65Array\x84\x20\x00\x18\xc8\x14\xff"), "{\"Array\":[-1,0,200,20]}"},
|
||||
}
|
||||
|
||||
var mapDecodeTestCases = []struct {
|
||||
bin []byte
|
||||
json string
|
||||
Bin []byte
|
||||
Json string
|
||||
}{
|
||||
{[]byte("\xa2\x64IETF\x20"), "{\"IETF\":-1}"},
|
||||
{[]byte("\xa2\x65Array\x84\x20\x00\x18\xc8\x14"), "{\"Array\":[-1,0,200,20]}"},
|
||||
{[]byte("\xa6\x61\x61\x01\x61\x62\x02\x61\x63\x03"), "{\"a\":1,\"b\":2,\"c\":3}"},
|
||||
{[]byte("\xbf\x61a\x01\x61b\x02\xff"), "{\"a\":1,\"b\":2}"},
|
||||
}
|
||||
|
||||
func TestDecodeMap(t *testing.T) {
|
||||
for _, tc := range mapDecodeTestCases {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
map2Json(getReader(string(tc.bin)), buf)
|
||||
if buf.String() != tc.json {
|
||||
t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.bin), buf.String(), tc.json)
|
||||
map2Json(getReader(string(tc.Bin)), buf)
|
||||
if buf.String() != tc.Json {
|
||||
t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.Bin), buf.String(), tc.Json)
|
||||
}
|
||||
}
|
||||
for _, tc := range infiniteMapDecodeTestCases {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
map2Json(getReader(string(tc.bin)), buf)
|
||||
if buf.String() != tc.json {
|
||||
t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.bin), buf.String(), tc.json)
|
||||
map2Json(getReader(string(tc.Bin)), buf)
|
||||
if buf.String() != tc.Json {
|
||||
t.Errorf("map2Json(0x%s)=%s, want: %s", hex.EncodeToString(tc.Bin), buf.String(), tc.Json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBool(t *testing.T) {
|
||||
for _, tc := range booleanTestCases {
|
||||
got := decodeSimpleFloat(getReader(tc.binary))
|
||||
if string(got) != tc.json {
|
||||
t.Errorf("decodeSimpleFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), string(got), tc.json)
|
||||
for _, tc := range internal.BooleanTestCases {
|
||||
got := decodeSimpleFloat(getReader(tc.Binary))
|
||||
if string(got) != tc.Json {
|
||||
t.Errorf("decodeSimpleFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), string(got), tc.Json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeFloat(t *testing.T) {
|
||||
for _, tc := range float32TestCases {
|
||||
got, _ := decodeFloat(getReader(tc.binary))
|
||||
if got != float64(tc.val) {
|
||||
t.Errorf("decodeFloat(0x%s)=%f, want:%f", hex.EncodeToString([]byte(tc.binary)), got, tc.val)
|
||||
for _, tc := range internal.Float32TestCases {
|
||||
got, _ := decodeFloat(getReader(tc.Binary))
|
||||
if got != float64(tc.Val) && math.IsNaN(got) != math.IsNaN(float64(tc.Val)) {
|
||||
t.Errorf("decodeFloat(0x%s)=%f, want:%f", hex.EncodeToString([]byte(tc.Binary)), got, tc.Val)
|
||||
}
|
||||
}
|
||||
for _, tc := range internal.Float64TestCases {
|
||||
got, _ := decodeFloat(getReader(tc.Binary))
|
||||
if got != tc.Val && math.IsNaN(got) != math.IsNaN(tc.Val) {
|
||||
t.Errorf("decodeFloat(0x%s)=%f, want:%f", hex.EncodeToString([]byte(tc.Binary)), got, tc.Val)
|
||||
}
|
||||
}
|
||||
|
||||
// Test float64 special values with correct CBOR encoding
|
||||
float64Tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want float64
|
||||
}{
|
||||
{"float64 NaN", "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00", math.NaN()},
|
||||
{"float64 +Inf", "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00", math.Inf(0)},
|
||||
{"float64 -Inf", "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00", math.Inf(-1)},
|
||||
{"float64 1.0", "\xfb\x3f\xf0\x00\x00\x00\x00\x00\x00", 1.0},
|
||||
}
|
||||
|
||||
for _, tt := range float64Tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, _ := decodeFloat(getReader(tt.input))
|
||||
if math.IsNaN(tt.want) {
|
||||
if !math.IsNaN(got) {
|
||||
t.Errorf("decodeFloat(%q) = %f, want NaN", tt.input, got)
|
||||
}
|
||||
} else if math.IsInf(tt.want, 0) {
|
||||
if !math.IsInf(got, 0) {
|
||||
t.Errorf("decodeFloat(%q) = %f, want +Inf", tt.input, got)
|
||||
}
|
||||
} else if math.IsInf(tt.want, -1) {
|
||||
if !math.IsInf(got, -1) {
|
||||
t.Errorf("decodeFloat(%q) = %f, want -Inf", tt.input, got)
|
||||
}
|
||||
} else if got != tt.want {
|
||||
t.Errorf("decodeFloat(%q) = %f, want %f", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTimestamp(t *testing.T) {
|
||||
decodeTimeZone, _ = time.LoadLocation("UTC")
|
||||
for _, tc := range timeIntegerTestcases {
|
||||
tm := decodeTagData(getReader(tc.binary))
|
||||
if string(tm) != "\""+tc.rfcStr+"\"" {
|
||||
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), tm, tc.rfcStr)
|
||||
for _, tc := range internal.TimeIntegerTestcases {
|
||||
tm := decodeTagData(getReader(tc.Binary))
|
||||
if string(tm) != "\""+tc.RfcStr+"\"" {
|
||||
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), tm, tc.RfcStr)
|
||||
}
|
||||
}
|
||||
for _, tc := range timeFloatTestcases {
|
||||
tm := decodeTagData(getReader(tc.out))
|
||||
for _, tc := range internal.TimeFloatTestcases {
|
||||
tm := decodeTagData(getReader(tc.Out))
|
||||
//Since we convert to float and back - it may be slightly off - so
|
||||
//we cannot check for exact equality instead, we'll check it is
|
||||
//very close to each other Less than a Microsecond (lets not yet do nanosec)
|
||||
|
||||
got, _ := time.Parse(string(tm), string(tm))
|
||||
want, _ := time.Parse(tc.rfcStr, tc.rfcStr)
|
||||
want, _ := time.Parse(tc.RfcStr, tc.RfcStr)
|
||||
if got.Sub(want) > time.Microsecond {
|
||||
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.out)), tm, tc.rfcStr)
|
||||
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Out)), tm, tc.RfcStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Test with decodeTimeZone = nil to cover the else branches
|
||||
oldTimeZone := decodeTimeZone
|
||||
decodeTimeZone = nil
|
||||
defer func() { decodeTimeZone = oldTimeZone }()
|
||||
|
||||
for _, tc := range internal.TimeIntegerTestcases {
|
||||
tm := decodeTagData(getReader(tc.Binary))
|
||||
if string(tm) != "\""+tc.RfcStr+"\"" {
|
||||
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), tm, tc.RfcStr)
|
||||
}
|
||||
}
|
||||
for _, tc := range internal.TimeFloatTestcases {
|
||||
tm := decodeTagData(getReader(tc.Out))
|
||||
got, _ := time.Parse(string(tm), string(tm))
|
||||
want, _ := time.Parse(tc.RfcStr, tc.RfcStr)
|
||||
if got.Sub(want) > time.Microsecond {
|
||||
t.Errorf("decodeFloat(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Out)), tm, tc.RfcStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeNetworkAddr(t *testing.T) {
|
||||
for _, tc := range ipAddrTestCases {
|
||||
d1 := decodeTagData(getReader(tc.binary))
|
||||
if string(d1) != tc.text {
|
||||
t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), d1, tc.text)
|
||||
for _, tc := range internal.IpAddrTestCases {
|
||||
d1 := decodeTagData(getReader(tc.Binary))
|
||||
if string(d1) != tc.Text {
|
||||
t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), d1, tc.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeMACAddr(t *testing.T) {
|
||||
for _, tc := range macAddrTestCases {
|
||||
d1 := decodeTagData(getReader(tc.binary))
|
||||
if string(d1) != tc.text {
|
||||
t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), d1, tc.text)
|
||||
for _, tc := range internal.MacAddrTestCases {
|
||||
d1 := decodeTagData(getReader(tc.Binary))
|
||||
if string(d1) != tc.Text {
|
||||
t.Errorf("decodeNetworkAddr(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), d1, tc.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeIPPrefix(t *testing.T) {
|
||||
for _, tc := range IPPrefixTestCases {
|
||||
d1 := decodeTagData(getReader(tc.binary))
|
||||
if string(d1) != tc.text {
|
||||
t.Errorf("decodeIPPrefix(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.binary)), d1, tc.text)
|
||||
for _, tc := range internal.IPPrefixTestCases {
|
||||
d1 := decodeTagData(getReader(tc.Binary))
|
||||
if string(d1) != tc.Text {
|
||||
t.Errorf("decodeIPPrefix(0x%s)=%s, want:%s", hex.EncodeToString([]byte(tc.Binary)), d1, tc.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var compositeCborTestCases = []struct {
|
||||
binary []byte
|
||||
json string
|
||||
Binary []byte
|
||||
Json string
|
||||
}{
|
||||
{[]byte("\xbf\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14\xff\xff"), "{\"IETF\":-1,\"Array\":[-1,0,200,20]}\n"},
|
||||
{[]byte("\xbf\x64IETF\x64YES!\x65Array\x9f\x20\x00\x18\xc8\x14\xff\xff"), "{\"IETF\":\"YES!\",\"Array\":[-1,0,200,20]}\n"},
|
||||
{[]byte("\xbf\x61a\x01\x61b\x02\x61c\x03\xff"), "{\"a\":1,\"b\":2,\"c\":3}\n"},
|
||||
{[]byte("\xc1\x1a\x51\x0f\x30\xd8"), "\"2013-02-04T03:54:00Z\"\n"},
|
||||
}
|
||||
|
||||
func TestDecodeCbor2Json(t *testing.T) {
|
||||
for _, tc := range compositeCborTestCases {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := Cbor2JsonManyObjects(getReader(string(tc.binary)), buf)
|
||||
if buf.String() != tc.json || err != nil {
|
||||
t.Errorf("cbor2JsonManyObjects(0x%s)=%s, want: %s, err:%s", hex.EncodeToString(tc.binary), buf.String(), tc.json, err.Error())
|
||||
err := Cbor2JsonManyObjects(getReader(string(tc.Binary)), buf)
|
||||
if buf.String() != tc.Json || err != nil {
|
||||
t.Errorf("cbor2JsonManyObjects(0x%s)=%s, want: %s, err:%s", hex.EncodeToString(tc.Binary), buf.String(), tc.Json, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var negativeCborTestCases = []struct {
|
||||
binary []byte
|
||||
Binary []byte
|
||||
errStr string
|
||||
}{
|
||||
{[]byte("\xb9\x64IETF\x20\x65Array\x9f\x20\x00\x18\xc8\x14"), "Tried to Read 18 Bytes.. But hit end of file"},
|
||||
@@ -197,9 +263,227 @@ var negativeCborTestCases = []struct {
|
||||
func TestDecodeNegativeCbor2Json(t *testing.T) {
|
||||
for _, tc := range negativeCborTestCases {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := Cbor2JsonManyObjects(getReader(string(tc.binary)), buf)
|
||||
err := Cbor2JsonManyObjects(getReader(string(tc.Binary)), buf)
|
||||
if err == nil || err.Error() != tc.errStr {
|
||||
t.Errorf("Expected error got:%s, want:%s", err, tc.errStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBinaryFmt(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []byte
|
||||
want bool
|
||||
}{
|
||||
{[]byte{}, false},
|
||||
{[]byte{0x00}, false},
|
||||
{[]byte{0x7F}, false},
|
||||
{[]byte{0x80}, true},
|
||||
{[]byte{0xFF}, true},
|
||||
{[]byte{0x00, 0x80}, false}, // Only checks first byte
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := binaryFmt(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("binaryFmt(%v) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeIfBinaryToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "non-binary input",
|
||||
input: []byte(`{"key":"value"}`),
|
||||
want: `{"key":"value"}`,
|
||||
},
|
||||
{
|
||||
name: "binary input - simple object",
|
||||
input: []byte("\xbf\x64IETF\x20\xff"), // {"IETF": -1} in indefinite length CBOR
|
||||
want: "{\"IETF\":-1}\n",
|
||||
},
|
||||
{
|
||||
name: "binary input - multiple objects",
|
||||
input: []byte("\xbf\x64IETF\x20\xff\xbf\x65Array\x84\x20\x00\x18\xc8\x14\xff"), // Two objects
|
||||
want: "{\"IETF\":-1}\n{\"Array\":[-1,0,200,20]}\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := DecodeIfBinaryToString(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("DecodeIfBinaryToString() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeObjectToStr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "non-binary input",
|
||||
input: []byte(`{"key":"value"}`),
|
||||
want: `{"key":"value"}`,
|
||||
},
|
||||
{
|
||||
name: "binary input - simple object",
|
||||
input: []byte("\xbf\x64IETF\x20\xff"), // {"IETF": -1} in indefinite length CBOR
|
||||
want: "{\"IETF\":-1}",
|
||||
},
|
||||
{
|
||||
name: "binary input - array",
|
||||
input: []byte("\x84\x20\x00\x18\xc8\x14"), // [-1, 0, 200, 20]
|
||||
want: "[-1,0,200,20]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := DecodeObjectToStr(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("DecodeObjectToStr() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeIfBinaryToBytes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "non-binary input",
|
||||
input: []byte(`{"key":"value"}`),
|
||||
want: []byte(`{"key":"value"}`),
|
||||
},
|
||||
{
|
||||
name: "binary input - simple object",
|
||||
input: []byte("\xbf\x64IETF\x20\xff"), // {"IETF": -1} in indefinite length CBOR
|
||||
want: []byte("{\"IETF\":-1}\n"),
|
||||
},
|
||||
{
|
||||
name: "binary input - multiple objects",
|
||||
input: []byte("\xbf\x64IETF\x20\xff\xbf\x65Array\x84\x20\x00\x18\xc8\x14\xff"), // Two objects
|
||||
want: []byte("{\"IETF\":-1}\n{\"Array\":[-1,0,200,20]}\n"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := DecodeIfBinaryToBytes(tt.input)
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("DecodeIfBinaryToBytes() = %q, want %q", string(got), string(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeEmbeddedCBOR(t *testing.T) {
|
||||
// Test embedded CBOR tag: 0xD8 0x3F (tag 63) followed by byte string
|
||||
// 0xD8 = major type 6 (tags) + additional type 24 (uint8 follows)
|
||||
// 0x3F = 63 (additionalTypeEmbeddedCBOR)
|
||||
// 0x43 = major type 2 (byte string) + length 3
|
||||
// 0x01 0x02 0x03 = the embedded CBOR data
|
||||
|
||||
embeddedCBOR := []byte("\xd8\x3f\x43\x01\x02\x03")
|
||||
expected := "\"data:application/cbor;base64,AQID\""
|
||||
|
||||
got := decodeTagData(getReader(string(embeddedCBOR)))
|
||||
if string(got) != expected {
|
||||
t.Errorf("decodeTagData(embedded CBOR) = %q, want %q", string(got), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeEmbeddedJSON(t *testing.T) {
|
||||
t.Run("valid embedded JSON", func(t *testing.T) {
|
||||
// Test embedded JSON tag: 0xD9 0x01 0x06 (tag 262) followed by byte string.
|
||||
// 0xD9 = major type 6 (tags) + additional type 25 (uint16 follows)
|
||||
// 0x01 0x06 = 262 (additionalTypeEmbeddedJSON)
|
||||
// 0x47 = major type 2 (byte string) + length 7
|
||||
// {"a":1} = embedded JSON payload (no surrounding quotes expected)
|
||||
embeddedJSON := []byte("\xd9\x01\x06\x47{\"a\":1}")
|
||||
expected := "{\"a\":1}"
|
||||
|
||||
got := decodeTagData(getReader(string(embeddedJSON)))
|
||||
if string(got) != expected {
|
||||
t.Errorf("decodeTagData(embedded JSON) = %q, want %q", string(got), expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unsupported embedded type panics", func(t *testing.T) {
|
||||
// Same embedded JSON tag, but followed by a UTF-8 string instead of a byte string.
|
||||
// This should hit the "Unsupported embedded Type" panic branch.
|
||||
bad := []byte("\xd9\x01\x06\x61x")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatalf("expected panic, got none")
|
||||
}
|
||||
}()
|
||||
|
||||
_ = decodeTagData(getReader(string(bad)))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDecodeHexString(t *testing.T) {
|
||||
// Test hex string tag: 0xD9 0x01 0x07 (tag 263) followed by byte string
|
||||
// 0xD9 = major type 6 (tags) + additional type 25 (uint16 follows)
|
||||
// 0x01 0x07 = 263 (additionalTypeTagHexString)
|
||||
// 0x43 = major type 2 (byte string) + length 3
|
||||
// 0x01 0x02 0x03 = the byte data to hex encode
|
||||
|
||||
hexString := []byte("\xd9\x01\x07\x43\x01\x02\x03")
|
||||
expected := "\"010203\""
|
||||
|
||||
got := decodeTagData(getReader(string(hexString)))
|
||||
if string(got) != expected {
|
||||
t.Errorf("decodeTagData(hex string) = %q, want %q", string(got), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeSimpleFloat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
// Boolean and null cases (already covered)
|
||||
{"true", "\xf5", "true"},
|
||||
{"false", "\xf4", "false"},
|
||||
{"null", "\xf6", "null"},
|
||||
|
||||
// Float32 cases
|
||||
{"float32 1.0", "\xfa\x3f\x80\x00\x00", "1"},
|
||||
{"float32 1.5", "\xfa\x3f\xc0\x00\x00", "1.5"},
|
||||
{"float32 +Inf", "\xfa\x7f\x80\x00\x00", "\"+Inf\""},
|
||||
{"float32 -Inf", "\xfa\xff\x80\x00\x00", "\"-Inf\""},
|
||||
{"float32 NaN", "\xfa\x7f\xc0\x00\x00", "\"NaN\""},
|
||||
|
||||
// Float64 cases
|
||||
{"float64 1.0", "\xfb\x3f\xf0\x00\x00\x00\x00\x00\x00", "1"},
|
||||
{"float64 +Inf", "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00", "\"+Inf\""},
|
||||
{"float64 -Inf", "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00", "\"-Inf\""},
|
||||
{"float64 NaN", "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00", "\"NaN\""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := decodeSimpleFloat(getReader(tt.input))
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("decodeSimpleFloat(%q) = %q, want %q", tt.input, string(got), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+54
-5
@@ -1,12 +1,14 @@
|
||||
package cbor
|
||||
|
||||
import "fmt"
|
||||
|
||||
// AppendStrings encodes and adds an array of strings to the dst byte array.
|
||||
func (e Encoder) AppendStrings(dst []byte, vals []string) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -23,13 +25,38 @@ func (Encoder) AppendString(dst []byte, s string) []byte {
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, majorTypeUtf8String, uint64(l))
|
||||
}
|
||||
return append(dst, s...)
|
||||
}
|
||||
|
||||
// AppendStringers encodes and adds an array of Stringer values
|
||||
// to the dst byte array.
|
||||
func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte {
|
||||
if vals == nil || len(vals) == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
dst = e.AppendArrayStart(dst)
|
||||
dst = e.AppendStringer(dst, vals[0])
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = e.AppendStringer(dst, val)
|
||||
}
|
||||
}
|
||||
return e.AppendArrayEnd(dst)
|
||||
}
|
||||
|
||||
// AppendStringer encodes and adds the Stringer value to the dst
|
||||
// byte array.
|
||||
func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte {
|
||||
if val == nil {
|
||||
return e.AppendNil(dst)
|
||||
}
|
||||
return e.AppendString(dst, val.String())
|
||||
}
|
||||
|
||||
// AppendBytes encodes and adds an array of bytes to the dst byte array.
|
||||
func (Encoder) AppendBytes(dst, s []byte) []byte {
|
||||
major := majorTypeByteString
|
||||
@@ -37,7 +64,7 @@ func (Encoder) AppendBytes(dst, s []byte) []byte {
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -50,7 +77,7 @@ func AppendEmbeddedJSON(dst, s []byte) []byte {
|
||||
minor := additionalTypeEmbeddedJSON
|
||||
|
||||
// Append the TAG to indicate this is Embedded JSON.
|
||||
dst = append(dst, byte(major|additionalTypeIntUint16))
|
||||
dst = append(dst, major|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(minor>>8))
|
||||
dst = append(dst, byte(minor&0xff))
|
||||
|
||||
@@ -60,7 +87,29 @@ func AppendEmbeddedJSON(dst, s []byte) []byte {
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
return append(dst, s...)
|
||||
}
|
||||
|
||||
// AppendEmbeddedCBOR adds a tag and embeds input CBOR as such.
|
||||
func AppendEmbeddedCBOR(dst, s []byte) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeEmbeddedCBOR
|
||||
|
||||
// Append the TAG to indicate this is Embedded JSON.
|
||||
dst = append(dst, major|additionalTypeIntUint8)
|
||||
dst = append(dst, minor)
|
||||
|
||||
// Append the CBOR Object as Byte String.
|
||||
major = majorTypeByteString
|
||||
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package cbor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
var encodeStringTests = []struct {
|
||||
@@ -12,6 +15,13 @@ var encodeStringTests = []struct {
|
||||
}{
|
||||
{"", "\x60", ""},
|
||||
{"\\", "\x61\x5c", "\\\\"},
|
||||
{"\"", "\x61\x22", "\\\""},
|
||||
{"\b", "\x61\x08", "\\b"},
|
||||
{"\f", "\x61\x0c", "\\f"},
|
||||
{"\n", "\x61\x0a", "\\n"},
|
||||
{"\r", "\x61\x0d", "\\r"},
|
||||
{"\t", "\x61\x09", "\\t"},
|
||||
{"Hi\t", "\x63Hi\x09", "Hi\\t"},
|
||||
{"\x00", "\x61\x00", "\\u0000"},
|
||||
{"\x01", "\x61\x01", "\\u0001"},
|
||||
{"\x02", "\x61\x02", "\\u0002"},
|
||||
@@ -31,6 +41,7 @@ var encodeStringTests = []struct {
|
||||
"<------------------------------------ This is a 100 character string ----------------------------->" +
|
||||
"<------------------------------------ This is a 100 character string ----------------------------->"},
|
||||
{"emoji \u2764\ufe0f!", "\x6demoji ❤️!", "emoji \u2764\ufe0f!"},
|
||||
{"invalid utf8 \xff", "\x6einvalid utf8 \xff", "invalid utf8 \\ufffd"},
|
||||
}
|
||||
|
||||
var encodeByteTests = []struct {
|
||||
@@ -44,6 +55,9 @@ var encodeByteTests = []struct {
|
||||
{[]byte("\x02"), "\x41\x02"},
|
||||
{[]byte("\x03"), "\x41\x03"},
|
||||
{[]byte("\x04"), "\x41\x04"},
|
||||
{[]byte("\f"), "\x41\x0C"},
|
||||
{[]byte("\n"), "\x41\x0A"},
|
||||
{[]byte("\r"), "\x41\x0D"},
|
||||
{[]byte("*"), "\x41*"},
|
||||
{[]byte("a"), "\x41a"},
|
||||
{[]byte("IETF"), "\x44IETF"},
|
||||
@@ -77,6 +91,90 @@ func TestAppendString(t *testing.T) {
|
||||
t.Errorf("appendString(%q) = %#q, want %#q", inp, got, want)
|
||||
}
|
||||
}
|
||||
func TestAppendStrings(t *testing.T) {
|
||||
array := []string{}
|
||||
for _, tt := range encodeStringTests {
|
||||
array = append(array, tt.plain)
|
||||
}
|
||||
want := make([]byte, 0)
|
||||
want = append(want, 0x95) // start array
|
||||
for _, tt := range encodeStringTests {
|
||||
want = append(want, []byte(tt.binary)...)
|
||||
}
|
||||
|
||||
got := enc.AppendStrings([]byte{}, array)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendStrings(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
// now empty array case
|
||||
array = make([]string, 0)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, 0x80) // start an empty string array
|
||||
got = enc.AppendStrings([]byte{}, array)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendStrings(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array, hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
// now large array case
|
||||
array = make([]string, 24)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, 0x98) // start a large array
|
||||
want = append(want, 0x18) // of length 24
|
||||
for i := 0; i < len(array); i++ {
|
||||
array[i] = "test"
|
||||
want = append(want, []byte("\x64test")...)
|
||||
}
|
||||
got = enc.AppendStrings([]byte{}, array)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendStrings(%v)\ngot: %s\nwant: %s",
|
||||
array,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendStringer(t *testing.T) {
|
||||
oldJSONMarshalFunc := JSONMarshalFunc
|
||||
defer func() {
|
||||
JSONMarshalFunc = oldJSONMarshalFunc
|
||||
}()
|
||||
|
||||
JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return internal.InterfaceMarshalFunc(v)
|
||||
}
|
||||
|
||||
for _, tt := range internal.EncodeStringerTests {
|
||||
got := enc.AppendStringer([]byte{}, tt.In)
|
||||
want := []byte(tt.Binary)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendStrings(%v)\ngot: %s\nwant: %s",
|
||||
tt.In,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendStringers(t *testing.T) {
|
||||
for _, tt := range internal.EncodeStringersTests {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, []byte(tt.Binary)...)
|
||||
|
||||
got := enc.AppendStringers([]byte{}, tt.In)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendStrings(%v)\ngot: %s\nwant: %s",
|
||||
tt,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendBytes(t *testing.T) {
|
||||
for _, tt := range encodeByteTests {
|
||||
@@ -97,6 +195,7 @@ func TestAppendBytes(t *testing.T) {
|
||||
t.Errorf("appendString(%q) = %#q, want %#q", inp, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendString(b *testing.B) {
|
||||
tests := map[string]string{
|
||||
"NoEncoding": `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,
|
||||
@@ -116,3 +215,69 @@ func BenchmarkAppendString(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendEmbeddedJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty JSON",
|
||||
input: []byte{},
|
||||
want: "\xd9\x01\x06@", // tag 0xd9 + empty byte string
|
||||
},
|
||||
{
|
||||
name: "small JSON",
|
||||
input: []byte(`{"key":"value"}`),
|
||||
want: "\xd9\x01\x06O{\"key\":\"value\"}", // tag 0xd9 + byte string with content
|
||||
},
|
||||
{
|
||||
name: "large JSON (>23 bytes)",
|
||||
input: []byte(`{"key":"this is a very long value that exceeds the 23 byte limit for direct encoding"}`),
|
||||
want: "\xd9\x01\x06XV{\"key\":\"this is a very long value that exceeds the 23 byte limit for direct encoding\"}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := AppendEmbeddedJSON([]byte{}, tt.input)
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("AppendEmbeddedJSON() = %q, want %q", string(got), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendEmbeddedCBOR(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty CBOR",
|
||||
input: []byte{},
|
||||
want: "\xd8?@", // tag 0xd8 + empty byte string
|
||||
},
|
||||
{
|
||||
name: "small CBOR",
|
||||
input: []byte{0x01, 0x02, 0x03},
|
||||
want: "\xd8?C\x01\x02\x03", // tag 0xd8 + byte string with 3 bytes
|
||||
},
|
||||
{
|
||||
name: "large CBOR (>23 bytes)",
|
||||
input: make([]byte, 30), // 30 bytes of zeros
|
||||
want: "\xd8?X\x1e" + string(make([]byte, 30)), // tag 0xd8 + byte string with length prefix
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := AppendEmbeddedCBOR([]byte{}, tt.input)
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("AppendEmbeddedCBOR() = %q, want %q", string(got), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+30
-12
@@ -4,10 +4,21 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Import from zerolog/global.go
|
||||
timeFormatUnix = ""
|
||||
timeFormatUnixMs = "UNIXMS"
|
||||
timeFormatUnixMicro = "UNIXMICRO"
|
||||
timeFormatUnixNano = "UNIXNANO"
|
||||
durationFormatFloat = "float"
|
||||
durationFormatInt = "int"
|
||||
durationFormatString = "string"
|
||||
)
|
||||
|
||||
func appendIntegerTimestamp(dst []byte, t time.Time) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeTimestamp
|
||||
dst = append(dst, byte(major|minor))
|
||||
dst = append(dst, major|minor)
|
||||
secs := t.Unix()
|
||||
var val uint64
|
||||
if secs < 0 {
|
||||
@@ -17,19 +28,18 @@ func appendIntegerTimestamp(dst []byte, t time.Time) []byte {
|
||||
major = majorTypeUnsignedInt
|
||||
val = uint64(secs)
|
||||
}
|
||||
dst = appendCborTypePrefix(dst, major, uint64(val))
|
||||
dst = appendCborTypePrefix(dst, major, val)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (e Encoder) appendFloatTimestamp(dst []byte, t time.Time) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeTimestamp
|
||||
dst = append(dst, byte(major|minor))
|
||||
dst = append(dst, major|minor)
|
||||
secs := t.Unix()
|
||||
nanos := t.Nanosecond()
|
||||
var val float64
|
||||
val = float64(secs)*1.0 + float64(nanos)*1E-9
|
||||
return e.AppendFloat64(dst, val)
|
||||
val := float64(secs)*1.0 + float64(nanos)*1e-9
|
||||
return e.AppendFloat64(dst, val, -1)
|
||||
}
|
||||
|
||||
// AppendTime encodes and adds a timestamp to the dst byte array.
|
||||
@@ -50,7 +60,7 @@ func (e Encoder) AppendTimes(dst []byte, vals []time.Time, unused string) []byte
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -64,17 +74,25 @@ func (e Encoder) AppendTimes(dst []byte, vals []time.Time, unused string) []byte
|
||||
// AppendDuration encodes and adds a duration to the dst byte array.
|
||||
// useInt field indicates whether to store the duration as seconds (integer) or
|
||||
// as seconds+nanoseconds (float).
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte {
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, unused int) []byte {
|
||||
if useInt {
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit))
|
||||
switch format {
|
||||
case durationFormatFloat:
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), unused)
|
||||
case durationFormatInt:
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
case durationFormatString:
|
||||
return e.AppendString(dst, d.String())
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), unused)
|
||||
}
|
||||
|
||||
// AppendDurations encodes and adds an array of durations to the dst byte array.
|
||||
// useInt field indicates whether to store the duration as seconds (integer) or
|
||||
// as seconds+nanoseconds (float).
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte {
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, unused int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
@@ -82,12 +100,12 @@ func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Dur
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, d := range vals {
|
||||
dst = e.AppendDuration(dst, d, unit, useInt)
|
||||
dst = e.AppendDuration(dst, d, unit, format, useInt, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
+277
-28
@@ -1,19 +1,161 @@
|
||||
package cbor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
func TestEncoder_AppendDuration(t *testing.T) {
|
||||
type args struct {
|
||||
dst []byte
|
||||
d time.Duration
|
||||
unit time.Duration
|
||||
format string
|
||||
useInt bool
|
||||
unused int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "useInt",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
useInt: true,
|
||||
},
|
||||
want: []byte{1},
|
||||
},
|
||||
{
|
||||
name: "formatFloat",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
format: durationFormatFloat,
|
||||
},
|
||||
want: []byte{251, 63, 243, 192, 202, 66, 131, 222, 27},
|
||||
},
|
||||
{
|
||||
name: "formatInt",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
format: durationFormatInt,
|
||||
},
|
||||
want: []byte{1},
|
||||
},
|
||||
{
|
||||
name: "formatString",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
format: durationFormatString,
|
||||
},
|
||||
want: []byte{107, 49, 46, 50, 51, 52, 53, 54, 55, 56, 57, 115},
|
||||
},
|
||||
{
|
||||
name: "formatBlank",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
},
|
||||
want: []byte{251, 63, 243, 192, 202, 66, 131, 222, 27},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := Encoder{}
|
||||
if got := e.AppendDuration(tt.args.dst, tt.args.d, tt.args.unit, tt.args.format, tt.args.useInt, tt.args.unused); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AppendDuration() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoder_AppendDurations(t *testing.T) {
|
||||
type args struct {
|
||||
dst []byte
|
||||
vals []time.Duration
|
||||
unit time.Duration
|
||||
format string
|
||||
useInt bool
|
||||
unused int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "useInt",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
useInt: true,
|
||||
},
|
||||
want: []byte{129, 1},
|
||||
},
|
||||
{
|
||||
name: "formatFloat",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
format: durationFormatFloat,
|
||||
},
|
||||
want: []byte{129, 251, 63, 243, 192, 202, 66, 131, 222, 27},
|
||||
},
|
||||
{
|
||||
name: "formatInt",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
format: durationFormatInt,
|
||||
},
|
||||
want: []byte{129, 1},
|
||||
},
|
||||
{
|
||||
name: "formatString",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
format: durationFormatString,
|
||||
},
|
||||
want: []byte{129, 107, 49, 46, 50, 51, 52, 53, 54, 55, 56, 57, 115},
|
||||
},
|
||||
{
|
||||
name: "formatBlank",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
},
|
||||
want: []byte{129, 251, 63, 243, 192, 202, 66, 131, 222, 27},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := Encoder{}
|
||||
if got := e.AppendDurations(tt.args.dst, tt.args.vals, tt.args.unit, tt.args.format, tt.args.useInt, tt.args.unused); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AppendDurations() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendTimeNow(t *testing.T) {
|
||||
tm := time.Now()
|
||||
s := enc.AppendTime([]byte{}, tm, "unused")
|
||||
got := string(s)
|
||||
|
||||
tm1 := float64(tm.Unix()) + float64(tm.Nanosecond())*1E-9
|
||||
tm1 := float64(tm.Unix()) + float64(tm.Nanosecond())*1e-9
|
||||
tm2 := math.Float64bits(tm1)
|
||||
var tm3 [8]byte
|
||||
for i := uint(0); i < 8; i++ {
|
||||
@@ -27,55 +169,162 @@ func TestAppendTimeNow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var timeIntegerTestcases = []struct {
|
||||
txt string
|
||||
binary string
|
||||
rfcStr string
|
||||
}{
|
||||
{"2013-02-03T19:54:00-08:00", "\xc1\x1a\x51\x0f\x30\xd8", "2013-02-04T03:54:00Z"},
|
||||
{"1950-02-03T19:54:00-08:00", "\xc1\x3a\x25\x71\x93\xa7", "1950-02-04T03:54:00Z"},
|
||||
}
|
||||
|
||||
func TestAppendTimePastPresentInteger(t *testing.T) {
|
||||
for _, tt := range timeIntegerTestcases {
|
||||
tin, err := time.Parse(time.RFC3339, tt.txt)
|
||||
for _, tt := range internal.TimeIntegerTestcases {
|
||||
tin, err := time.Parse(time.RFC3339, tt.Txt)
|
||||
if err != nil {
|
||||
fmt.Println("Cannot parse input", tt.txt, ".. Skipping!", err)
|
||||
fmt.Println("Cannot parse input", tt.Txt, ".. Skipping!", err)
|
||||
continue
|
||||
}
|
||||
b := enc.AppendTime([]byte{}, tin, "unused")
|
||||
if got, want := string(b), tt.binary; got != want {
|
||||
t.Errorf("appendString(%s) = 0x%s, want 0x%s", tt.txt,
|
||||
if got, want := string(b), tt.Binary; got != want {
|
||||
t.Errorf("appendString(%s) = 0x%s, want 0x%s", tt.Txt,
|
||||
hex.EncodeToString(b),
|
||||
hex.EncodeToString([]byte(want)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var timeFloatTestcases = []struct {
|
||||
rfcStr string
|
||||
out string
|
||||
}{
|
||||
{"2006-01-02T15:04:05.999999-08:00", "\xc1\xfb\x41\xd0\xee\x6c\x59\x7f\xff\xfc"},
|
||||
{"1956-01-02T15:04:05.999999-08:00", "\xc1\xfb\xc1\xba\x53\x81\x1a\x00\x00\x11"},
|
||||
}
|
||||
|
||||
func TestAppendTimePastPresentFloat(t *testing.T) {
|
||||
const timeFloatFmt = "2006-01-02T15:04:05.999999-07:00"
|
||||
for _, tt := range timeFloatTestcases {
|
||||
tin, err := time.Parse(timeFloatFmt, tt.rfcStr)
|
||||
for _, tt := range internal.TimeFloatTestcases {
|
||||
tin, err := time.Parse(timeFloatFmt, tt.RfcStr)
|
||||
if err != nil {
|
||||
fmt.Println("Cannot parse input", tt.rfcStr, ".. Skipping!")
|
||||
fmt.Println("Cannot parse input", tt.RfcStr, ".. Skipping!")
|
||||
continue
|
||||
}
|
||||
b := enc.AppendTime([]byte{}, tin, "unused")
|
||||
if got, want := string(b), tt.out; got != want {
|
||||
t.Errorf("appendString(%s) = 0x%s, want 0x%s", tt.rfcStr,
|
||||
if got, want := string(b), tt.Out; got != want {
|
||||
t.Errorf("appendString(%s) = 0x%s, want 0x%s", tt.RfcStr,
|
||||
hex.EncodeToString(b),
|
||||
hex.EncodeToString([]byte(want)))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAppendTimes(t *testing.T) {
|
||||
const timeFloatFmt = "2006-01-02T15:04:05.999999-07:00"
|
||||
array := make([]time.Time, len(internal.TimeFloatTestcases))
|
||||
want := make([]byte, 0)
|
||||
want = append(want, 0x82) // start small array
|
||||
for i, tt := range internal.TimeFloatTestcases {
|
||||
array[i], _ = time.Parse(timeFloatFmt, tt.RfcStr)
|
||||
want = append(want, []byte(tt.Out)...)
|
||||
}
|
||||
|
||||
got := enc.AppendTimes([]byte{}, array, "unused")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendTimes(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array, hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
// now empty array case
|
||||
array = make([]time.Time, 0)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, 0x9f) // start and end array
|
||||
want = append(want, 0xff) // for empty array
|
||||
got = enc.AppendTimes([]byte{}, array, "unused")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendTimes(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array, hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
// now large array case
|
||||
testtime, _ := time.Parse(timeFloatFmt, internal.TimeFloatTestcases[0].RfcStr)
|
||||
outbytes := internal.TimeFloatTestcases[0].Out
|
||||
array = make([]time.Time, 24)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, 0x98) // start a large array
|
||||
want = append(want, 0x18) // of length 24
|
||||
for i := 0; i < len(array); i++ {
|
||||
array[i] = testtime
|
||||
want = append(want, []byte(outbytes)...)
|
||||
}
|
||||
got = enc.AppendTimes([]byte{}, array, "unused")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendTimes(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendDurationFloat(t *testing.T) {
|
||||
for _, tt := range internal.DurTestcases {
|
||||
dur := tt.Duration
|
||||
want := []byte{}
|
||||
want = append(want, []byte(tt.FloatOut)...)
|
||||
got := enc.AppendDuration([]byte{}, dur, time.Microsecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDuration(%v)=\ngot: 0x%s\nwant: 0x%s",
|
||||
dur,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAppendDurationInteger(t *testing.T) {
|
||||
for _, tt := range internal.DurTestcases {
|
||||
dur := tt.Duration
|
||||
want := []byte{}
|
||||
want = append(want, []byte(tt.IntegerOut)...)
|
||||
got := enc.AppendDuration([]byte{}, dur, time.Microsecond, "", true, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDuration(%v)=\ngot: 0x%s\nwant: 0x%s",
|
||||
dur,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAppendDurations(t *testing.T) {
|
||||
array := make([]time.Duration, len(internal.DurTestcases))
|
||||
want := make([]byte, 0)
|
||||
want = append(want, 0x83) // start 3 element array
|
||||
for i, tt := range internal.DurTestcases {
|
||||
array[i] = tt.Duration
|
||||
want = append(want, []byte(tt.FloatOut)...)
|
||||
}
|
||||
|
||||
got := enc.AppendDurations([]byte{}, array, time.Microsecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDurations(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array, hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
// now empty array case
|
||||
array = make([]time.Duration, 0)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, 0x9f) // start and end array
|
||||
want = append(want, 0xff) // for empty array
|
||||
got = enc.AppendDurations([]byte{}, array, time.Microsecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDurations(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array, hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
// now large array case
|
||||
testtime := internal.DurTestcases[0].Duration
|
||||
outbytes := internal.DurTestcases[0].FloatOut
|
||||
array = make([]time.Duration, 24)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, 0x98) // start a large array
|
||||
want = append(want, 0x18) // of length 24
|
||||
for i := 0; i < len(array); i++ {
|
||||
array[i] = testtime
|
||||
want = append(want, []byte(outbytes)...)
|
||||
}
|
||||
got = enc.AppendDurations([]byte{}, array, time.Microsecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDurations(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendTime(b *testing.B) {
|
||||
tests := map[string]string{
|
||||
|
||||
+90
-44
@@ -1,42 +1,42 @@
|
||||
package cbor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// AppendNil inserts a 'Nil' object into the dst byte array.
|
||||
func (Encoder) AppendNil(dst []byte) []byte {
|
||||
return append(dst, byte(majorTypeSimpleAndFloat|additionalTypeNull))
|
||||
return append(dst, majorTypeSimpleAndFloat|additionalTypeNull)
|
||||
}
|
||||
|
||||
// AppendBeginMarker inserts a map start into the dst byte array.
|
||||
func (Encoder) AppendBeginMarker(dst []byte) []byte {
|
||||
return append(dst, byte(majorTypeMap|additionalTypeInfiniteCount))
|
||||
return append(dst, majorTypeMap|additionalTypeInfiniteCount)
|
||||
}
|
||||
|
||||
// AppendEndMarker inserts a map end into the dst byte array.
|
||||
func (Encoder) AppendEndMarker(dst []byte) []byte {
|
||||
return append(dst, byte(majorTypeSimpleAndFloat|additionalTypeBreak))
|
||||
return append(dst, majorTypeSimpleAndFloat|additionalTypeBreak)
|
||||
}
|
||||
|
||||
// AppendObjectData takes an object in form of a byte array and appends to dst.
|
||||
func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
|
||||
// BeginMarker is present in the dst, which
|
||||
// should not be copied when appending to existing data.
|
||||
return append(dst, o[1:]...)
|
||||
// BeginMarker is present in the dst, which
|
||||
// should not be copied when appending to existing data.
|
||||
return append(dst, o[1:]...)
|
||||
}
|
||||
|
||||
// AppendArrayStart adds markers to indicate the start of an array.
|
||||
func (Encoder) AppendArrayStart(dst []byte) []byte {
|
||||
return append(dst, byte(majorTypeArray|additionalTypeInfiniteCount))
|
||||
return append(dst, majorTypeArray|additionalTypeInfiniteCount)
|
||||
}
|
||||
|
||||
// AppendArrayEnd adds markers to indicate the end of an array.
|
||||
func (Encoder) AppendArrayEnd(dst []byte) []byte {
|
||||
return append(dst, byte(majorTypeSimpleAndFloat|additionalTypeBreak))
|
||||
return append(dst, majorTypeSimpleAndFloat|additionalTypeBreak)
|
||||
}
|
||||
|
||||
// AppendArrayDelim adds markers to indicate end of a particular array element.
|
||||
@@ -57,7 +57,7 @@ func (Encoder) AppendBool(dst []byte, val bool) []byte {
|
||||
if val {
|
||||
b = additionalTypeBoolTrue
|
||||
}
|
||||
return append(dst, byte(majorTypeSimpleAndFloat|b))
|
||||
return append(dst, majorTypeSimpleAndFloat|b)
|
||||
}
|
||||
|
||||
// AppendBools encodes and inserts an array of boolean values into the dst byte array.
|
||||
@@ -69,7 +69,7 @@ func (e Encoder) AppendBools(dst []byte, vals []bool) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func (Encoder) AppendInt(dst []byte, val int) []byte {
|
||||
}
|
||||
if contentVal <= additionalMax {
|
||||
lb := byte(contentVal)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(contentVal))
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func (e Encoder) AppendInts(dst []byte, vals []int) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func (e Encoder) AppendInts8(dst []byte, vals []int8) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func (e Encoder) AppendInts16(dst []byte, vals []int16) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func (e Encoder) AppendInts32(dst []byte, vals []int32) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -197,7 +197,7 @@ func (Encoder) AppendInt64(dst []byte, val int64) []byte {
|
||||
}
|
||||
if contentVal <= additionalMax {
|
||||
lb := byte(contentVal)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(contentVal))
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func (e Encoder) AppendInts64(dst []byte, vals []int64) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func (e Encoder) AppendUints(dst []byte, vals []uint) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func (e Encoder) AppendUints8(dst []byte, vals []uint8) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -285,7 +285,7 @@ func (e Encoder) AppendUints16(dst []byte, vals []uint16) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -309,7 +309,7 @@ func (e Encoder) AppendUints32(dst []byte, vals []uint32) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -325,9 +325,9 @@ func (Encoder) AppendUint64(dst []byte, val uint64) []byte {
|
||||
contentVal := val
|
||||
if contentVal <= additionalMax {
|
||||
lb := byte(contentVal)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(contentVal))
|
||||
dst = appendCborTypePrefix(dst, major, contentVal)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
@@ -341,7 +341,7 @@ func (e Encoder) AppendUints64(dst []byte, vals []uint64) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
@@ -352,7 +352,7 @@ func (e Encoder) AppendUints64(dst []byte, vals []uint64) []byte {
|
||||
}
|
||||
|
||||
// AppendFloat32 encodes and inserts a single precision float value into the dst byte array.
|
||||
func (Encoder) AppendFloat32(dst []byte, val float32) []byte {
|
||||
func (Encoder) AppendFloat32(dst []byte, val float32, unused int) []byte {
|
||||
switch {
|
||||
case math.IsNaN(float64(val)):
|
||||
return append(dst, "\xfa\x7f\xc0\x00\x00"...)
|
||||
@@ -368,11 +368,11 @@ func (Encoder) AppendFloat32(dst []byte, val float32) []byte {
|
||||
for i := uint(0); i < 4; i++ {
|
||||
buf[i] = byte(n >> ((3 - i) * 8))
|
||||
}
|
||||
return append(append(dst, byte(major|subType)), buf[0], buf[1], buf[2], buf[3])
|
||||
return append(append(dst, major|subType), buf[0], buf[1], buf[2], buf[3])
|
||||
}
|
||||
|
||||
// AppendFloats32 encodes and inserts an array of single precision float value into the dst byte array.
|
||||
func (e Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
|
||||
func (e Encoder) AppendFloats32(dst []byte, vals []float32, unused int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
@@ -380,18 +380,18 @@ func (e Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendFloat32(dst, v)
|
||||
dst = e.AppendFloat32(dst, v, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendFloat64 encodes and inserts a double precision float value into the dst byte array.
|
||||
func (Encoder) AppendFloat64(dst []byte, val float64) []byte {
|
||||
func (Encoder) AppendFloat64(dst []byte, val float64, unused int) []byte {
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
return append(dst, "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00"...)
|
||||
@@ -403,7 +403,7 @@ func (Encoder) AppendFloat64(dst []byte, val float64) []byte {
|
||||
major := majorTypeSimpleAndFloat
|
||||
subType := additionalTypeFloat64
|
||||
n := math.Float64bits(val)
|
||||
dst = append(dst, byte(major|subType))
|
||||
dst = append(dst, major|subType)
|
||||
for i := uint(1); i <= 8; i++ {
|
||||
b := byte(n >> ((8 - i) * 8))
|
||||
dst = append(dst, b)
|
||||
@@ -412,7 +412,7 @@ func (Encoder) AppendFloat64(dst []byte, val float64) []byte {
|
||||
}
|
||||
|
||||
// AppendFloats64 encodes and inserts an array of double precision float values into the dst byte array.
|
||||
func (e Encoder) AppendFloats64(dst []byte, vals []float64) []byte {
|
||||
func (e Encoder) AppendFloats64(dst []byte, vals []float64, unused int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
@@ -420,50 +420,96 @@ func (e Encoder) AppendFloats64(dst []byte, vals []float64) []byte {
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, byte(major|lb))
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendFloat64(dst, v)
|
||||
dst = e.AppendFloat64(dst, v, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInterface takes an arbitrary object and converts it to JSON and embeds it dst.
|
||||
func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte {
|
||||
marshaled, err := json.Marshal(i)
|
||||
marshaled, err := JSONMarshalFunc(i)
|
||||
if err != nil {
|
||||
return e.AppendString(dst, fmt.Sprintf("marshaling error: %v", err))
|
||||
}
|
||||
return AppendEmbeddedJSON(dst, marshaled)
|
||||
}
|
||||
|
||||
// AppendIPAddr encodes and inserts an IP Address (IPv4 or IPv6).
|
||||
// AppendType appends the parameter type (as a string) to the input byte slice.
|
||||
func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
|
||||
if i == nil {
|
||||
return e.AppendString(dst, "<nil>")
|
||||
}
|
||||
return e.AppendString(dst, reflect.TypeOf(i).String())
|
||||
}
|
||||
|
||||
// AppendIPAddr adds a net.IP IPv4 or IPv6 address into the dst byte array.
|
||||
func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
|
||||
dst = append(dst, byte(majorTypeTags|additionalTypeIntUint16))
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr>>8))
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr&0xff))
|
||||
return e.AppendBytes(dst, ip)
|
||||
}
|
||||
|
||||
// AppendIPPrefix encodes and inserts an IP Address Prefix (Address + Mask Length).
|
||||
// AppendIPAddrs adds a []net.IP array of IPv4 or IPv6 address into the dst byte array.
|
||||
func (e Encoder) AppendIPAddrs(dst []byte, ips []net.IP) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(ips)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range ips {
|
||||
dst = e.AppendIPAddr(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendIPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (address & mask) into the dst byte array.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
dst = append(dst, byte(majorTypeTags|additionalTypeIntUint16))
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkPrefix>>8))
|
||||
dst = append(dst, byte(additionalTypeTagNetworkPrefix&0xff))
|
||||
|
||||
// Prefix is a tuple (aka MAP of 1 pair of elements) -
|
||||
// first element is prefix, second is mask length.
|
||||
dst = append(dst, byte(majorTypeMap|0x1))
|
||||
dst = append(dst, majorTypeMap|0x1)
|
||||
dst = e.AppendBytes(dst, pfx.IP)
|
||||
maskLen, _ := pfx.Mask.Size()
|
||||
return e.AppendUint8(dst, uint8(maskLen))
|
||||
}
|
||||
|
||||
// AppendMACAddr encodes and inserts an Hardware (MAC) address.
|
||||
// AppendIPPrefixes adds a []net.IPNet array of IPv4 or IPv6 Prefix (address & mask) into the dst byte array.
|
||||
func (e Encoder) AppendIPPrefixes(dst []byte, pfxs []net.IPNet) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(pfxs)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range pfxs {
|
||||
dst = e.AppendIPPrefix(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr encodes and inserts a Hardware (MAC) address.
|
||||
func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte {
|
||||
dst = append(dst, byte(majorTypeTags|additionalTypeIntUint16))
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr>>8))
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr&0xff))
|
||||
return e.AppendBytes(dst, ha)
|
||||
@@ -471,7 +517,7 @@ func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte {
|
||||
|
||||
// AppendHex adds a TAG and inserts a hex bytes as a string.
|
||||
func (e Encoder) AppendHex(dst []byte, val []byte) []byte {
|
||||
dst = append(dst, byte(majorTypeTags|additionalTypeIntUint16))
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagHexString>>8))
|
||||
dst = append(dst, byte(additionalTypeTagHexString&0xff))
|
||||
return e.AppendBytes(dst, val)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// +build !386
|
||||
|
||||
package cbor
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var enc2 = Encoder{}
|
||||
|
||||
var integerTestCases_64bit = []struct {
|
||||
val int
|
||||
binary string
|
||||
}{
|
||||
// Value in 8 bytes.
|
||||
{0xabcd100000000, "\x1b\x00\x0a\xbc\xd1\x00\x00\x00\x00"},
|
||||
{1000000000000, "\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"},
|
||||
// Value in 8 bytes.
|
||||
{-0xabcd100000001, "\x3b\x00\x0a\xbc\xd1\x00\x00\x00\x00"},
|
||||
{-1000000000001, "\x3b\x00\x00\x00\xe8\xd4\xa5\x10\x00"},
|
||||
}
|
||||
|
||||
func TestAppendInt_64bit(t *testing.T) {
|
||||
for _, tc := range integerTestCases_64bit {
|
||||
s := enc2.AppendInt([]byte{}, tc.val)
|
||||
got := string(s)
|
||||
if got != tc.binary {
|
||||
t.Errorf("AppendInt(0x%x)=0x%s, want: 0x%s",
|
||||
tc.val, hex.EncodeToString(s),
|
||||
hex.EncodeToString([]byte(tc.binary)))
|
||||
}
|
||||
}
|
||||
}
|
||||
+961
-173
File diff suppressed because it is too large
Load Diff
+11
-4
@@ -1,12 +1,19 @@
|
||||
package json
|
||||
|
||||
// JSONMarshalFunc is used to marshal interface to JSON encoded byte slice.
|
||||
// Making it package level instead of embedded in Encoder brings
|
||||
// some extra efforts at importing, but avoids value copy when the functions
|
||||
// of Encoder being invoked.
|
||||
// DO REMEMBER to set this variable at importing, or
|
||||
// you might get a nil pointer dereference panic at runtime.
|
||||
var JSONMarshalFunc func(v interface{}) ([]byte, error)
|
||||
|
||||
type Encoder struct{}
|
||||
|
||||
// AppendKey appends a new key to the output JSON.
|
||||
func (e Encoder) AppendKey(dst []byte, key string) []byte {
|
||||
if len(dst) > 1 && dst[len(dst)-1] != '{' {
|
||||
if dst[len(dst)-1] != '{' {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
dst = e.AppendString(dst, key)
|
||||
return append(dst, ':')
|
||||
}
|
||||
return append(e.AppendString(dst, key), ':')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendKey(t *testing.T) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, []byte("{\"key\":")...)
|
||||
|
||||
got := enc.AppendKey([]byte("{"), "key") // test with empty object
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendKey(%v)\ngot: %s\nwant: %s",
|
||||
"key",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
|
||||
want = make([]byte, 0)
|
||||
want = append(want, []byte("},\"key\":")...) // test with non-empty object
|
||||
|
||||
got = enc.AppendKey([]byte("}"), "key")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendKey(%v)\ngot: %s\nwant: %s",
|
||||
"key",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func (Encoder) AppendBytes(dst, s []byte) []byte {
|
||||
func (Encoder) AppendHex(dst, s []byte) []byte {
|
||||
dst = append(dst, '"')
|
||||
for _, v := range s {
|
||||
dst = append(dst, hex[v>>4], hex[v&0x0f])
|
||||
dst = append(dst, hexCharacters[v>>4], hexCharacters[v&0x0f])
|
||||
}
|
||||
return append(dst, '"')
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func appendBytesComplex(dst, s []byte, i int) []byte {
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
|
||||
dst = append(dst, '\\', 'u', '0', '0', hexCharacters[b>>4], hexCharacters[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
|
||||
@@ -3,24 +3,26 @@ package json
|
||||
import (
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
var enc = Encoder{}
|
||||
|
||||
func TestAppendBytes(t *testing.T) {
|
||||
for _, tt := range encodeStringTests {
|
||||
b := enc.AppendBytes([]byte{}, []byte(tt.in))
|
||||
if got, want := string(b), tt.out; got != want {
|
||||
t.Errorf("appendBytes(%q) = %#q, want %#q", tt.in, got, want)
|
||||
for _, tt := range internal.EncodeStringTests {
|
||||
b := enc.AppendBytes([]byte{}, []byte(tt.In))
|
||||
if got, want := string(b), tt.Out; got != want {
|
||||
t.Errorf("appendBytes(%q) = %#q, want %#q", tt.In, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendHex(t *testing.T) {
|
||||
for _, tt := range encodeHexTests {
|
||||
b := enc.AppendHex([]byte{}, []byte{tt.in})
|
||||
if got, want := string(b), tt.out; got != want {
|
||||
t.Errorf("appendHex(%x) = %s, want %s", tt.in, got, want)
|
||||
for _, tt := range internal.EncodeHexTests {
|
||||
b := enc.AppendHex([]byte{}, []byte{tt.In})
|
||||
if got, want := string(b), tt.Out; got != want {
|
||||
t.Errorf("appendHex(%x) = %s, want %s", tt.In, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
var float64Tests = []struct {
|
||||
Name string
|
||||
Val float64
|
||||
Want string
|
||||
}{
|
||||
{
|
||||
Name: "Positive integer",
|
||||
Val: 1234.0,
|
||||
Want: "1234",
|
||||
},
|
||||
{
|
||||
Name: "Negative integer",
|
||||
Val: -5678.0,
|
||||
Want: "-5678",
|
||||
},
|
||||
{
|
||||
Name: "Positive decimal",
|
||||
Val: 12.3456,
|
||||
Want: "12.3456",
|
||||
},
|
||||
{
|
||||
Name: "Negative decimal",
|
||||
Val: -78.9012,
|
||||
Want: "-78.9012",
|
||||
},
|
||||
{
|
||||
Name: "Large positive number",
|
||||
Val: 123456789.0,
|
||||
Want: "123456789",
|
||||
},
|
||||
{
|
||||
Name: "Large negative number",
|
||||
Val: -987654321.0,
|
||||
Want: "-987654321",
|
||||
},
|
||||
{
|
||||
Name: "Zero",
|
||||
Val: 0.0,
|
||||
Want: "0",
|
||||
},
|
||||
{
|
||||
Name: "Smallest positive value",
|
||||
Val: math.SmallestNonzeroFloat64,
|
||||
Want: "5e-324",
|
||||
},
|
||||
{
|
||||
Name: "Largest positive value",
|
||||
Val: math.MaxFloat64,
|
||||
Want: "1.7976931348623157e+308",
|
||||
},
|
||||
{
|
||||
Name: "Smallest negative value",
|
||||
Val: -math.SmallestNonzeroFloat64,
|
||||
Want: "-5e-324",
|
||||
},
|
||||
{
|
||||
Name: "Largest negative value",
|
||||
Val: -math.MaxFloat64,
|
||||
Want: "-1.7976931348623157e+308",
|
||||
},
|
||||
{
|
||||
Name: "NaN",
|
||||
Val: math.NaN(),
|
||||
Want: `"NaN"`,
|
||||
},
|
||||
{
|
||||
Name: "+Inf",
|
||||
Val: math.Inf(1),
|
||||
Want: `"+Inf"`,
|
||||
},
|
||||
{
|
||||
Name: "-Inf",
|
||||
Val: math.Inf(-1),
|
||||
Want: `"-Inf"`,
|
||||
},
|
||||
{
|
||||
Name: "Clean up e-09 to e-9 case 1",
|
||||
Val: 1e-9,
|
||||
Want: "1e-9",
|
||||
},
|
||||
{
|
||||
Name: "Clean up e-09 to e-9 case 2",
|
||||
Val: -2.236734e-9,
|
||||
Want: "-2.236734e-9",
|
||||
},
|
||||
}
|
||||
|
||||
func TestEncoder_AppendFloat64(t *testing.T) {
|
||||
for _, tc := range float64Tests {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
var b []byte
|
||||
b = (Encoder{}).AppendFloat64(b, tc.Val, -1)
|
||||
if s := string(b); tc.Want != s {
|
||||
t.Errorf("%q", s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzEncoder_AppendFloat64(f *testing.F) {
|
||||
for _, tc := range float64Tests {
|
||||
f.Add(tc.Val)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, val float64) {
|
||||
actual := (Encoder{}).AppendFloat64(nil, val, -1)
|
||||
if len(actual) == 0 {
|
||||
t.Fatal("empty buffer")
|
||||
}
|
||||
|
||||
if actual[0] == '"' {
|
||||
switch string(actual) {
|
||||
case `"NaN"`:
|
||||
if !math.IsNaN(val) {
|
||||
t.Fatalf("expected %v got NaN", val)
|
||||
}
|
||||
case `"+Inf"`:
|
||||
if !math.IsInf(val, 1) {
|
||||
t.Fatalf("expected %v got +Inf", val)
|
||||
}
|
||||
case `"-Inf"`:
|
||||
if !math.IsInf(val, -1) {
|
||||
t.Fatalf("expected %v got -Inf", val)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected string: %s", actual)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if expected, err := json.Marshal(val); err != nil {
|
||||
t.Error(err)
|
||||
} else if string(actual) != string(expected) {
|
||||
t.Errorf("expected %s, got %s", expected, actual)
|
||||
}
|
||||
|
||||
var parsed float64
|
||||
if err := json.Unmarshal(actual, &parsed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if parsed != val {
|
||||
t.Fatalf("expected %v, got %v", val, parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var float32Tests = []struct {
|
||||
Name string
|
||||
Val float32
|
||||
Want string
|
||||
}{
|
||||
{
|
||||
Name: "Positive integer",
|
||||
Val: 1234.0,
|
||||
Want: "1234",
|
||||
},
|
||||
{
|
||||
Name: "Negative integer",
|
||||
Val: -5678.0,
|
||||
Want: "-5678",
|
||||
},
|
||||
{
|
||||
Name: "Positive decimal",
|
||||
Val: 12.3456,
|
||||
Want: "12.3456",
|
||||
},
|
||||
{
|
||||
Name: "Negative decimal",
|
||||
Val: -78.9012,
|
||||
Want: "-78.9012",
|
||||
},
|
||||
{
|
||||
Name: "Large positive number",
|
||||
Val: 123456789.0,
|
||||
Want: "123456790",
|
||||
},
|
||||
{
|
||||
Name: "Large negative number",
|
||||
Val: -987654321.0,
|
||||
Want: "-987654340",
|
||||
},
|
||||
{
|
||||
Name: "Zero",
|
||||
Val: 0.0,
|
||||
Want: "0",
|
||||
},
|
||||
{
|
||||
Name: "Smallest positive value",
|
||||
Val: math.SmallestNonzeroFloat32,
|
||||
Want: "1e-45",
|
||||
},
|
||||
{
|
||||
Name: "Largest positive value",
|
||||
Val: math.MaxFloat32,
|
||||
Want: "3.4028235e+38",
|
||||
},
|
||||
{
|
||||
Name: "Smallest negative value",
|
||||
Val: -math.SmallestNonzeroFloat32,
|
||||
Want: "-1e-45",
|
||||
},
|
||||
{
|
||||
Name: "Largest negative value",
|
||||
Val: -math.MaxFloat32,
|
||||
Want: "-3.4028235e+38",
|
||||
},
|
||||
{
|
||||
Name: "NaN",
|
||||
Val: float32(math.NaN()),
|
||||
Want: `"NaN"`,
|
||||
},
|
||||
{
|
||||
Name: "+Inf",
|
||||
Val: float32(math.Inf(1)),
|
||||
Want: `"+Inf"`,
|
||||
},
|
||||
{
|
||||
Name: "-Inf",
|
||||
Val: float32(math.Inf(-1)),
|
||||
Want: `"-Inf"`,
|
||||
},
|
||||
{
|
||||
Name: "Clean up e-09 to e-9 case 1",
|
||||
Val: 1e-9,
|
||||
Want: "1e-9",
|
||||
},
|
||||
{
|
||||
Name: "Clean up e-09 to e-9 case 2",
|
||||
Val: -2.236734e-9,
|
||||
Want: "-2.236734e-9",
|
||||
},
|
||||
}
|
||||
|
||||
func TestEncoder_AppendFloat32(t *testing.T) {
|
||||
for _, tc := range float32Tests {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
var b []byte
|
||||
b = (Encoder{}).AppendFloat32(b, tc.Val, -1)
|
||||
if s := string(b); tc.Want != s {
|
||||
t.Errorf("%q", s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzEncoder_AppendFloat32(f *testing.F) {
|
||||
for _, tc := range float32Tests {
|
||||
f.Add(tc.Val)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, val float32) {
|
||||
actual := (Encoder{}).AppendFloat32(nil, val, -1)
|
||||
if len(actual) == 0 {
|
||||
t.Fatal("empty buffer")
|
||||
}
|
||||
|
||||
if actual[0] == '"' {
|
||||
val := float64(val)
|
||||
switch string(actual) {
|
||||
case `"NaN"`:
|
||||
if !math.IsNaN(val) {
|
||||
t.Fatalf("expected %v got NaN", val)
|
||||
}
|
||||
case `"+Inf"`:
|
||||
if !math.IsInf(val, 1) {
|
||||
t.Fatalf("expected %v got +Inf", val)
|
||||
}
|
||||
case `"-Inf"`:
|
||||
if !math.IsInf(val, -1) {
|
||||
t.Fatalf("expected %v got -Inf", val)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected string: %s", actual)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if expected, err := json.Marshal(val); err != nil {
|
||||
t.Error(err)
|
||||
} else if string(actual) != string(expected) {
|
||||
t.Errorf("expected %s, got %s", expected, actual)
|
||||
}
|
||||
|
||||
var parsed float32
|
||||
if err := json.Unmarshal(actual, &parsed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if parsed != val {
|
||||
t.Fatalf("expected %v, got %v", val, parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func generateFloat32s(n int) []float32 {
|
||||
floats := make([]float32, n)
|
||||
for i := 0; i < n; i++ {
|
||||
floats[i] = rand.Float32()
|
||||
}
|
||||
return floats
|
||||
}
|
||||
|
||||
func generateFloat64s(n int) []float64 {
|
||||
floats := make([]float64, n)
|
||||
for i := 0; i < n; i++ {
|
||||
floats[i] = rand.Float64()
|
||||
}
|
||||
return floats
|
||||
}
|
||||
|
||||
func TestAppendFloats32(t *testing.T) {
|
||||
doOne := func(vals []float32) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
if math.IsNaN(float64(val)) {
|
||||
want = append(want, []byte(`"NaN"`)...)
|
||||
} else if math.IsInf(float64(val), 1) {
|
||||
want = append(want, []byte(`"+Inf"`)...)
|
||||
} else if math.IsInf(float64(val), -1) {
|
||||
want = append(want, []byte(`"-Inf"`)...)
|
||||
} else {
|
||||
want = append(want, []byte(fmt.Sprintf("%v", float32(val)))...)
|
||||
}
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendFloats32([]byte{}, vals, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendFloats32(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]float32, 0)
|
||||
for _, tc := range internal.Float32TestCases {
|
||||
if tc.Val > 0 && tc.Val < 1e-4 {
|
||||
continue // we want to ignore very small numbers for this test
|
||||
}
|
||||
array = append(array, float32(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
|
||||
func TestAppendFloats64(t *testing.T) {
|
||||
doOne := func(vals []float64) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
if math.IsNaN(val) {
|
||||
want = append(want, []byte(`"NaN"`)...)
|
||||
} else if math.IsInf(val, 1) {
|
||||
want = append(want, []byte(`"+Inf"`)...)
|
||||
} else if math.IsInf(val, -1) {
|
||||
want = append(want, []byte(`"-Inf"`)...)
|
||||
} else {
|
||||
want = append(want, []byte(fmt.Sprintf("%v", val))...)
|
||||
}
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendFloats64([]byte{}, vals, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendFloats64(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]float64, 0)
|
||||
for _, tc := range internal.Float64TestCases {
|
||||
if tc.Val > 0 && tc.Val < 1e-4 {
|
||||
continue // we want to ignore very small numbers for this test
|
||||
}
|
||||
array = append(array, tc.Val)
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
|
||||
// this is really just for the memory allocation characteristics
|
||||
func BenchmarkEncoder_AppendFloat32(b *testing.B) {
|
||||
floats := append(generateFloat32s(5000), float32(math.NaN()), float32(math.Inf(1)), float32(math.Inf(-1)))
|
||||
dst := make([]byte, 0, 128)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, f := range floats {
|
||||
dst = (Encoder{}).AppendFloat32(dst[:0], f, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is really just for the memory allocation characteristics
|
||||
func BenchmarkEncoder_AppendFloat64(b *testing.B) {
|
||||
floats := append(generateFloat64s(5000), math.NaN(), math.Inf(1), math.Inf(-1))
|
||||
dst := make([]byte, 0, 128)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, f := range floats {
|
||||
dst = (Encoder{}).AppendFloat64(dst[:0], f, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
func TestAppendInts8(t *testing.T) {
|
||||
doOne := func(vals []int8) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", int8(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendInts8([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInts8(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]int8, 0)
|
||||
for _, tc := range internal.IntegerTestCases {
|
||||
if (tc.Val < math.MinInt8) || (tc.Val > math.MaxInt8) {
|
||||
continue
|
||||
}
|
||||
array = append(array, int8(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
func TestAppendUints8(t *testing.T) {
|
||||
doOne := func(vals []uint8) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%v", uint8(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendUints8([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendUints8(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]uint8, 0)
|
||||
for _, tc := range internal.UnsignedIntegerTestCases {
|
||||
if tc.Val > math.MaxUint8 {
|
||||
continue
|
||||
}
|
||||
array = append(array, uint8(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
func TestAppendInts16(t *testing.T) {
|
||||
doOne := func(vals []int16) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", int16(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendInts16([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInts16(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]int16, 0)
|
||||
for _, tc := range internal.IntegerTestCases {
|
||||
if (tc.Val < math.MinInt16) || (tc.Val > math.MaxInt16) {
|
||||
continue
|
||||
}
|
||||
array = append(array, int16(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
func TestAppendUints16(t *testing.T) {
|
||||
doOne := func(vals []uint16) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", uint16(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendUints16([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendUints16(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]uint16, 0)
|
||||
for _, tc := range internal.UnsignedIntegerTestCases {
|
||||
if tc.Val > math.MaxUint16 {
|
||||
continue
|
||||
}
|
||||
array = append(array, uint16(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
func TestAppendInts32(t *testing.T) {
|
||||
doOne := func(vals []int32) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", int32(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendInts32([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInts32(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]int32, 0)
|
||||
for _, tc := range internal.IntegerTestCases {
|
||||
if (tc.Val < math.MinInt32) || (tc.Val > math.MaxInt32) {
|
||||
continue
|
||||
}
|
||||
array = append(array, int32(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
func TestAppendUints32(t *testing.T) {
|
||||
doOne := func(vals []uint32) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", uint32(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendUints32([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendUints32(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]uint32, 0)
|
||||
for _, tc := range internal.UnsignedIntegerTestCases {
|
||||
if tc.Val > math.MaxUint32 {
|
||||
continue
|
||||
}
|
||||
array = append(array, uint32(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
|
||||
func TestAppendInt64(t *testing.T) {
|
||||
doOne := func(vals []int64) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", int64(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendInts64([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInts64(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]int64, 0)
|
||||
for _, tc := range internal.IntegerTestCases {
|
||||
array = append(array, int64(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
func TestAppendUints64(t *testing.T) {
|
||||
doOne := func(vals []uint64) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", uint64(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendUints64([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendUints64(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]uint64, 0)
|
||||
for _, tc := range internal.UnsignedIntegerTestCases {
|
||||
array = append(array, uint64(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
|
||||
func TestAppendInt(t *testing.T) {
|
||||
for _, tc := range internal.IntegerTestCases {
|
||||
want := []byte(fmt.Sprintf("%d", tc.Val))
|
||||
got := enc.AppendInt([]byte{}, tc.Val)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInt(0x%x)\ngot: %s\nwant: %s",
|
||||
tc.Val,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAppendUint(t *testing.T) {
|
||||
for _, tc := range internal.UnsignedIntegerTestCases {
|
||||
want := []byte(fmt.Sprintf("%d", tc.Val))
|
||||
got := enc.AppendUint([]byte{}, tc.Val)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendUint(0x%x)\ngot: %s\nwant: %s",
|
||||
tc.Val,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendInts(t *testing.T) {
|
||||
doOne := func(vals []int) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", int(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendInts([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInts(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]int, 0)
|
||||
for _, tc := range internal.IntegerTestCases {
|
||||
array = append(array, int(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
func TestAppendUints(t *testing.T) {
|
||||
doOne := func(vals []uint) {
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, val := range vals {
|
||||
want = append(want, []byte(fmt.Sprintf("%d", uint(val)))...)
|
||||
if i < len(vals)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendUints([]byte{}, vals)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendUints(%v)\ngot: %s\nwant: %s",
|
||||
vals,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
array := make([]uint, 0)
|
||||
for _, tc := range internal.UnsignedIntegerTestCases {
|
||||
array = append(array, uint(tc.Val))
|
||||
}
|
||||
|
||||
doOne(array)
|
||||
doOne(array[:1]) // single element
|
||||
doOne(array[:0]) // edge case of zero length
|
||||
}
|
||||
+34
-6
@@ -1,8 +1,11 @@
|
||||
package json
|
||||
|
||||
import "unicode/utf8"
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const hex = "0123456789abcdef"
|
||||
const hexCharacters = "0123456789abcdef"
|
||||
|
||||
var noEscapeTable = [256]bool{}
|
||||
|
||||
@@ -34,7 +37,7 @@ func (e Encoder) AppendStrings(dst []byte, vals []string) []byte {
|
||||
//
|
||||
// The operation loops though each byte in the string looking
|
||||
// for characters that need json or utf8 encoding. If the string
|
||||
// does not need encoding, then the string is appended in it's
|
||||
// does not need encoding, then the string is appended in its
|
||||
// entirety to the byte slice.
|
||||
// If we encounter a byte that does need encoding, switch up
|
||||
// the operation and perform a byte-by-byte read-encode-append.
|
||||
@@ -53,13 +56,38 @@ func (Encoder) AppendString(dst []byte, s string) []byte {
|
||||
return append(dst, '"')
|
||||
}
|
||||
}
|
||||
// The string has no need for encoding an therefore is directly
|
||||
// The string has no need for encoding and therefore is directly
|
||||
// appended to the byte slice.
|
||||
dst = append(dst, s...)
|
||||
// End with a double quote
|
||||
return append(dst, '"')
|
||||
}
|
||||
|
||||
// AppendStringers encodes the provided Stringer list to json and
|
||||
// appends the encoded Stringer list to the input byte slice.
|
||||
func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte {
|
||||
if vals == nil || len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendStringer(dst, vals[0])
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = e.AppendStringer(append(dst, ','), val)
|
||||
}
|
||||
}
|
||||
return append(dst, ']')
|
||||
}
|
||||
|
||||
// AppendStringer encodes the input Stringer to json and appends the
|
||||
// encoded Stringer value to the input byte slice.
|
||||
func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte {
|
||||
if val == nil {
|
||||
return e.AppendInterface(dst, nil)
|
||||
}
|
||||
return e.AppendString(dst, val.String())
|
||||
}
|
||||
|
||||
// appendStringComplex is used by appendString to take over an in
|
||||
// progress JSON string encoding that encountered a character that needs
|
||||
// to be encoded.
|
||||
@@ -71,7 +99,7 @@ func appendStringComplex(dst []byte, s string, i int) []byte {
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
// In case of error, first append previous simple characters to
|
||||
// the byte slice if any and append a remplacement character code
|
||||
// the byte slice if any and append a replacement character code
|
||||
// in place of the invalid sequence.
|
||||
if start < i {
|
||||
dst = append(dst, s[start:i]...)
|
||||
@@ -109,7 +137,7 @@ func appendStringComplex(dst []byte, s string, i int) []byte {
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, '\\', 'u', '0', '0', hex[b>>4], hex[b&0xF])
|
||||
dst = append(dst, '\\', 'u', '0', '0', hexCharacters[b>>4], hexCharacters[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
|
||||
@@ -2,72 +2,51 @@ package json
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
var encodeStringTests = []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"", `""`},
|
||||
{"\\", `"\\"`},
|
||||
{"\x00", `"\u0000"`},
|
||||
{"\x01", `"\u0001"`},
|
||||
{"\x02", `"\u0002"`},
|
||||
{"\x03", `"\u0003"`},
|
||||
{"\x04", `"\u0004"`},
|
||||
{"\x05", `"\u0005"`},
|
||||
{"\x06", `"\u0006"`},
|
||||
{"\x07", `"\u0007"`},
|
||||
{"\x08", `"\b"`},
|
||||
{"\x09", `"\t"`},
|
||||
{"\x0a", `"\n"`},
|
||||
{"\x0b", `"\u000b"`},
|
||||
{"\x0c", `"\f"`},
|
||||
{"\x0d", `"\r"`},
|
||||
{"\x0e", `"\u000e"`},
|
||||
{"\x0f", `"\u000f"`},
|
||||
{"\x10", `"\u0010"`},
|
||||
{"\x11", `"\u0011"`},
|
||||
{"\x12", `"\u0012"`},
|
||||
{"\x13", `"\u0013"`},
|
||||
{"\x14", `"\u0014"`},
|
||||
{"\x15", `"\u0015"`},
|
||||
{"\x16", `"\u0016"`},
|
||||
{"\x17", `"\u0017"`},
|
||||
{"\x18", `"\u0018"`},
|
||||
{"\x19", `"\u0019"`},
|
||||
{"\x1a", `"\u001a"`},
|
||||
{"\x1b", `"\u001b"`},
|
||||
{"\x1c", `"\u001c"`},
|
||||
{"\x1d", `"\u001d"`},
|
||||
{"\x1e", `"\u001e"`},
|
||||
{"\x1f", `"\u001f"`},
|
||||
{"✭", `"✭"`},
|
||||
{"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence
|
||||
{"ascii", `"ascii"`},
|
||||
{"\"a", `"\"a"`},
|
||||
{"\x1fa", `"\u001fa"`},
|
||||
{"foo\"bar\"baz", `"foo\"bar\"baz"`},
|
||||
{"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`},
|
||||
{"emoji \u2764\ufe0f!", `"emoji ❤️!"`},
|
||||
}
|
||||
|
||||
var encodeHexTests = []struct {
|
||||
in byte
|
||||
out string
|
||||
}{
|
||||
{0x00, `"00"`},
|
||||
{0x0f, `"0f"`},
|
||||
{0x10, `"10"`},
|
||||
{0xf0, `"f0"`},
|
||||
{0xff, `"ff"`},
|
||||
}
|
||||
|
||||
func TestAppendString(t *testing.T) {
|
||||
for _, tt := range encodeStringTests {
|
||||
b := enc.AppendString([]byte{}, tt.in)
|
||||
if got, want := string(b), tt.out; got != want {
|
||||
t.Errorf("appendString(%q) = %#q, want %#q", tt.in, got, want)
|
||||
for _, tt := range internal.EncodeStringTests {
|
||||
b := enc.AppendString([]byte{}, tt.In)
|
||||
if got, want := string(b), tt.Out; got != want {
|
||||
t.Errorf("appendString(%q) = %#q, want %#q", tt.In, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendStrings(t *testing.T) {
|
||||
for _, tt := range internal.EncodeStringsTests {
|
||||
b := enc.AppendStrings([]byte{}, tt.In)
|
||||
if got, want := string(b), tt.Out; got != want {
|
||||
t.Errorf("appendStrings(%q) = %#q, want %#q", tt.In, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendStringer(t *testing.T) {
|
||||
oldJSONMarshalFunc := JSONMarshalFunc
|
||||
defer func() {
|
||||
JSONMarshalFunc = oldJSONMarshalFunc
|
||||
}()
|
||||
|
||||
JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return internal.InterfaceMarshalFunc(v)
|
||||
}
|
||||
|
||||
for _, tt := range internal.EncodeStringerTests {
|
||||
b := enc.AppendStringer([]byte{}, tt.In)
|
||||
if got, want := string(b), tt.Out; got != want {
|
||||
t.Errorf("AppendStringer(%q)\ngot: %#q, want: %#q", tt.In, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendStringers(t *testing.T) {
|
||||
for _, tt := range internal.EncodeStringersTests {
|
||||
b := enc.AppendStringers([]byte{}, tt.In)
|
||||
if got, want := string(b), tt.Out; got != want {
|
||||
t.Errorf("appendStrings(%q) = %#q, want %#q", tt.In, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
-17
@@ -5,11 +5,29 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Import from zerolog/global.go
|
||||
timeFormatUnix = ""
|
||||
timeFormatUnixMs = "UNIXMS"
|
||||
timeFormatUnixMicro = "UNIXMICRO"
|
||||
timeFormatUnixNano = "UNIXNANO"
|
||||
durationFormatFloat = "float"
|
||||
durationFormatInt = "int"
|
||||
durationFormatString = "string"
|
||||
)
|
||||
|
||||
// AppendTime formats the input time with the given format
|
||||
// and appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendTime(dst []byte, t time.Time, format string) []byte {
|
||||
if format == "" {
|
||||
switch format {
|
||||
case timeFormatUnix:
|
||||
return e.AppendInt64(dst, t.Unix())
|
||||
case timeFormatUnixMs:
|
||||
return e.AppendInt64(dst, t.UnixNano()/1000000)
|
||||
case timeFormatUnixMicro:
|
||||
return e.AppendInt64(dst, t.UnixNano()/1000)
|
||||
case timeFormatUnixNano:
|
||||
return e.AppendInt64(dst, t.UnixNano())
|
||||
}
|
||||
return append(t.AppendFormat(append(dst, '"'), format), '"')
|
||||
}
|
||||
@@ -17,8 +35,15 @@ func (e Encoder) AppendTime(dst []byte, t time.Time, format string) []byte {
|
||||
// AppendTimes converts the input times with the given format
|
||||
// and appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendTimes(dst []byte, vals []time.Time, format string) []byte {
|
||||
if format == "" {
|
||||
switch format {
|
||||
case timeFormatUnix:
|
||||
return appendUnixTimes(dst, vals)
|
||||
case timeFormatUnixMs:
|
||||
return appendUnixNanoTimes(dst, vals, 1000000)
|
||||
case timeFormatUnixMicro:
|
||||
return appendUnixNanoTimes(dst, vals, 1000)
|
||||
case timeFormatUnixNano:
|
||||
return appendUnixNanoTimes(dst, vals, 1)
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
@@ -49,26 +74,49 @@ func appendUnixTimes(dst []byte, vals []time.Time) []byte {
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendDuration formats the input duration with the given unit & format
|
||||
// and appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, useInt bool) []byte {
|
||||
if useInt {
|
||||
return strconv.AppendInt(dst, int64(d/unit), 10)
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit))
|
||||
}
|
||||
|
||||
// AppendDurations formats the input durations with the given unit & format
|
||||
// and appends the encoded string list to the input byte slice.
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool) []byte {
|
||||
func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendDuration(dst, vals[0], unit, useInt)
|
||||
dst = strconv.AppendInt(dst, vals[0].UnixNano()/div, 10)
|
||||
if len(vals) > 1 {
|
||||
for _, d := range vals[1:] {
|
||||
dst = e.AppendDuration(append(dst, ','), d, unit, useInt)
|
||||
for _, t := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), t.UnixNano()/div, 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendDuration formats the input duration with the given unit & format
|
||||
// and appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte {
|
||||
if useInt {
|
||||
return strconv.AppendInt(dst, int64(d/unit), 10)
|
||||
}
|
||||
switch format {
|
||||
case durationFormatFloat:
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), precision)
|
||||
case durationFormatInt:
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
case durationFormatString:
|
||||
return e.AppendString(dst, d.String())
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), precision)
|
||||
}
|
||||
|
||||
// AppendDurations formats the input durations with the given unit & format
|
||||
// and appends the encoded string list to the input byte slice.
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendDuration(dst, vals[0], unit, format, useInt, precision)
|
||||
if len(vals) > 1 {
|
||||
for _, d := range vals[1:] {
|
||||
dst = e.AppendDuration(append(dst, ','), d, unit, format, useInt, precision)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
func TestEncoder_AppendDuration(t *testing.T) {
|
||||
type args struct {
|
||||
dst []byte
|
||||
d time.Duration
|
||||
unit time.Duration
|
||||
format string
|
||||
useInt bool
|
||||
unused int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "useInt",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
useInt: true,
|
||||
},
|
||||
want: []byte{49},
|
||||
},
|
||||
{
|
||||
name: "formatFloat",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
format: durationFormatFloat,
|
||||
},
|
||||
want: []byte{49},
|
||||
},
|
||||
{
|
||||
name: "formatInt",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
format: durationFormatInt,
|
||||
},
|
||||
want: []byte{49},
|
||||
},
|
||||
{
|
||||
name: "formatString",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
format: durationFormatString,
|
||||
},
|
||||
want: []byte{34, 49, 46, 50, 51, 52, 53, 54, 55, 56, 57, 115, 34},
|
||||
},
|
||||
{
|
||||
name: "formatBlank",
|
||||
args: args{
|
||||
d: 1234567890,
|
||||
unit: time.Second,
|
||||
},
|
||||
want: []byte{49},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := Encoder{}
|
||||
if got := e.AppendDuration(tt.args.dst, tt.args.d, tt.args.unit, tt.args.format, tt.args.useInt, tt.args.unused); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AppendDuration() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoder_AppendDurations(t *testing.T) {
|
||||
type args struct {
|
||||
dst []byte
|
||||
vals []time.Duration
|
||||
unit time.Duration
|
||||
format string
|
||||
useInt bool
|
||||
unused int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "useInt",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
useInt: true,
|
||||
},
|
||||
want: []byte{91, 49, 93},
|
||||
},
|
||||
{
|
||||
name: "formatFloat",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
format: durationFormatFloat,
|
||||
},
|
||||
want: []byte{91, 49, 93},
|
||||
},
|
||||
{
|
||||
name: "formatInt",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
format: durationFormatInt,
|
||||
},
|
||||
want: []byte{91, 49, 93},
|
||||
},
|
||||
{
|
||||
name: "formatString",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
format: durationFormatString,
|
||||
},
|
||||
want: []byte{91, 34, 49, 46, 50, 51, 52, 53, 54, 55, 56, 57, 115, 34, 93},
|
||||
},
|
||||
{
|
||||
name: "formatBlank",
|
||||
args: args{
|
||||
vals: []time.Duration{1234567890},
|
||||
unit: time.Second,
|
||||
},
|
||||
want: []byte{91, 49, 93},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := Encoder{}
|
||||
if got := e.AppendDurations(tt.args.dst, tt.args.vals, tt.args.unit, tt.args.format, tt.args.useInt, tt.args.unused); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AppendDurations() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendTimeNow(t *testing.T) {
|
||||
tm := time.Now()
|
||||
got := enc.AppendTime([]byte{}, tm, time.RFC3339)
|
||||
want := tm.AppendFormat([]byte{'"'}, time.RFC3339)
|
||||
want = append(want, '"')
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendTime(%s)\ngot: %s\nwant: %s",
|
||||
"time.Now()",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendTimePastPresentInteger(t *testing.T) {
|
||||
for _, tt := range internal.TimeIntegerTestcases {
|
||||
tin, err := time.Parse(time.RFC3339, tt.Txt)
|
||||
if err != nil {
|
||||
fmt.Println("Cannot parse input", tt.Txt, ".. Skipping!", err)
|
||||
continue
|
||||
}
|
||||
|
||||
got := enc.AppendTime([]byte{}, tin, timeFormatUnix)
|
||||
want := []byte(fmt.Sprintf("%d", tt.UnixInt))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("appendString(%s)\ngot: %s\nwant: %s",
|
||||
tt.Txt,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
got = enc.AppendTime([]byte{}, tin, timeFormatUnixMs)
|
||||
want = []byte(fmt.Sprintf("%d", tt.UnixInt*1000))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("appendString(%s)\ngot: %s\nwant: %s",
|
||||
tt.Txt,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
got = enc.AppendTime([]byte{}, tin, timeFormatUnixMicro)
|
||||
want = []byte(fmt.Sprintf("%d", tt.UnixInt*1000000))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("appendString(%s)\ngot: %s\nwant: %s",
|
||||
tt.Txt,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
got = enc.AppendTime([]byte{}, tin, timeFormatUnixNano)
|
||||
want = []byte(fmt.Sprintf("%d", tt.UnixInt*1000000000))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("appendString(%s)\ngot: %s\nwant: %s",
|
||||
tt.Txt,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendTimePastPresentFloat(t *testing.T) {
|
||||
const timeFloatFmt = "2006-01-02T15:04:05.999999-07:00"
|
||||
for _, tt := range internal.TimeFloatTestcases {
|
||||
tin, err := time.Parse(timeFloatFmt, tt.RfcStr)
|
||||
if err != nil {
|
||||
fmt.Println("Cannot parse input", tt.RfcStr, ".. Skipping!")
|
||||
continue
|
||||
}
|
||||
got := enc.AppendTime([]byte{}, tin, timeFormatUnix)
|
||||
want := []byte(fmt.Sprintf("%d", tt.UnixInt))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("appendString(%s)\ngot: %s\nwant: %s",
|
||||
tt.RfcStr,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAppendTimes(t *testing.T) {
|
||||
doOne := func(multiplier int, format string) {
|
||||
array := make([]time.Time, 0)
|
||||
want := append([]byte{}, '[')
|
||||
want = append(want, ']')
|
||||
got := enc.AppendTimes([]byte{}, array, format)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendTimes(%v)\ngot: %s\nwant: %s",
|
||||
array,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
|
||||
array = make([]time.Time, len(internal.TimeIntegerTestcases))
|
||||
want = append([]byte{}, '[')
|
||||
for i, tt := range internal.TimeIntegerTestcases {
|
||||
if tin, err := time.Parse(time.RFC3339, tt.RfcStr); err != nil {
|
||||
fmt.Println("Cannot parse input", tt.RfcStr, ".. Skipping!")
|
||||
continue
|
||||
} else {
|
||||
array[i] = tin
|
||||
}
|
||||
if multiplier == 0 {
|
||||
want = append(want, '"')
|
||||
formatted := array[i].Format(format)
|
||||
want = append(want, []byte(fmt.Sprintf("%v", formatted))...)
|
||||
want = append(want, '"')
|
||||
} else {
|
||||
scaled := tt.UnixInt * multiplier
|
||||
want = append(want, []byte(fmt.Sprintf("%d", scaled))...)
|
||||
}
|
||||
if i < len(internal.TimeIntegerTestcases)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got = enc.AppendTimes([]byte{}, array, format)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendTimes(%v) %d %s\ngot: %s\nwant: %s",
|
||||
array, multiplier, format,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
doOne(0, time.RFC3339)
|
||||
doOne(1, timeFormatUnix)
|
||||
doOne(1000, timeFormatUnixMs)
|
||||
doOne(1000000, timeFormatUnixMicro)
|
||||
doOne(1000000000, timeFormatUnixNano)
|
||||
}
|
||||
|
||||
func TestAppendDurationFloat(t *testing.T) {
|
||||
for _, tt := range internal.DurTestcases {
|
||||
dur := tt.Duration
|
||||
want := []byte{}
|
||||
want = append(want, []byte(fmt.Sprintf("%v", dur.Microseconds()))...)
|
||||
got := enc.AppendDuration([]byte{}, dur, time.Microsecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDuration(%v)=\ngot: %s\nwant: %s",
|
||||
dur,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
|
||||
want = []byte{}
|
||||
fraction := float64(dur) / float64(time.Millisecond)
|
||||
want = append(want, []byte(fmt.Sprintf("%v", fraction))...)
|
||||
got = enc.AppendDuration([]byte{}, dur, time.Millisecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDuration(%v)=\ngot: %s\nwant: %s",
|
||||
dur,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAppendDurationInteger(t *testing.T) {
|
||||
for _, tt := range internal.DurTestcases {
|
||||
dur := tt.Duration
|
||||
want := []byte{}
|
||||
whole := int(dur) / int(time.Microsecond)
|
||||
want = append(want, []byte(fmt.Sprintf("%v", whole))...)
|
||||
got := enc.AppendDuration([]byte{}, dur, time.Microsecond, "", true, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDuration(%v)=\ngot: %s\nwant: %s",
|
||||
dur,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAppendDurations(t *testing.T) {
|
||||
array := make([]time.Duration, len(internal.DurTestcases))
|
||||
want := make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
for i, tt := range internal.DurTestcases {
|
||||
array[i] = tt.Duration
|
||||
whole := int(tt.Duration) / int(time.Microsecond)
|
||||
want = append(want, []byte(fmt.Sprintf("%v", whole))...)
|
||||
if i < len(internal.DurTestcases)-1 {
|
||||
want = append(want, ',')
|
||||
}
|
||||
}
|
||||
want = append(want, ']')
|
||||
|
||||
got := enc.AppendDurations([]byte{}, array, time.Microsecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDurations(%v)\ngot: %s\nwant: %s",
|
||||
array,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
|
||||
// now empty array case
|
||||
array = make([]time.Duration, 0)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, '[')
|
||||
want = append(want, ']')
|
||||
got = enc.AppendDurations([]byte{}, array, time.Microsecond, "", false, -1)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendDurations(%v)\ngot: %s\nwant: %s",
|
||||
array,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendTime(b *testing.B) {
|
||||
tests := map[string]string{
|
||||
"Integer": "Feb 3, 2013 at 7:54pm (PST)",
|
||||
"Float": "2006-01-02T15:04:05.999999-08:00",
|
||||
}
|
||||
const timeFloatFmt = "2006-01-02T15:04:05.999999-07:00"
|
||||
|
||||
for name, str := range tests {
|
||||
t, err := time.Parse(time.RFC3339, str)
|
||||
if err != nil {
|
||||
t, _ = time.Parse(timeFloatFmt, str)
|
||||
}
|
||||
b.Run(name, func(b *testing.B) {
|
||||
buf := make([]byte, 0, 100)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = enc.AppendTime(buf, t, "unused")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+91
-27
@@ -1,10 +1,10 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
@@ -279,7 +279,7 @@ func (Encoder) AppendUints32(dst []byte, vals []uint32) []byte {
|
||||
// AppendUint64 converts the input uint64 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendUint64(dst []byte, val uint64) []byte {
|
||||
return strconv.AppendUint(dst, uint64(val), 10)
|
||||
return strconv.AppendUint(dst, val, 10)
|
||||
}
|
||||
|
||||
// AppendUints64 encodes the input uint64s to json and
|
||||
@@ -299,9 +299,9 @@ func (Encoder) AppendUints64(dst []byte, vals []uint64) []byte {
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendFloat(dst []byte, val float64, bitSize int) []byte {
|
||||
func appendFloat(dst []byte, val float64, bitSize, precision int) []byte {
|
||||
// JSON does not permit NaN or Infinity. A typical JSON encoder would fail
|
||||
// with an error, but a logging library wants the data to get thru so we
|
||||
// with an error, but a logging library wants the data to get through so we
|
||||
// make a tradeoff and store those types as string.
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
@@ -311,26 +311,47 @@ func appendFloat(dst []byte, val float64, bitSize int) []byte {
|
||||
case math.IsInf(val, -1):
|
||||
return append(dst, `"-Inf"`...)
|
||||
}
|
||||
return strconv.AppendFloat(dst, val, 'f', -1, bitSize)
|
||||
// convert as if by es6 number to string conversion
|
||||
// see also https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/encoding/json/encode.go;l=573
|
||||
strFmt := byte('f')
|
||||
// If precision is set to a value other than -1, we always just format the float using that precision.
|
||||
if precision == -1 {
|
||||
// Use float32 comparisons for underlying float32 value to get precise cutoffs right.
|
||||
if abs := math.Abs(val); abs != 0 {
|
||||
if bitSize == 64 && (abs < 1e-6 || abs >= 1e21) || bitSize == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
|
||||
strFmt = 'e'
|
||||
}
|
||||
}
|
||||
}
|
||||
dst = strconv.AppendFloat(dst, val, strFmt, precision, bitSize)
|
||||
if strFmt == 'e' {
|
||||
// Clean up e-09 to e-9
|
||||
n := len(dst)
|
||||
if n >= 4 && dst[n-4] == 'e' && dst[n-3] == '-' && dst[n-2] == '0' {
|
||||
dst[n-2] = dst[n-1]
|
||||
dst = dst[:n-1]
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendFloat32 converts the input float32 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendFloat32(dst []byte, val float32) []byte {
|
||||
return appendFloat(dst, float64(val), 32)
|
||||
func (Encoder) AppendFloat32(dst []byte, val float32, precision int) []byte {
|
||||
return appendFloat(dst, float64(val), 32, precision)
|
||||
}
|
||||
|
||||
// AppendFloats32 encodes the input float32s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
|
||||
func (Encoder) AppendFloats32(dst []byte, vals []float32, precision int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = appendFloat(dst, float64(vals[0]), 32)
|
||||
dst = appendFloat(dst, float64(vals[0]), 32, precision)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = appendFloat(append(dst, ','), float64(val), 32)
|
||||
dst = appendFloat(append(dst, ','), float64(val), 32, precision)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
@@ -339,21 +360,21 @@ func (Encoder) AppendFloats32(dst []byte, vals []float32) []byte {
|
||||
|
||||
// AppendFloat64 converts the input float64 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendFloat64(dst []byte, val float64) []byte {
|
||||
return appendFloat(dst, val, 64)
|
||||
func (Encoder) AppendFloat64(dst []byte, val float64, precision int) []byte {
|
||||
return appendFloat(dst, val, 64, precision)
|
||||
}
|
||||
|
||||
// AppendFloats64 encodes the input float64s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendFloats64(dst []byte, vals []float64) []byte {
|
||||
func (Encoder) AppendFloats64(dst []byte, vals []float64, precision int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = appendFloat(dst, vals[0], 32)
|
||||
dst = appendFloat(dst, vals[0], 64, precision)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = appendFloat(append(dst, ','), val, 64)
|
||||
dst = appendFloat(append(dst, ','), val, 64, precision)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
@@ -363,40 +384,83 @@ func (Encoder) AppendFloats64(dst []byte, vals []float64) []byte {
|
||||
// AppendInterface marshals the input interface to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte {
|
||||
marshaled, err := json.Marshal(i)
|
||||
marshaled, err := JSONMarshalFunc(i)
|
||||
if err != nil {
|
||||
return e.AppendString(dst, fmt.Sprintf("marshaling error: %v", err))
|
||||
}
|
||||
return append(dst, marshaled...)
|
||||
}
|
||||
|
||||
// AppendType appends the parameter type (as a string) to the input byte slice.
|
||||
func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
|
||||
if i == nil {
|
||||
return e.AppendString(dst, "<nil>")
|
||||
}
|
||||
return e.AppendString(dst, reflect.TypeOf(i).String())
|
||||
}
|
||||
|
||||
// AppendObjectData takes in an object that is already in a byte array
|
||||
// and adds it to the dst.
|
||||
func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
|
||||
// Two conditions we want to put a ',' between existing content and
|
||||
// new content:
|
||||
// 1. new content starts with '{' - which shd be dropped OR
|
||||
// 2. existing content has already other fields
|
||||
// Three conditions apply here:
|
||||
// 1. new content starts with '{' - which should be dropped OR
|
||||
// 2. new content starts with '{' - which should be replaced with ','
|
||||
// to separate with existing content OR
|
||||
// 3. existing content has already other fields
|
||||
if o[0] == '{' {
|
||||
o[0] = ','
|
||||
if len(dst) > 1 {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
o = o[1:]
|
||||
} else if len(dst) > 1 {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
return append(dst, o...)
|
||||
}
|
||||
|
||||
// AppendIPAddr adds IPv4 or IPv6 address to dst.
|
||||
// AppendIPAddr adds a net.IP IPv4 or IPv6 address to dst.
|
||||
func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
|
||||
return e.AppendString(dst, ip.String())
|
||||
}
|
||||
|
||||
// AppendIPPrefix adds IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
return e.AppendString(dst, pfx.String())
|
||||
|
||||
// AppendIPAddrs adds a []net.IP array of IPv4 or IPv6 address to dst.
|
||||
func (e Encoder) AppendIPAddrs(dst []byte, ips []net.IP) []byte {
|
||||
if len(ips) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendString(dst, ips[0].String())
|
||||
if len(ips) > 1 {
|
||||
for _, ip := range ips[1:] {
|
||||
dst = e.AppendString(append(dst, ','), ip.String())
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr adds MAC address to dst.
|
||||
// AppendIPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
return e.AppendString(dst, pfx.String())
|
||||
}
|
||||
|
||||
// AppendIPPrefixes adds a []net.IPNet array of IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefixes(dst []byte, pfxs []net.IPNet) []byte {
|
||||
if len(pfxs) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendString(dst, pfxs[0].String())
|
||||
if len(pfxs) > 1 {
|
||||
for _, pfx := range pfxs[1:] {
|
||||
dst = e.AppendString(append(dst, ','), pfx.String())
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr adds a net.HardwareAddr MAC address to dst.
|
||||
func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte {
|
||||
return e.AppendString(dst, ha.String())
|
||||
}
|
||||
|
||||
+295
-18
@@ -1,26 +1,157 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog/internal"
|
||||
)
|
||||
|
||||
func TestAppendNil(t *testing.T) {
|
||||
got := enc.AppendNil([]byte{})
|
||||
want := []byte(`null`)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendNil() = %s, want: %s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
func TestAppendBeginMarker(t *testing.T) {
|
||||
got := enc.AppendBeginMarker([]byte{})
|
||||
want := []byte(`{`)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendBeginMarker()\ngot: %s, want: %s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
func TestAppendEndMarker(t *testing.T) {
|
||||
got := enc.AppendEndMarker([]byte{})
|
||||
want := []byte(`}`)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendEndMarker()\ngot: %s, want: %s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
func TestAppendArrayStart(t *testing.T) {
|
||||
got := enc.AppendArrayStart([]byte{})
|
||||
want := []byte("[")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendArrayStart() = %s, want: %s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
func TestAppendArrayEnd(t *testing.T) {
|
||||
got := enc.AppendArrayEnd([]byte{})
|
||||
want := []byte("]")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendArrayEnd() = %s, want: %s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
func TestAppendArrayDelim(t *testing.T) {
|
||||
got := enc.AppendArrayDelim([]byte{})
|
||||
want := []byte("")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendArrayDelim() = 0x%s, want: 0x%s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
|
||||
got = enc.AppendArrayDelim([]byte("a"))
|
||||
want = []byte("a,")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendArrayDelim() = 0x%s, want: 0x%s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
func TestAppendLineBreak(t *testing.T) {
|
||||
got := enc.AppendLineBreak([]byte{})
|
||||
want := []byte("\n")
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendLineBreak() = 0x%s, want: 0x%s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
// inline copy from globals.go of InterfaceMarshalFunc used in tests to avoid import cycle
|
||||
func interfaceMarshalFunc(v interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := buf.Bytes()
|
||||
if len(b) > 0 {
|
||||
// Remove trailing \n which is added by Encode.
|
||||
return b[:len(b)-1], nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func TestAppendInterface(t *testing.T) {
|
||||
oldJSONMarshalFunc := JSONMarshalFunc
|
||||
defer func() {
|
||||
JSONMarshalFunc = oldJSONMarshalFunc
|
||||
}()
|
||||
|
||||
JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return interfaceMarshalFunc(v)
|
||||
}
|
||||
|
||||
var i int = 17
|
||||
got := enc.AppendInterface([]byte{}, i)
|
||||
want := make([]byte, 0)
|
||||
want = append(want, []byte("17")...) // of type interface, two characters
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInterface\ngot: 0x%s\nwant: 0x%s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
|
||||
JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return nil, errors.New("test")
|
||||
}
|
||||
|
||||
got = enc.AppendInterface([]byte{}, nil)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, []byte("\"marshaling error: test\"")...) // of type interface, two characters
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendInterface\ngot: 0x%s\nwant: 0x%s",
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendType(t *testing.T) {
|
||||
w := map[string]func(interface{}) []byte{
|
||||
"AppendInt": func(v interface{}) []byte { return enc.AppendInt([]byte{}, v.(int)) },
|
||||
"AppendInt8": func(v interface{}) []byte { return enc.AppendInt8([]byte{}, v.(int8)) },
|
||||
"AppendInt16": func(v interface{}) []byte { return enc.AppendInt16([]byte{}, v.(int16)) },
|
||||
"AppendInt32": func(v interface{}) []byte { return enc.AppendInt32([]byte{}, v.(int32)) },
|
||||
"AppendInt64": func(v interface{}) []byte { return enc.AppendInt64([]byte{}, v.(int64)) },
|
||||
"AppendUint": func(v interface{}) []byte { return enc.AppendUint([]byte{}, v.(uint)) },
|
||||
"AppendUint8": func(v interface{}) []byte { return enc.AppendUint8([]byte{}, v.(uint8)) },
|
||||
"AppendUint16": func(v interface{}) []byte { return enc.AppendUint16([]byte{}, v.(uint16)) },
|
||||
"AppendUint32": func(v interface{}) []byte { return enc.AppendUint32([]byte{}, v.(uint32)) },
|
||||
"AppendUint64": func(v interface{}) []byte { return enc.AppendUint64([]byte{}, v.(uint64)) },
|
||||
"AppendFloat32": func(v interface{}) []byte { return enc.AppendFloat32([]byte{}, v.(float32)) },
|
||||
"AppendFloat64": func(v interface{}) []byte { return enc.AppendFloat64([]byte{}, v.(float64)) },
|
||||
"AppendInt": func(v interface{}) []byte { return enc.AppendInt([]byte{}, v.(int)) },
|
||||
"AppendInt8": func(v interface{}) []byte { return enc.AppendInt8([]byte{}, v.(int8)) },
|
||||
"AppendInt16": func(v interface{}) []byte { return enc.AppendInt16([]byte{}, v.(int16)) },
|
||||
"AppendInt32": func(v interface{}) []byte { return enc.AppendInt32([]byte{}, v.(int32)) },
|
||||
"AppendInt64": func(v interface{}) []byte { return enc.AppendInt64([]byte{}, v.(int64)) },
|
||||
"AppendUint": func(v interface{}) []byte { return enc.AppendUint([]byte{}, v.(uint)) },
|
||||
"AppendUint8": func(v interface{}) []byte { return enc.AppendUint8([]byte{}, v.(uint8)) },
|
||||
"AppendUint16": func(v interface{}) []byte { return enc.AppendUint16([]byte{}, v.(uint16)) },
|
||||
"AppendUint32": func(v interface{}) []byte { return enc.AppendUint32([]byte{}, v.(uint32)) },
|
||||
"AppendUint64": func(v interface{}) []byte { return enc.AppendUint64([]byte{}, v.(uint64)) },
|
||||
"AppendFloat32": func(v interface{}) []byte { return enc.AppendFloat32([]byte{}, v.(float32), -1) },
|
||||
"AppendFloat64": func(v interface{}) []byte { return enc.AppendFloat64([]byte{}, v.(float64), -1) },
|
||||
"AppendFloat32SmallPrecision": func(v interface{}) []byte { return enc.AppendFloat32([]byte{}, v.(float32), 1) },
|
||||
"AppendFloat64SmallPrecision": func(v interface{}) []byte { return enc.AppendFloat64([]byte{}, v.(float64), 1) },
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -44,7 +175,7 @@ func TestAppendType(t *testing.T) {
|
||||
{"AppendFloat32(0)", "AppendFloat32", float32(0), []byte(`0`)},
|
||||
{"AppendFloat32(-1.1)", "AppendFloat32", float32(-1.1), []byte(`-1.1`)},
|
||||
{"AppendFloat32(1e20)", "AppendFloat32", float32(1e20), []byte(`100000000000000000000`)},
|
||||
{"AppendFloat32(1e21)", "AppendFloat32", float32(1e21), []byte(`1000000000000000000000`)},
|
||||
{"AppendFloat32(1e21)", "AppendFloat32", float32(1e21), []byte(`1e+21`)},
|
||||
|
||||
{"AppendFloat64(-Inf)", "AppendFloat64", float64(math.Inf(-1)), []byte(`"-Inf"`)},
|
||||
{"AppendFloat64(+Inf)", "AppendFloat64", float64(math.Inf(1)), []byte(`"+Inf"`)},
|
||||
@@ -52,7 +183,10 @@ func TestAppendType(t *testing.T) {
|
||||
{"AppendFloat64(0)", "AppendFloat64", float64(0), []byte(`0`)},
|
||||
{"AppendFloat64(-1.1)", "AppendFloat64", float64(-1.1), []byte(`-1.1`)},
|
||||
{"AppendFloat64(1e20)", "AppendFloat64", float64(1e20), []byte(`100000000000000000000`)},
|
||||
{"AppendFloat64(1e21)", "AppendFloat64", float64(1e21), []byte(`1000000000000000000000`)},
|
||||
{"AppendFloat64(1e21)", "AppendFloat64", float64(1e21), []byte(`1e+21`)},
|
||||
|
||||
{"AppendFloat32SmallPrecision(-1.123)", "AppendFloat32SmallPrecision", float32(-1.123), []byte(`-1.1`)},
|
||||
{"AppendFloat64SmallPrecision(-1.123)", "AppendFloat64SmallPrecision", float64(-1.123), []byte(`-1.1`)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -63,7 +197,7 @@ func TestAppendType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_appendMAC(t *testing.T) {
|
||||
func TestAppendMAC(t *testing.T) {
|
||||
MACtests := []struct {
|
||||
input string
|
||||
want []byte
|
||||
@@ -81,7 +215,70 @@ func Test_appendMAC(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_appendIP(t *testing.T) {
|
||||
func TestAppendBool(t *testing.T) {
|
||||
for _, tc := range internal.BooleanTestCases {
|
||||
s := enc.AppendBool([]byte{}, tc.Val)
|
||||
got := string(s)
|
||||
if got != tc.Json {
|
||||
t.Errorf("AppendBool(%s)=0x%s, want: 0x%s",
|
||||
tc.Json,
|
||||
string(s),
|
||||
string([]byte(tc.Binary)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendBoolArray(t *testing.T) {
|
||||
for _, tc := range internal.BooleanArrayTestCases {
|
||||
s := enc.AppendBools([]byte{}, tc.Val)
|
||||
got := string(s)
|
||||
if got != tc.Json {
|
||||
t.Errorf("AppendBools(%s)=0x%s, want: 0x%s",
|
||||
tc.Json,
|
||||
hex.EncodeToString(s),
|
||||
hex.EncodeToString([]byte(tc.Binary)))
|
||||
}
|
||||
}
|
||||
|
||||
// now empty array case
|
||||
array := make([]bool, 0)
|
||||
want := make([]byte, 0)
|
||||
want = append(want, []byte("[]")...) // start and end array
|
||||
got := enc.AppendBools([]byte{}, array)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendBools(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array,
|
||||
hex.EncodeToString(got),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
// now a large array case
|
||||
array = make([]bool, 24)
|
||||
want = make([]byte, 0)
|
||||
want = append(want, []byte("[")...) // start a large array
|
||||
for i := 0; i < 24; i++ {
|
||||
array[i] = bool(i%2 == 1)
|
||||
if array[i] {
|
||||
want = append(want, []byte("true")...)
|
||||
} else {
|
||||
want = append(want, []byte("false")...)
|
||||
}
|
||||
if (i + 1) < 24 {
|
||||
want = append(want, []byte(",")...)
|
||||
}
|
||||
}
|
||||
want = append(want, []byte("]")...) // end a large array
|
||||
|
||||
got = enc.AppendBools([]byte{}, array)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("AppendBools(%v)\ngot: 0x%s\nwant: 0x%s",
|
||||
array,
|
||||
string(got),
|
||||
string(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendIP(t *testing.T) {
|
||||
IPv4tests := []struct {
|
||||
input net.IP
|
||||
want []byte
|
||||
@@ -114,7 +311,45 @@ func Test_appendIP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_appendIPPrefix(t *testing.T) {
|
||||
var IPAddrArrayTestCases = []struct {
|
||||
input []net.IP
|
||||
want []byte
|
||||
}{
|
||||
{[]net.IP{}, []byte(`[]`)},
|
||||
{[]net.IP{{127, 0, 0, 0}}, []byte(`["127.0.0.0"]`)},
|
||||
{[]net.IP{{0, 0, 0, 0}, {192, 168, 0, 100}}, []byte(`["0.0.0.0","192.168.0.100"]`)},
|
||||
}
|
||||
|
||||
func TestAppendIPAddrs(t *testing.T) {
|
||||
for _, tt := range IPAddrArrayTestCases {
|
||||
t.Run("IPAddrs", func(t *testing.T) {
|
||||
if got := enc.AppendIPAddrs([]byte{}, tt.input); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("appendIPAddr() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var IPPrefixArrayTestCases = []struct {
|
||||
input []net.IPNet
|
||||
want []byte
|
||||
}{
|
||||
{[]net.IPNet{}, []byte(`[]`)},
|
||||
{[]net.IPNet{{IP: net.IP{127, 0, 0, 0}, Mask: net.CIDRMask(24, 32)}}, []byte(`["127.0.0.0/24"]`)},
|
||||
{[]net.IPNet{{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(0, 32)}, {IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}}, []byte(`["0.0.0.0/0","192.168.0.100/24"]`)},
|
||||
}
|
||||
|
||||
func TestAppendIPPrefixes(t *testing.T) {
|
||||
for _, tt := range IPPrefixArrayTestCases {
|
||||
t.Run("IPPrefixes", func(t *testing.T) {
|
||||
if got := enc.AppendIPPrefixes([]byte{}, tt.input); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("appendIPAddr() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendIPPrefix(t *testing.T) {
|
||||
IPv4Prefixtests := []struct {
|
||||
input net.IPNet
|
||||
want []byte
|
||||
@@ -148,7 +383,7 @@ func Test_appendIPPrefix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_appendMac(t *testing.T) {
|
||||
func TestAppendMACAddr(t *testing.T) {
|
||||
MACtests := []struct {
|
||||
input net.HardwareAddr
|
||||
want []byte
|
||||
@@ -165,3 +400,45 @@ func Test_appendMac(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendType2(t *testing.T) {
|
||||
typeTests := []struct {
|
||||
label string
|
||||
input interface{}
|
||||
want []byte
|
||||
}{
|
||||
{"int", 42, []byte(`"int"`)},
|
||||
{"MAC", net.HardwareAddr{0x12, 0x34, 0x00, 0x00, 0x90, 0xab}, []byte(`"net.HardwareAddr"`)},
|
||||
{"float64", float64(2.50), []byte(`"float64"`)},
|
||||
{"nil", nil, []byte(`"<nil>"`)},
|
||||
{"bool", true, []byte(`"bool"`)},
|
||||
}
|
||||
|
||||
for _, tt := range typeTests {
|
||||
t.Run(tt.label, func(t *testing.T) {
|
||||
if got := enc.AppendType([]byte{}, tt.input); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("appendType() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendObjectData(t *testing.T) {
|
||||
tests := []struct {
|
||||
dst []byte
|
||||
obj []byte
|
||||
want []byte
|
||||
}{
|
||||
{[]byte{}, []byte(`{"foo":"bar"}`), []byte(`"foo":"bar"}`)},
|
||||
{[]byte(`{"qux":"quz"`), []byte(`{"foo":"bar"}`), []byte(`{"qux":"quz","foo":"bar"}`)},
|
||||
{[]byte{}, []byte(`"foo":"bar"`), []byte(`"foo":"bar"`)},
|
||||
{[]byte(`{"qux":"quz"`), []byte(`"foo":"bar"`), []byte(`{"qux":"quz","foo":"bar"`)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run("ObjectData", func(t *testing.T) {
|
||||
if got := enc.AppendObjectData(tt.dst, tt.obj); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("appendObjectData() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
var BooleanTestCases = []struct {
|
||||
Val bool
|
||||
Binary string
|
||||
Json string
|
||||
}{
|
||||
{true, "\xf5", "true"},
|
||||
{false, "\xf4", "false"},
|
||||
}
|
||||
|
||||
var BooleanArrayTestCases = []struct {
|
||||
Val []bool
|
||||
Binary string
|
||||
Json string
|
||||
}{
|
||||
{[]bool{}, "\x9f\xff", "[]"},
|
||||
{[]bool{false}, "\x81\xf4", "[false]"},
|
||||
{[]bool{true, false, true}, "\x83\xf5\xf4\xf5", "[true,false,true]"},
|
||||
{[]bool{true, false, false, true, false, true}, "\x86\xf5\xf4\xf4\xf5\xf4\xf5", "[true,false,false,true,false,true]"},
|
||||
}
|
||||
|
||||
var IntegerTestCases = []struct {
|
||||
Val int
|
||||
Binary string
|
||||
}{
|
||||
// Value included in the type.
|
||||
{0, "\x00"},
|
||||
{1, "\x01"},
|
||||
{2, "\x02"},
|
||||
{3, "\x03"},
|
||||
{8, "\x08"},
|
||||
{9, "\x09"},
|
||||
{10, "\x0a"},
|
||||
{22, "\x16"},
|
||||
{23, "\x17"},
|
||||
|
||||
// Value in 1 byte.
|
||||
{24, "\x18\x18"},
|
||||
{25, "\x18\x19"},
|
||||
{26, "\x18\x1a"},
|
||||
{127, "\x18\x7f"},
|
||||
{254, "\x18\xfe"},
|
||||
{255, "\x18\xff"},
|
||||
|
||||
// Value in 2 bytes.
|
||||
{256, "\x19\x01\x00"},
|
||||
{257, "\x19\x01\x01"},
|
||||
{1000, "\x19\x03\xe8"},
|
||||
{0xFFFF, "\x19\xff\xff"},
|
||||
|
||||
// Value in 4 bytes.
|
||||
{0x10000, "\x1a\x00\x01\x00\x00"},
|
||||
{0x7FFFFFFE, "\x1a\x7f\xff\xff\xfe"},
|
||||
{1000000, "\x1a\x00\x0f\x42\x40"},
|
||||
|
||||
// Negative number test cases.
|
||||
// Value included in the type.
|
||||
{-1, "\x20"},
|
||||
{-2, "\x21"},
|
||||
{-3, "\x22"},
|
||||
{-10, "\x29"},
|
||||
{-21, "\x34"},
|
||||
{-22, "\x35"},
|
||||
{-23, "\x36"},
|
||||
{-24, "\x37"},
|
||||
|
||||
// Value in 1 byte.
|
||||
{-25, "\x38\x18"},
|
||||
{-26, "\x38\x19"},
|
||||
{-100, "\x38\x63"},
|
||||
{-128, "\x38\x7f"},
|
||||
{-254, "\x38\xfd"},
|
||||
{-255, "\x38\xfe"},
|
||||
{-256, "\x38\xff"},
|
||||
|
||||
// Value in 2 bytes.
|
||||
{-257, "\x39\x01\x00"},
|
||||
{-258, "\x39\x01\x01"},
|
||||
{-1000, "\x39\x03\xe7"},
|
||||
|
||||
// Value in 4 bytes.
|
||||
{-0x10001, "\x3a\x00\x01\x00\x00"},
|
||||
{-0x7FFFFFFE, "\x3a\x7f\xff\xff\xfd"},
|
||||
{-1000000, "\x3a\x00\x0f\x42\x3f"},
|
||||
|
||||
//Constants
|
||||
{math.MaxInt8, "\x18\x7f"},
|
||||
{math.MinInt8, "\x38\x7f"},
|
||||
{math.MaxInt16, "\x19\x7f\xff"},
|
||||
{math.MinInt16, "\x39\x7f\xff"},
|
||||
{math.MaxInt32, "\x1a\x7f\xff\xff\xff"},
|
||||
{math.MinInt32, "\x3a\x7f\xff\xff\xff"},
|
||||
{math.MaxInt64, "\x1b\x7f\xff\xff\xff\xff\xff\xff\xff"},
|
||||
{math.MinInt64, "\x3b\x7f\xff\xff\xff\xff\xff\xff\xff"},
|
||||
}
|
||||
|
||||
type UnsignedIntTestCase struct {
|
||||
Val uint
|
||||
Binary string
|
||||
Bigbinary string
|
||||
}
|
||||
|
||||
var AdditionalUnsignedIntegerTestCases = []UnsignedIntTestCase{
|
||||
{0x7FFFFFFF, "\x18\xff", "\x1a\x7f\xff\xff\xff"},
|
||||
{0x80000000, "\x19\xff\xff", "\x1a\x80\x00\x00\x00"},
|
||||
{1000000, "\x1b\x80\x00\x00\x00\x00\x00\x00\x00", "\x1a\x00\x0f\x42\x40"},
|
||||
|
||||
//Constants
|
||||
{math.MaxUint8, "\x18\xff", "\x18\xff"},
|
||||
{math.MaxUint16, "\x19\xff\xff", "\x19\xff\xff"},
|
||||
{math.MaxUint32, "\x1a\xff\xff\xff\xff", "\x1a\xff\xff\xff\xff"},
|
||||
{math.MaxUint64, "\x1b\xff\xff\xff\xff\xff\xff\xff\xff", "\x1b\xff\xff\xff\xff\xff\xff\xff\xff"},
|
||||
}
|
||||
|
||||
func unsignedIntegerTestCases() []UnsignedIntTestCase {
|
||||
size := len(IntegerTestCases) + len(AdditionalUnsignedIntegerTestCases)
|
||||
cases := make([]UnsignedIntTestCase, 0, size)
|
||||
cases = append(cases, AdditionalUnsignedIntegerTestCases...)
|
||||
for _, itc := range IntegerTestCases {
|
||||
if itc.Val < 0 {
|
||||
continue
|
||||
}
|
||||
cases = append(cases, UnsignedIntTestCase{Val: uint(itc.Val), Binary: itc.Binary, Bigbinary: itc.Binary})
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
var UnsignedIntegerTestCases = unsignedIntegerTestCases()
|
||||
|
||||
var Float32TestCases = []struct {
|
||||
Val float32
|
||||
Binary string
|
||||
}{
|
||||
{0.0, "\xfa\x00\x00\x00\x00"},
|
||||
{-0.0, "\xfa\x00\x00\x00\x00"},
|
||||
{1.0, "\xfa\x3f\x80\x00\x00"},
|
||||
{1.5, "\xfa\x3f\xc0\x00\x00"},
|
||||
{65504.0, "\xfa\x47\x7f\xe0\x00"},
|
||||
{-4.0, "\xfa\xc0\x80\x00\x00"},
|
||||
{0.00006103515625, "\xfa\x38\x80\x00\x00"},
|
||||
{float32(math.Inf(0)), "\xfa\x7f\x80\x00\x00"},
|
||||
{float32(math.Inf(-1)), "\xfa\xff\x80\x00\x00"},
|
||||
{float32(math.NaN()), "\xfa\x7f\xc0\x00\x00"},
|
||||
{math.SmallestNonzeroFloat32, "\xfa\x00\x00\x00\x01"},
|
||||
{math.MaxFloat32, "\xfa\x7f\x7f\xff\xff"},
|
||||
}
|
||||
|
||||
var Float64TestCases = []struct {
|
||||
Val float64
|
||||
Binary string
|
||||
}{
|
||||
{0.0, "\xfa\x00\x00\x00\x00"},
|
||||
{-0.0, "\xfa\x00\x00\x00\x00"},
|
||||
{1.0, "\xfa\x3f\x80\x00\x00"},
|
||||
{1.5, "\xfa\x3f\xc0\x00\x00"},
|
||||
{65504.0, "\xfa\x47\x7f\xe0\x00"},
|
||||
{-4.0, "\xfa\xc0\x80\x00\x00"},
|
||||
{0.00006103515625, "\xfa\x38\x80\x00\x00"},
|
||||
{math.Inf(0), "\xfa\x7f\x80\x00\x00\x00\x00\x00\x00"},
|
||||
{math.Inf(-1), "\xfa\xff\x80\x00\x00\x00\x00\x00\x00"},
|
||||
{math.NaN(), "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00"},
|
||||
{math.SmallestNonzeroFloat64, "\xfa\x00\x00\x00\x00\x00\x00\x00\x01"},
|
||||
{math.MaxFloat64, "\xfa\x7f\x7f\xff\xff"},
|
||||
}
|
||||
|
||||
var IntegerArrayTestCases = []struct {
|
||||
Val []int
|
||||
Binary string
|
||||
Json string
|
||||
}{
|
||||
{[]int{}, "\x9f\xff", "[]"},
|
||||
{[]int{32768}, "\x81\x19\x80\x00", "[32768]"},
|
||||
{[]int{-1, 0, 200, 20}, "\x84\x20\x00\x18\xc8\x14", "[-1,0,200,20]"},
|
||||
{[]int{-200, -10, 200, 400}, "\x84\x38\xc7\x29\x18\xc8\x19\x01\x90", "[-200,-10,200,400]"},
|
||||
{[]int{1, 2, 3}, "\x83\x01\x02\x03", "[1,2,3]"},
|
||||
{[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25},
|
||||
"\x98\x19\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x18\x18\x19",
|
||||
"[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]"},
|
||||
}
|
||||
|
||||
var IpAddrTestCases = []struct {
|
||||
Ipaddr net.IP
|
||||
Text string
|
||||
Binary string
|
||||
}{
|
||||
{net.IP{10, 0, 0, 1}, "\"10.0.0.1\"", "\xd9\x01\x04\x44\x0a\x00\x00\x01"},
|
||||
{net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0, 0x0, 0x0, 0x0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34},
|
||||
"\"2001:db8:85a3::8a2e:370:7334\"",
|
||||
"\xd9\x01\x04\x50\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34"},
|
||||
}
|
||||
|
||||
var IPAddrArrayTestCases = []struct {
|
||||
Val []net.IP
|
||||
Binary string
|
||||
Json string
|
||||
}{
|
||||
{[]net.IP{}, "\x9f\xff", "[]"},
|
||||
{[]net.IP{{127, 0, 0, 0}}, "\x81\xd9\x01\x04\x44\x7f\x00\x00\x00", "[127.0.0.0]"},
|
||||
{[]net.IP{{0, 0, 0, 0}, {192, 168, 0, 100}}, "\x82\xd9\x01\x04\x44\x00\x00\x00\x00\xd9\x01\x04\x44\xc0\xa8\x00\x64", "[0.0.0.0,192.168.0.100]"},
|
||||
}
|
||||
|
||||
var IPPrefixTestCases = []struct {
|
||||
Pfx net.IPNet
|
||||
Text string // ASCII representation of pfx
|
||||
Binary string // CBOR representation of pfx
|
||||
}{
|
||||
{net.IPNet{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(0, 32)}, "\"0.0.0.0/0\"", "\xd9\x01\x05\xa1\x44\x00\x00\x00\x00\x00"},
|
||||
{net.IPNet{IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}, "\"192.168.0.100/24\"",
|
||||
"\xd9\x01\x05\xa1\x44\xc0\xa8\x00\x64\x18\x18"},
|
||||
{net.IPNet{IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Mask: net.CIDRMask(128, 128)}, "\"::1/128\"",
|
||||
"\xd9\x01\x05\xa1\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x18\x80"},
|
||||
}
|
||||
|
||||
var IPPrefixArrayTestCases = []struct {
|
||||
Val []net.IPNet
|
||||
Binary string
|
||||
Json string
|
||||
}{
|
||||
{[]net.IPNet{}, "\x9f\xff", "[]"},
|
||||
{[]net.IPNet{{IP: net.IP{127, 0, 0, 0}, Mask: net.CIDRMask(24, 32)}}, "\x81\xd9\x01\x05\xa1\x44\x7f\x00\x00\x00\x18\x18", "[127.0.0.0/24]"},
|
||||
{[]net.IPNet{{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(0, 32)}, {IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}}, "\x82\xd9\x01\x05\xa1\x44\x00\x00\x00\x00\x00\xd9\x01\x05\xa1\x44\xc0\xa8\x00\x64\x18\x18", "[0.0.0.0/0,192.168.0.100/24]"},
|
||||
}
|
||||
|
||||
var MacAddrTestCases = []struct {
|
||||
Macaddr net.HardwareAddr
|
||||
Text string // ASCII representation of macaddr
|
||||
Binary string // CBOR representation of macaddr
|
||||
}{
|
||||
{net.HardwareAddr{0x12, 0x34, 0x56, 0x78, 0x90, 0xab}, "\"12:34:56:78:90:ab\"", "\xd9\x01\x04\x46\x12\x34\x56\x78\x90\xab"},
|
||||
{net.HardwareAddr{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3}, "\"20:01:0d:b8:85:a3\"", "\xd9\x01\x04\x46\x20\x01\x0d\xb8\x85\xa3"},
|
||||
}
|
||||
|
||||
var EncodeHexTests = []struct {
|
||||
In byte
|
||||
Out string
|
||||
}{
|
||||
{0x00, `"00"`},
|
||||
{0x0f, `"0f"`},
|
||||
{0x10, `"10"`},
|
||||
{0xf0, `"f0"`},
|
||||
{0xff, `"ff"`},
|
||||
}
|
||||
|
||||
var EncodeStringTests = []struct {
|
||||
In string
|
||||
Out string
|
||||
}{
|
||||
{"", `""`},
|
||||
{"\\", `"\\"`},
|
||||
{"\x00", `"\u0000"`},
|
||||
{"\x01", `"\u0001"`},
|
||||
{"\x02", `"\u0002"`},
|
||||
{"\x03", `"\u0003"`},
|
||||
{"\x04", `"\u0004"`},
|
||||
{"\x05", `"\u0005"`},
|
||||
{"\x06", `"\u0006"`},
|
||||
{"\x07", `"\u0007"`},
|
||||
{"\x08", `"\b"`},
|
||||
{"\x09", `"\t"`},
|
||||
{"\x0a", `"\n"`},
|
||||
{"\x0b", `"\u000b"`},
|
||||
{"\x0c", `"\f"`},
|
||||
{"\x0d", `"\r"`},
|
||||
{"\x0e", `"\u000e"`},
|
||||
{"\x0f", `"\u000f"`},
|
||||
{"\x10", `"\u0010"`},
|
||||
{"\x11", `"\u0011"`},
|
||||
{"\x12", `"\u0012"`},
|
||||
{"\x13", `"\u0013"`},
|
||||
{"\x14", `"\u0014"`},
|
||||
{"\x15", `"\u0015"`},
|
||||
{"\x16", `"\u0016"`},
|
||||
{"\x17", `"\u0017"`},
|
||||
{"\x18", `"\u0018"`},
|
||||
{"\x19", `"\u0019"`},
|
||||
{"\x1a", `"\u001a"`},
|
||||
{"\x1b", `"\u001b"`},
|
||||
{"\x1c", `"\u001c"`},
|
||||
{"\x1d", `"\u001d"`},
|
||||
{"\x1e", `"\u001e"`},
|
||||
{"\x1f", `"\u001f"`},
|
||||
{"✭", `"✭"`},
|
||||
{"foo\xc2\x7fbar", `"foo\ufffd\u007fbar"`}, // invalid sequence
|
||||
{"ascii", `"ascii"`},
|
||||
{"\"a", `"\"a"`},
|
||||
{"\x1fa", `"\u001fa"`},
|
||||
{"foo\"bar\"baz", `"foo\"bar\"baz"`},
|
||||
{"\x1ffoo\x1fbar\x1fbaz", `"\u001ffoo\u001fbar\u001fbaz"`},
|
||||
{"emoji \u2764\ufe0f!", `"emoji ❤️!"`},
|
||||
}
|
||||
|
||||
var EncodeStringsTests = []struct {
|
||||
In []string
|
||||
Out string
|
||||
}{
|
||||
{nil, `[]`},
|
||||
{[]string{}, `[]`},
|
||||
{[]string{"A"}, `["A"]`},
|
||||
{[]string{"A", "B"}, `["A","B"]`},
|
||||
}
|
||||
|
||||
var EncodeStringerTests = []struct {
|
||||
In fmt.Stringer
|
||||
Out string
|
||||
Binary string
|
||||
}{
|
||||
{nil, `null`, "\xf6"},
|
||||
{fmt.Stringer(nil), `null`, "\xf6"},
|
||||
{net.IPv4bcast, `"255.255.255.255"`, "\x6f\x32\x35\x35\x2e\x32\x35\x35\x2e\x32\x35\x35\x2e\x32\x35\x35"},
|
||||
}
|
||||
|
||||
var EncodeStringersTests = []struct {
|
||||
In []fmt.Stringer
|
||||
Out string
|
||||
Binary string
|
||||
}{
|
||||
{nil, `[]`, "\x9f\xff"},
|
||||
{[]fmt.Stringer{}, `[]`, "\x9f\xff"},
|
||||
{[]fmt.Stringer{net.IPv4bcast}, `["255.255.255.255"]`, "\x9f\x6f255.255.255.255\xff"},
|
||||
{[]fmt.Stringer{net.IPv4allsys, net.IPv4allrouter}, `["224.0.0.1","224.0.0.2"]`, "\x9f\x69224.0.0.1\x69224.0.0.2\xff"},
|
||||
}
|
||||
|
||||
var TimeIntegerTestcases = []struct {
|
||||
Txt string
|
||||
Binary string
|
||||
RfcStr string
|
||||
UnixInt int
|
||||
}{
|
||||
{"2013-02-03T19:54:00-08:00", "\xc1\x1a\x51\x0f\x30\xd8", "2013-02-04T03:54:00Z", 1359950040},
|
||||
{"1950-02-03T19:54:00-08:00", "\xc1\x3a\x25\x71\x93\xa7", "1950-02-04T03:54:00Z", -628200360},
|
||||
}
|
||||
|
||||
var TimeFloatTestcases = []struct {
|
||||
RfcStr string
|
||||
Out string
|
||||
UnixInt int
|
||||
}{
|
||||
{"2006-01-02T15:04:05.999999-08:00", "\xc1\xfb\x41\xd0\xee\x6c\x59\x7f\xff\xfc", 1136243045},
|
||||
{"1956-01-02T15:04:05.999999-08:00", "\xc1\xfb\xc1\xba\x53\x81\x1a\x00\x00\x11", -441680155},
|
||||
}
|
||||
|
||||
var DurTestcases = []struct {
|
||||
Duration time.Duration
|
||||
FloatOut string
|
||||
IntegerOut string
|
||||
}{
|
||||
{1000, "\xfb\x3f\xf0\x00\x00\x00\x00\x00\x00", "\x01"},
|
||||
{2000, "\xfb\x40\x00\x00\x00\x00\x00\x00\x00", "\x02"},
|
||||
{200000, "\xfb\x40\x69\x00\x00\x00\x00\x00\x00", "\x18\xc8"},
|
||||
}
|
||||
|
||||
// inline copy from globals.go of InterfaceMarshalFunc used in tests to avoid import cycle
|
||||
func InterfaceMarshalFunc(v interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := buf.Bytes()
|
||||
if len(b) > 0 {
|
||||
// Remove trailing \n which is added by Encode.
|
||||
return b[:len(b)-1], nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
+50
-15
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
// Package journald provides a io.Writer to send the logs
|
||||
@@ -10,8 +11,9 @@ package journald
|
||||
|
||||
// Zerolog's Top level key/Value Pairs are translated to
|
||||
// journald's args - all Values are sent to journald as strings.
|
||||
// And all key strings are converted to uppercase before sending
|
||||
// to journald (as required by journald).
|
||||
// And all key strings are converted to uppercase and sanitized
|
||||
// by replacing any characters not in [A-Z0-9_] with '_' before
|
||||
// sending to journald (as required by journald).
|
||||
|
||||
// In addition, entire log message (all Key Value Pairs), is also
|
||||
// sent to journald under the key "JSON".
|
||||
@@ -20,15 +22,22 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/coreos/go-systemd/journal"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/internal/cbor"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/journal"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/internal/cbor"
|
||||
)
|
||||
|
||||
const defaultJournalDPrio = journal.PriNotice
|
||||
|
||||
// SendFunc is the function used to send logs to journald.
|
||||
// It can be replaced in tests for mocking. If nil, journal.Send is used directly.
|
||||
// This variable should only be modified in tests and must not be changed while the
|
||||
// writer is in use. Tests that modify this variable should not use t.Parallel().
|
||||
var SendFunc func(string, journal.Priority, map[string]string) error
|
||||
|
||||
// NewJournalDWriter returns a zerolog log destination
|
||||
// to be used as parameter to New() calls. Writing logs
|
||||
// to this writer will send the log messages to journalD
|
||||
@@ -47,6 +56,8 @@ func levelToJPrio(zLevel string) journal.Priority {
|
||||
lvl, _ := zerolog.ParseLevel(zLevel)
|
||||
|
||||
switch lvl {
|
||||
case zerolog.TraceLevel:
|
||||
return journal.PriDebug
|
||||
case zerolog.DebugLevel:
|
||||
return journal.PriDebug
|
||||
case zerolog.InfoLevel:
|
||||
@@ -65,18 +76,33 @@ func levelToJPrio(zLevel string) journal.Priority {
|
||||
return defaultJournalDPrio
|
||||
}
|
||||
|
||||
func (w journalWriter) Write(p []byte) (n int, err error) {
|
||||
if !journal.Enabled() {
|
||||
err = fmt.Errorf("Cannot connect to journalD!!")
|
||||
return
|
||||
// sanitizeKey converts a key to uppercase and replaces invalid characters with '_'
|
||||
// JournalD requires keys start with A-Z and contain only A-Z, 0-9, or _
|
||||
func sanitizeKey(key string) string {
|
||||
sanitized := strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r - 'a' + 'A'
|
||||
} else if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
|
||||
return r
|
||||
} else {
|
||||
return '_'
|
||||
}
|
||||
}, key)
|
||||
if len(sanitized) == 0 || sanitized[0] >= '0' && sanitized[0] <= '9' || sanitized[0] == '_' {
|
||||
sanitized = "X" + sanitized
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func (w journalWriter) Write(p []byte) (n int, err error) {
|
||||
var event map[string]interface{}
|
||||
origPLen := len(p)
|
||||
p = cbor.DecodeIfBinaryToBytes(p)
|
||||
d := json.NewDecoder(bytes.NewReader(p))
|
||||
d.UseNumber()
|
||||
err = d.Decode(&event)
|
||||
jPrio := defaultJournalDPrio
|
||||
args := make(map[string]string, 0)
|
||||
args := make(map[string]string)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -86,7 +112,7 @@ func (w journalWriter) Write(p []byte) (n int, err error) {
|
||||
|
||||
msg := ""
|
||||
for key, value := range event {
|
||||
jKey := strings.ToUpper(key)
|
||||
jKey := sanitizeKey(key)
|
||||
switch key {
|
||||
case zerolog.LevelFieldName, zerolog.TimestampFieldName:
|
||||
continue
|
||||
@@ -95,13 +121,13 @@ func (w journalWriter) Write(p []byte) (n int, err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch value.(type) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
args[jKey], _ = value.(string)
|
||||
args[jKey] = v
|
||||
case json.Number:
|
||||
args[jKey] = fmt.Sprint(value)
|
||||
default:
|
||||
b, err := json.Marshal(value)
|
||||
b, err := zerolog.InterfaceMarshalFunc(value)
|
||||
if err != nil {
|
||||
args[jKey] = fmt.Sprintf("[error: %v]", err)
|
||||
} else {
|
||||
@@ -110,6 +136,15 @@ func (w journalWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
}
|
||||
args["JSON"] = string(p)
|
||||
err = journal.Send(msg, jPrio, args)
|
||||
if SendFunc != nil {
|
||||
err = SendFunc(msg, jPrio, args)
|
||||
} else {
|
||||
err = journal.Send(msg, jPrio, args)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
n = origPLen
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+243
-5
@@ -1,12 +1,21 @@
|
||||
// +build !windows
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package journald_test
|
||||
package journald
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
import "github.com/rs/zerolog/journald"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/journal"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func ExampleNewJournalDWriter() {
|
||||
log := zerolog.New(journald.NewJournalDWriter())
|
||||
log := zerolog.New(NewJournalDWriter())
|
||||
log.Info().Str("foo", "bar").Uint64("small", 123).Float64("float", 3.14).Uint64("big", 1152921504606846976).Msg("Journal Test")
|
||||
// Output:
|
||||
}
|
||||
@@ -42,3 +51,232 @@ Thu 2018-04-26 22:30:20.768136 PDT [s=3284d695bde946e4b5017c77a399237f;i=329f0;b
|
||||
_PID=27103
|
||||
_SOURCE_REALTIME_TIMESTAMP=1524807020768136
|
||||
*/
|
||||
|
||||
func TestSanitizeKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"test", "TEST"},
|
||||
{"Test", "TEST"},
|
||||
{"test-key", "TEST_KEY"},
|
||||
{"Test.Key", "TEST_KEY"},
|
||||
{"test_key123", "TEST_KEY123"},
|
||||
{"invalid@key!", "INVALID_KEY_"},
|
||||
{"a1B2_c3D4", "A1B2_C3D4"},
|
||||
{"_", "X_"},
|
||||
{"", "X"},
|
||||
{"123", "X123"},
|
||||
{"a-b.c_d", "A_B_C_D"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := sanitizeKey(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("sanitizeKey(%q) = %q; want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteReturnsNoOfWrittenBytes(t *testing.T) {
|
||||
input := []byte(`{"level":"info","time":1570912626,"message":"Starting..."}`)
|
||||
wr := NewJournalDWriter()
|
||||
want := len(input)
|
||||
got, err := wr.Write(input)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
if want != got {
|
||||
t.Errorf("Expected %d bytes to be written got %d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiWrite(t *testing.T) {
|
||||
var (
|
||||
w1 = new(bytes.Buffer)
|
||||
w2 = new(bytes.Buffer)
|
||||
w3 = NewJournalDWriter()
|
||||
)
|
||||
|
||||
zerolog.ErrorHandler = func(err error) {
|
||||
if err == io.ErrShortWrite {
|
||||
t.Errorf("Unexpected ShortWriteError")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
log := zerolog.New(io.MultiWriter(w1, w2, w3)).With().Logger()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
log.Info().Msg("Tick!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteWithVariousTypes(t *testing.T) {
|
||||
mock := &mockSend{}
|
||||
oldSend := SendFunc
|
||||
SendFunc = mock.send
|
||||
defer func() { SendFunc = oldSend }()
|
||||
|
||||
wr := NewJournalDWriter()
|
||||
log := zerolog.New(wr)
|
||||
|
||||
// This should cover the default case in the switch for value types
|
||||
log.Info().Bool("flag", true).Str("foo", "bar").Uint64("small", 123).Float64("float", 3.14).Uint64("big", 1152921504606846976).Interface("data", map[string]int{"a": 1}).Msg("Test various types")
|
||||
|
||||
// Verify the call
|
||||
if len(mock.calls) != 1 {
|
||||
t.Fatalf("Expected 1 call, got %d", len(mock.calls))
|
||||
}
|
||||
|
||||
call := mock.calls[0]
|
||||
|
||||
// Check that flag is sanitized to FLAG and value is "true"
|
||||
if call.args["FLAG"] != "true" {
|
||||
t.Errorf("Expected FLAG=true, got %s", call.args["FLAG"])
|
||||
}
|
||||
|
||||
// Check that data is marshaled (should be a JSON string)
|
||||
expectedData := `{"a":1}`
|
||||
if call.args["DATA"] != expectedData {
|
||||
t.Errorf("Expected DATA=%q, got %q", expectedData, call.args["DATA"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteWithAllLevels(t *testing.T) {
|
||||
wr := NewJournalDWriter()
|
||||
|
||||
// Save original FatalExitFunc
|
||||
oldFatalExitFunc := zerolog.FatalExitFunc
|
||||
defer func() { zerolog.FatalExitFunc = oldFatalExitFunc }()
|
||||
|
||||
// Set FatalExitFunc to prevent actual exit
|
||||
zerolog.FatalExitFunc = func() {}
|
||||
|
||||
log := zerolog.New(wr)
|
||||
|
||||
// Test all zerolog levels to cover levelToJPrio switch cases
|
||||
log.Trace().Msg("Trace level")
|
||||
log.Debug().Msg("Debug level")
|
||||
log.Info().Msg("Info level")
|
||||
log.Warn().Msg("Warn level")
|
||||
log.Error().Msg("Error level")
|
||||
log.Log().Msg("No level")
|
||||
|
||||
// For Fatal, it will call FatalExitFunc instead of exiting
|
||||
log.Fatal().Msg("Fatal level")
|
||||
|
||||
// For Panic, use recover to catch the panic, do last because it will stop of this test execution
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("Expected panic from Panic level")
|
||||
}
|
||||
}()
|
||||
log.Panic().Msg("Panic level")
|
||||
|
||||
}
|
||||
|
||||
func TestWriteOutputs(t *testing.T) {
|
||||
mock := &mockSend{}
|
||||
oldSend := SendFunc
|
||||
SendFunc = mock.send
|
||||
defer func() { SendFunc = oldSend }()
|
||||
|
||||
wr := NewJournalDWriter()
|
||||
log := zerolog.New(wr)
|
||||
|
||||
// Log a message with various fields
|
||||
log.Info().Str("test-key", "value").Int("number", 42).Msg("Test message")
|
||||
|
||||
// Check that SendFunc was called
|
||||
if len(mock.calls) != 1 {
|
||||
t.Fatalf("Expected 1 call to SendFunc, got %d", len(mock.calls))
|
||||
}
|
||||
|
||||
call := mock.calls[0]
|
||||
|
||||
// Check message
|
||||
if call.msg != "Test message" {
|
||||
t.Errorf("Expected msg 'Test message', got %q", call.msg)
|
||||
}
|
||||
|
||||
// Check priority
|
||||
if call.prio != journal.PriInfo {
|
||||
t.Errorf("Expected prio %d (PriInfo), got %d", journal.PriInfo, call.prio)
|
||||
}
|
||||
|
||||
// Check args
|
||||
expectedArgs := map[string]string{
|
||||
"TEST_KEY": "value",
|
||||
"NUMBER": "42",
|
||||
"JSON": `{"level":"info","test-key":"value","number":42,"message":"Test message"}` + "\n",
|
||||
}
|
||||
|
||||
for k, v := range expectedArgs {
|
||||
if call.args[k] != v {
|
||||
t.Errorf("Expected args[%q] = %q, got %q", k, v, call.args[k])
|
||||
}
|
||||
}
|
||||
|
||||
// Check that LEVEL is not in args (since it's skipped)
|
||||
if _, ok := call.args["LEVEL"]; ok {
|
||||
t.Error("LEVEL should not be in args")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteWithMarshalError(t *testing.T) {
|
||||
mock := &mockSend{}
|
||||
oldSend := SendFunc
|
||||
SendFunc = mock.send
|
||||
defer func() { SendFunc = oldSend }()
|
||||
|
||||
// Save original marshal func
|
||||
originalMarshal := zerolog.InterfaceMarshalFunc
|
||||
defer func() { zerolog.InterfaceMarshalFunc = originalMarshal }()
|
||||
|
||||
// Set marshal func to fail
|
||||
zerolog.InterfaceMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return nil, fmt.Errorf("fake error")
|
||||
}
|
||||
|
||||
wr := NewJournalDWriter()
|
||||
log := zerolog.New(wr)
|
||||
|
||||
// This should trigger the error handling in the default case
|
||||
log.Info().Interface("data", map[string]int{"a": 1}).Msg("Test with error")
|
||||
|
||||
// Verify the call
|
||||
if len(mock.calls) != 1 {
|
||||
t.Fatalf("Expected 1 call, got %d", len(mock.calls))
|
||||
}
|
||||
|
||||
call := mock.calls[0]
|
||||
|
||||
// Check that data has the error message
|
||||
got := call.args["DATA"]
|
||||
want := "error: fake error"
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("Expected DATA to contain %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
type mockSend struct {
|
||||
calls []struct {
|
||||
msg string
|
||||
prio journal.Priority
|
||||
args map[string]string
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockSend) send(msg string, prio journal.Priority, args map[string]string) error {
|
||||
m.calls = append(m.calls, struct {
|
||||
msg string
|
||||
prio journal.Priority
|
||||
args map[string]string
|
||||
}{msg, prio, args})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,112 +2,129 @@
|
||||
//
|
||||
// A global Logger can be use for simple logging:
|
||||
//
|
||||
// import "github.com/rs/zerolog/log"
|
||||
// import "github.com/rs/zerolog/log"
|
||||
//
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
|
||||
//
|
||||
// NOTE: To import the global logger, import the "log" subpackage "github.com/rs/zerolog/log".
|
||||
//
|
||||
// Fields can be added to log messages:
|
||||
//
|
||||
// log.Info().Str("foo", "bar").Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
// log.Info().Str("foo", "bar").Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
//
|
||||
// Create logger instance to manage different outputs:
|
||||
//
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Str("foo", "bar").
|
||||
// Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Str("foo", "bar").
|
||||
// Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
//
|
||||
// Sub-loggers let you chain loggers with additional context:
|
||||
//
|
||||
// sublogger := log.With().Str("component": "foo").Logger()
|
||||
// sublogger.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
|
||||
// sublogger := log.With().Str("component", "foo").Logger()
|
||||
// sublogger.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
|
||||
//
|
||||
// Level logging
|
||||
//
|
||||
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
//
|
||||
// log.Debug().Msg("filtered out message")
|
||||
// log.Info().Msg("routed message")
|
||||
// log.Debug().Msg("filtered out message")
|
||||
// log.Info().Msg("routed message")
|
||||
//
|
||||
// if e := log.Debug(); e.Enabled() {
|
||||
// // Compute log output only if enabled.
|
||||
// value := compute()
|
||||
// e.Str("foo": value).Msg("some debug message")
|
||||
// }
|
||||
// // Output: {"level":"info","time":1494567715,"routed message"}
|
||||
// if e := log.Debug(); e.Enabled() {
|
||||
// // Compute log output only if enabled.
|
||||
// value := compute()
|
||||
// e.Str("foo": value).Msg("some debug message")
|
||||
// }
|
||||
// // Output: {"level":"info","time":1494567715,"routed message"}
|
||||
//
|
||||
// Customize automatic field names:
|
||||
//
|
||||
// log.TimestampFieldName = "t"
|
||||
// log.LevelFieldName = "p"
|
||||
// log.MessageFieldName = "m"
|
||||
// log.TimestampFieldName = "t"
|
||||
// log.LevelFieldName = "p"
|
||||
// log.MessageFieldName = "m"
|
||||
//
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
|
||||
//
|
||||
// Log with no level and message:
|
||||
//
|
||||
// log.Log().Str("foo","bar").Msg("")
|
||||
// // Output: {"time":1494567715,"foo":"bar"}
|
||||
// log.Log().Str("foo","bar").Msg("")
|
||||
// // Output: {"time":1494567715,"foo":"bar"}
|
||||
//
|
||||
// Add contextual fields to global Logger:
|
||||
//
|
||||
// log.Logger = log.With().Str("foo", "bar").Logger()
|
||||
// log.Logger = log.With().Str("foo", "bar").Logger()
|
||||
//
|
||||
// Sample logs:
|
||||
//
|
||||
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
|
||||
// sampled.Info().Msg("will be logged every 10 messages")
|
||||
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
|
||||
// sampled.Info().Msg("will be logged every 10 messages")
|
||||
//
|
||||
// Log with contextual hooks:
|
||||
//
|
||||
// // Create the hook:
|
||||
// type SeverityHook struct{}
|
||||
// // Create the hook:
|
||||
// type SeverityHook struct{}
|
||||
//
|
||||
// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
// if level != zerolog.NoLevel {
|
||||
// e.Str("severity", level.String())
|
||||
// }
|
||||
// }
|
||||
// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
// if level != zerolog.NoLevel {
|
||||
// e.Str("severity", level.String())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // And use it:
|
||||
// var h SeverityHook
|
||||
// log := zerolog.New(os.Stdout).Hook(h)
|
||||
// log.Warn().Msg("")
|
||||
// // Output: {"level":"warn","severity":"warn"}
|
||||
// // And use it:
|
||||
// var h SeverityHook
|
||||
// log := zerolog.New(os.Stdout).Hook(h)
|
||||
// log.Warn().Msg("")
|
||||
// // Output: {"level":"warn","severity":"warn"}
|
||||
//
|
||||
// # Caveats
|
||||
//
|
||||
// Caveats
|
||||
// Field duplication:
|
||||
//
|
||||
// There is no fields deduplication out-of-the-box.
|
||||
// Using the same key multiple times creates new key in final JSON each time.
|
||||
//
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Timestamp().
|
||||
// Msg("dup")
|
||||
// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Timestamp().
|
||||
// Msg("dup")
|
||||
// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
|
||||
//
|
||||
// However, it’s not a big deal though as JSON accepts dup keys,
|
||||
// the last one prevails.
|
||||
// In this case, many consumers will take the last value,
|
||||
// but this is not guaranteed; check yours if in doubt.
|
||||
//
|
||||
// Concurrency safety:
|
||||
//
|
||||
// Be careful when calling UpdateContext. It is not concurrency safe. Use the With method to create a child logger:
|
||||
//
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// // Create a child logger for concurrency safety
|
||||
// logger := log.Logger.With().Logger()
|
||||
//
|
||||
// // Add context fields, for example User-Agent from HTTP headers
|
||||
// logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
// ...
|
||||
// })
|
||||
// }
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Level defines log levels.
|
||||
type Level uint8
|
||||
type Level int8
|
||||
|
||||
const (
|
||||
// DebugLevel defines debug log level.
|
||||
@@ -126,53 +143,87 @@ const (
|
||||
NoLevel
|
||||
// Disabled disables the logger.
|
||||
Disabled
|
||||
|
||||
// TraceLevel defines trace log level.
|
||||
TraceLevel Level = -1
|
||||
// Values less than TraceLevel are handled as numbers.
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case TraceLevel:
|
||||
return LevelTraceValue
|
||||
case DebugLevel:
|
||||
return "debug"
|
||||
return LevelDebugValue
|
||||
case InfoLevel:
|
||||
return "info"
|
||||
return LevelInfoValue
|
||||
case WarnLevel:
|
||||
return "warn"
|
||||
return LevelWarnValue
|
||||
case ErrorLevel:
|
||||
return "error"
|
||||
return LevelErrorValue
|
||||
case FatalLevel:
|
||||
return "fatal"
|
||||
return LevelFatalValue
|
||||
case PanicLevel:
|
||||
return "panic"
|
||||
return LevelPanicValue
|
||||
case Disabled:
|
||||
return "disabled"
|
||||
case NoLevel:
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
return strconv.Itoa(int(l))
|
||||
}
|
||||
|
||||
// ParseLevel converts a level string into a zerolog Level value.
|
||||
// returns an error if the input string does not match known values.
|
||||
func ParseLevel(levelStr string) (Level, error) {
|
||||
switch levelStr {
|
||||
case DebugLevel.String():
|
||||
switch {
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(TraceLevel)):
|
||||
return TraceLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(DebugLevel)):
|
||||
return DebugLevel, nil
|
||||
case InfoLevel.String():
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(InfoLevel)):
|
||||
return InfoLevel, nil
|
||||
case WarnLevel.String():
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(WarnLevel)):
|
||||
return WarnLevel, nil
|
||||
case ErrorLevel.String():
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(ErrorLevel)):
|
||||
return ErrorLevel, nil
|
||||
case FatalLevel.String():
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(FatalLevel)):
|
||||
return FatalLevel, nil
|
||||
case PanicLevel.String():
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(PanicLevel)):
|
||||
return PanicLevel, nil
|
||||
case NoLevel.String():
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(Disabled)):
|
||||
return Disabled, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(NoLevel)):
|
||||
return NoLevel, nil
|
||||
}
|
||||
return NoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to NoLevel", levelStr)
|
||||
i, err := strconv.Atoi(levelStr)
|
||||
if err != nil {
|
||||
return NoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to NoLevel", levelStr)
|
||||
}
|
||||
if i > 127 || i < -128 {
|
||||
return NoLevel, fmt.Errorf("Out-Of-Bounds Level: '%d', defaulting to NoLevel", i)
|
||||
}
|
||||
return Level(i), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler to allow for easy reading from toml/yaml/json formats
|
||||
func (l *Level) UnmarshalText(text []byte) error {
|
||||
if l == nil {
|
||||
return errors.New("can't unmarshal a nil *Level")
|
||||
}
|
||||
var err error
|
||||
*l, err = ParseLevel(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler to allow for easy writing into toml/yaml/json formats
|
||||
func (l Level) MarshalText() ([]byte, error) {
|
||||
return []byte(LevelFieldMarshalFunc(l)), nil
|
||||
}
|
||||
|
||||
// A Logger represents an active logging object that generates lines
|
||||
// of JSON output to an io.Writer. Each logging operation makes a single
|
||||
// call to the Writer's Write method. There is no guaranty on access
|
||||
// call to the Writer's Write method. There is no guarantee on access
|
||||
// serialization to the Writer. If your Writer is not thread safe,
|
||||
// you may consider a sync wrapper.
|
||||
type Logger struct {
|
||||
@@ -181,6 +232,8 @@ type Logger struct {
|
||||
sampler Sampler
|
||||
context []byte
|
||||
hooks []Hook
|
||||
stack bool
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// New creates a root logger with given output writer. If the output writer implements
|
||||
@@ -188,17 +241,17 @@ type Logger struct {
|
||||
// one.
|
||||
//
|
||||
// Each logging operation makes a single call to the Writer's Write method. There is no
|
||||
// guaranty on access serialization to the Writer. If your Writer is not thread safe,
|
||||
// guarantee on access serialization to the Writer. If your Writer is not thread safe,
|
||||
// you may consider using sync wrapper.
|
||||
func New(w io.Writer) Logger {
|
||||
if w == nil {
|
||||
w = ioutil.Discard
|
||||
w = io.Discard
|
||||
}
|
||||
lw, ok := w.(LevelWriter)
|
||||
if !ok {
|
||||
lw = levelWriterAdapter{w}
|
||||
lw = LevelWriterAdapter{w}
|
||||
}
|
||||
return Logger{w: lw}
|
||||
return Logger{w: lw, level: TraceLevel}
|
||||
}
|
||||
|
||||
// Nop returns a disabled logger for which all operation are no-op.
|
||||
@@ -211,6 +264,7 @@ func (l Logger) Output(w io.Writer) Logger {
|
||||
l2 := New(w)
|
||||
l2.level = l.level
|
||||
l2.sampler = l.sampler
|
||||
l2.stack = l.stack
|
||||
if len(l.hooks) > 0 {
|
||||
l2.hooks = append(l2.hooks, l.hooks...)
|
||||
}
|
||||
@@ -227,20 +281,28 @@ func (l Logger) With() Context {
|
||||
l.context = make([]byte, 0, 500)
|
||||
if context != nil {
|
||||
l.context = append(l.context, context...)
|
||||
} else {
|
||||
// This is needed for AppendKey to not check len of input
|
||||
// thus making it inlinable
|
||||
l.context = enc.AppendBeginMarker(l.context)
|
||||
}
|
||||
return Context{l}
|
||||
}
|
||||
|
||||
// UpdateContext updates the internal logger's context.
|
||||
//
|
||||
// Use this method with caution. If unsure, prefer the With method.
|
||||
// Caution: This method is not concurrency safe.
|
||||
// Use the With method to create a child logger before modifying the context from concurrent goroutines.
|
||||
func (l *Logger) UpdateContext(update func(c Context) Context) {
|
||||
if l == disabledLogger {
|
||||
if l.disabled() {
|
||||
return
|
||||
}
|
||||
if cap(l.context) == 0 {
|
||||
l.context = make([]byte, 0, 500)
|
||||
}
|
||||
if len(l.context) == 0 {
|
||||
l.context = enc.AppendBeginMarker(l.context)
|
||||
}
|
||||
c := update(Context{*l})
|
||||
l.context = c.l.context
|
||||
}
|
||||
@@ -251,6 +313,11 @@ func (l Logger) Level(lvl Level) Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
// GetLevel returns the current Level of l.
|
||||
func (l Logger) GetLevel() Level {
|
||||
return l.level
|
||||
}
|
||||
|
||||
// Sample returns a logger with the s sampler.
|
||||
func (l Logger) Sample(s Sampler) Logger {
|
||||
l.sampler = s
|
||||
@@ -258,11 +325,23 @@ func (l Logger) Sample(s Sampler) Logger {
|
||||
}
|
||||
|
||||
// Hook returns a logger with the h Hook.
|
||||
func (l Logger) Hook(h Hook) Logger {
|
||||
l.hooks = append(l.hooks, h)
|
||||
func (l Logger) Hook(hooks ...Hook) Logger {
|
||||
if len(hooks) == 0 {
|
||||
return l
|
||||
}
|
||||
newHooks := make([]Hook, len(l.hooks), len(l.hooks)+len(hooks))
|
||||
copy(newHooks, l.hooks)
|
||||
l.hooks = append(newHooks, hooks...)
|
||||
return l
|
||||
}
|
||||
|
||||
// Trace starts a new message with trace level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Trace() *Event {
|
||||
return l.newEvent(TraceLevel, nil)
|
||||
}
|
||||
|
||||
// Debug starts a new message with debug level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
@@ -291,27 +370,56 @@ func (l *Logger) Error() *Event {
|
||||
return l.newEvent(ErrorLevel, nil)
|
||||
}
|
||||
|
||||
// Fatal starts a new message with fatal level. The os.Exit(1) function
|
||||
// is called by the Msg method.
|
||||
// Err starts a new message with error level with err as a field if not nil or
|
||||
// with info level if err is nil.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Err(err error) *Event {
|
||||
if err != nil {
|
||||
return l.Error().Err(err)
|
||||
}
|
||||
|
||||
return l.Info()
|
||||
}
|
||||
|
||||
// Fatal starts a new message with fatal level. The FatalExitFunc interceptor function
|
||||
// is called by the Msg method, which by default terminates the program immediately
|
||||
// using os.Exit(1), any desired behavior can be implemented by setting FatalExitFunc.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Fatal() *Event {
|
||||
return l.newEvent(FatalLevel, func(msg string) { os.Exit(1) })
|
||||
return l.newEvent(FatalLevel, func(msg string) {
|
||||
if closer, ok := l.w.(io.Closer); ok {
|
||||
// Close the writer to flush any buffered message. Otherwise the message
|
||||
// could be lost if FatalExitFunc() terminates the program immediately or
|
||||
// os.Exit(1) is called if not FatalExitFunc isn't set (default).
|
||||
closer.Close()
|
||||
}
|
||||
if FatalExitFunc != nil {
|
||||
FatalExitFunc()
|
||||
} else {
|
||||
os.Exit(1) // untestable: terminates the program, cannot be covered
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Panic starts a new message with panic level. The message is also sent
|
||||
// to the panic function.
|
||||
// Panic starts a new message with panic level. The panic() function
|
||||
// is called by the Msg method, which stops the ordinary flow of a goroutine.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Panic() *Event {
|
||||
return l.newEvent(PanicLevel, func(msg string) { panic(msg) })
|
||||
}
|
||||
|
||||
// WithLevel starts a new message with level.
|
||||
// WithLevel starts a new message with level. Unlike Fatal and Panic
|
||||
// methods, WithLevel does not terminate the program or stop the ordinary
|
||||
// flow of a goroutine when used with their respective levels.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) WithLevel(level Level) *Event {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
return l.Trace()
|
||||
case DebugLevel:
|
||||
return l.Debug()
|
||||
case InfoLevel:
|
||||
@@ -321,15 +429,15 @@ func (l *Logger) WithLevel(level Level) *Event {
|
||||
case ErrorLevel:
|
||||
return l.Error()
|
||||
case FatalLevel:
|
||||
return l.Fatal()
|
||||
return l.newEvent(FatalLevel, nil)
|
||||
case PanicLevel:
|
||||
return l.Panic()
|
||||
return l.newEvent(PanicLevel, nil)
|
||||
case NoLevel:
|
||||
return l.Log()
|
||||
case Disabled:
|
||||
return nil
|
||||
default:
|
||||
panic("zerolog: WithLevel(): invalid level: " + strconv.Itoa(int(level)))
|
||||
return l.newEvent(level, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +453,7 @@ func (l *Logger) Log() *Event {
|
||||
// Arguments are handled in the manner of fmt.Print.
|
||||
func (l *Logger) Print(v ...interface{}) {
|
||||
if e := l.Debug(); e.Enabled() {
|
||||
e.Msg(fmt.Sprint(v...))
|
||||
e.CallerSkipFrame(1).Msg(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +461,15 @@ func (l *Logger) Print(v ...interface{}) {
|
||||
// Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Printf(format string, v ...interface{}) {
|
||||
if e := l.Debug(); e.Enabled() {
|
||||
e.Msg(fmt.Sprintf(format, v...))
|
||||
e.CallerSkipFrame(1).Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Println sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Println.
|
||||
func (l *Logger) Println(v ...interface{}) {
|
||||
if e := l.Debug(); e.Enabled() {
|
||||
e.CallerSkipFrame(1).Msg(fmt.Sprintln(v...))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,29 +481,43 @@ func (l Logger) Write(p []byte) (n int, err error) {
|
||||
// Trim CR added by stdlog.
|
||||
p = p[0 : n-1]
|
||||
}
|
||||
l.Log().Msg(string(p))
|
||||
l.Log().CallerSkipFrame(1).Msg(string(p))
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Logger) newEvent(level Level, done func(string)) *Event {
|
||||
enabled := l.should(level)
|
||||
if !enabled {
|
||||
if done != nil {
|
||||
done("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
e := newEvent(l.w, level)
|
||||
e := newEvent(l.w, level, l.stack, l.ctx, l.hooks)
|
||||
e.done = done
|
||||
e.ch = l.hooks
|
||||
if level != NoLevel {
|
||||
e.Str(LevelFieldName, level.String())
|
||||
if level != NoLevel && LevelFieldName != "" {
|
||||
e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
|
||||
}
|
||||
if l.context != nil && len(l.context) > 0 {
|
||||
if len(l.context) > 1 {
|
||||
e.buf = enc.AppendObjectData(e.buf, l.context)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (l *Logger) scratchEvent() *Event {
|
||||
return newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, l.stack, l.ctx, l.hooks)
|
||||
}
|
||||
|
||||
// disabled returns true if the logger is a disabled or nop logger.
|
||||
func (l *Logger) disabled() bool {
|
||||
return l.w == nil || l.level == Disabled
|
||||
}
|
||||
|
||||
// should returns true if the log event should be logged.
|
||||
func (l *Logger) should(lvl Level) bool {
|
||||
if l.disabled() {
|
||||
return false
|
||||
}
|
||||
if lvl < l.level || lvl < GlobalLevel() {
|
||||
return false
|
||||
}
|
||||
|
||||
+18
-2
@@ -3,6 +3,7 @@ package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
@@ -37,6 +38,21 @@ func Hook(h zerolog.Hook) zerolog.Logger {
|
||||
return Logger.Hook(h)
|
||||
}
|
||||
|
||||
// Err starts a new message with error level with err as a field if not nil or
|
||||
// with info level if err is nil.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func Err(err error) *zerolog.Event {
|
||||
return Logger.Err(err)
|
||||
}
|
||||
|
||||
// Trace starts a new message with trace level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func Trace() *zerolog.Event {
|
||||
return Logger.Trace()
|
||||
}
|
||||
|
||||
// Debug starts a new message with debug level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
@@ -99,13 +115,13 @@ func Log() *zerolog.Event {
|
||||
// Print sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Print.
|
||||
func Print(v ...interface{}) {
|
||||
Logger.Print(v...)
|
||||
Logger.Debug().CallerSkipFrame(1).Msg(fmt.Sprint(v...))
|
||||
}
|
||||
|
||||
// Printf sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Printf.
|
||||
func Printf(format string, v ...interface{}) {
|
||||
Logger.Printf(format, v...)
|
||||
Logger.Debug().CallerSkipFrame(1).Msgf(format, v...)
|
||||
}
|
||||
|
||||
// Ctx returns the Logger associated with the ctx. If no logger
|
||||
|
||||
+111
-8
@@ -1,8 +1,11 @@
|
||||
//go:build !binary_log
|
||||
// +build !binary_log
|
||||
|
||||
package log_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"os"
|
||||
@@ -54,6 +57,25 @@ func ExampleLog() {
|
||||
// Output: {"time":1199811905,"message":"hello world"}
|
||||
}
|
||||
|
||||
// Example of a conditional level based on the presence of an error.
|
||||
func ExampleErr() {
|
||||
setup()
|
||||
err := errors.New("some error")
|
||||
log.Err(err).Msg("hello world")
|
||||
log.Err(nil).Msg("hello world")
|
||||
|
||||
// Output: {"level":"error","error":"some error","time":1199811905,"message":"hello world"}
|
||||
// {"level":"info","time":1199811905,"message":"hello world"}
|
||||
}
|
||||
|
||||
// Example of a log at a particular "level" (in this case, "trace")
|
||||
func ExampleTrace() {
|
||||
setup()
|
||||
log.Trace().Msg("hello world")
|
||||
|
||||
// Output: {"level":"trace","time":1199811905,"message":"hello world"}
|
||||
}
|
||||
|
||||
// Example of a log at a particular "level" (in this case, "debug")
|
||||
func ExampleDebug() {
|
||||
setup()
|
||||
@@ -100,7 +122,13 @@ func ExampleFatal() {
|
||||
// Outputs: {"level":"fatal","time":1199811905,"error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"}
|
||||
}
|
||||
|
||||
// TODO: Panic
|
||||
// Example of a log at a particular "level" (in this case, "panic")
|
||||
func ExamplePanic() {
|
||||
setup()
|
||||
|
||||
log.Panic().Msg("Cannot start")
|
||||
// Outputs: {"level":"panic","time":1199811905,"message":"Cannot start"} then panics
|
||||
}
|
||||
|
||||
// This example uses command-line flags to demonstrate various outputs
|
||||
// depending on the chosen log level.
|
||||
@@ -128,16 +156,91 @@ func Example() {
|
||||
// Output: {"level":"info","time":1199811905,"message":"This message appears when log level set to Debug or Info"}
|
||||
}
|
||||
|
||||
// TODO: Output
|
||||
// Example of using the Output function in the log package to change the output destination
|
||||
func ExampleOutput() {
|
||||
setup()
|
||||
|
||||
// TODO: With
|
||||
out := &bytes.Buffer{}
|
||||
tee := log.Output(out)
|
||||
tee.Info().Msg("hello world")
|
||||
written := out.Len()
|
||||
|
||||
// TODO: Level
|
||||
log.Info().Int("bytes", written).Msg("wrote")
|
||||
// Output: {"level":"info","bytes":59,"time":1199811905,"message":"wrote"}
|
||||
}
|
||||
|
||||
// TODO: Sample
|
||||
// Example of using the With function to add context fields
|
||||
func ExampleWith() {
|
||||
setup()
|
||||
|
||||
// TODO: Hook
|
||||
// you have to assign the result of With() to a new Logger and can't inline the level calls
|
||||
// because they need a *Logger receiver
|
||||
augmented := log.With().Str("service", "myservice").Logger()
|
||||
augmented.Info().Msg("hello world")
|
||||
// Output: {"level":"info","service":"myservice","time":1199811905,"message":"hello world"}
|
||||
}
|
||||
|
||||
// TODO: WithLevel
|
||||
// Example of using the Level function to set the log level
|
||||
func ExampleLevel() {
|
||||
setup()
|
||||
|
||||
// TODO: Ctx
|
||||
// you have to assign the result of Level() to a new Logger and can't inline the level calls
|
||||
// because they need a *Logger receiver
|
||||
leveled := log.Level(zerolog.ErrorLevel)
|
||||
leveled.Info().Msg("hello world")
|
||||
leveled.Error().Msg("I said HELLO")
|
||||
// Output: {"level":"error","time":1199811905,"message":"I said HELLO"}
|
||||
}
|
||||
|
||||
type valueKeyType int
|
||||
|
||||
var valueKey valueKeyType = 42
|
||||
|
||||
var captainHook = zerolog.HookFunc(func(e *zerolog.Event, l zerolog.Level, msg string) {
|
||||
e.Interface("key", e.GetCtx().Value(valueKey))
|
||||
e.Bool("is_error", l > zerolog.ErrorLevel)
|
||||
e.Int("msg_len", len(msg))
|
||||
})
|
||||
|
||||
// Example of using the Logger Hook function to add hooks
|
||||
func ExampleLogger_Hook() {
|
||||
setup()
|
||||
|
||||
hooked := log.Hook(captainHook)
|
||||
hooked.Info().Msg("watch out!")
|
||||
// Output: {"level":"info","time":1199811905,"key":null,"is_error":false,"msg_len":10,"message":"watch out!"}
|
||||
}
|
||||
|
||||
// Example of using the WithLevel function to set the log level
|
||||
func ExampleWithLevel() {
|
||||
setup()
|
||||
|
||||
// you have to assign the result of Level() to a new Logger and can't inline the level calls
|
||||
// because they need a *Logger receiver
|
||||
event := log.WithLevel(zerolog.ErrorLevel)
|
||||
event.Msg("taxes are due")
|
||||
// Output: {"level":"error","time":1199811905,"message":"taxes are due"}
|
||||
}
|
||||
|
||||
// Example of using the Ctx function in the log package to log with context
|
||||
func ExampleCtx() {
|
||||
setup()
|
||||
|
||||
hooked := log.Hook(captainHook)
|
||||
ctx := context.WithValue(context.Background(), valueKey, "12345")
|
||||
logger := hooked.With().Ctx(ctx).Logger()
|
||||
log.Ctx(logger.WithContext(ctx)).Info().Msg("hello world")
|
||||
// Output: {"level":"info","time":1199811905,"key":"12345","is_error":false,"msg_len":11,"message":"hello world"}
|
||||
}
|
||||
|
||||
// Example of using the Sample function in the log package to set a sampler
|
||||
func ExampleSample() {
|
||||
setup()
|
||||
|
||||
sampled := log.Sample(&zerolog.BasicSampler{N: 2})
|
||||
sampled.Info().Msg("hello world")
|
||||
sampled.Info().Msg("I said, hello world")
|
||||
sampled.Info().Msg("Can you here me now world")
|
||||
// Output: {"level":"info","time":1199811905,"message":"hello world"}
|
||||
// {"level":"info","time":1199811905,"message":"Can you here me now world"}
|
||||
}
|
||||
|
||||
+249
-38
@@ -72,7 +72,7 @@ func ExampleLogger_Hook() {
|
||||
var levelNameHook LevelNameHook
|
||||
var messageHook MessageHook = "The message"
|
||||
|
||||
log := zerolog.New(os.Stdout).Hook(levelNameHook).Hook(messageHook)
|
||||
log := zerolog.New(os.Stdout).Hook(levelNameHook, messageHook)
|
||||
|
||||
log.Info().Msg("hello world")
|
||||
|
||||
@@ -95,6 +95,25 @@ func ExampleLogger_Printf() {
|
||||
// Output: {"level":"debug","message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleLogger_Println() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
log.Println("hello world")
|
||||
|
||||
// Output: {"level":"debug","message":"hello world\n"}
|
||||
}
|
||||
|
||||
func ExampleLogger_Trace() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
log.Trace().
|
||||
Str("foo", "bar").
|
||||
Int("n", 123).
|
||||
Msg("hello world")
|
||||
|
||||
// Output: {"level":"trace","foo":"bar","n":123,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleLogger_Debug() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
@@ -173,12 +192,13 @@ func ExampleLogger_Log() {
|
||||
func ExampleEvent_Dict() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Dict("dict", zerolog.Dict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).
|
||||
e := log.Log().
|
||||
Str("foo", "bar")
|
||||
|
||||
e.Dict("dict", e.CreateDict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).
|
||||
Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}
|
||||
@@ -223,15 +243,20 @@ func (uu Users) MarshalZerologArray(a *zerolog.Array) {
|
||||
func ExampleEvent_Array() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Array("array", zerolog.Arr().
|
||||
Str("baz").
|
||||
Int(1),
|
||||
).
|
||||
e := log.Log().
|
||||
Str("foo", "bar")
|
||||
|
||||
e.Array("array", e.CreateArray().
|
||||
Str("baz").
|
||||
Int(1).
|
||||
Dict(e.CreateDict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
),
|
||||
).
|
||||
Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","array":["baz",1],"message":"hello world"}
|
||||
// Output: {"foo":"bar","array":["baz",1,{"bar":"baz","n":1}],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_Array_object() {
|
||||
@@ -265,6 +290,34 @@ func ExampleEvent_Object() {
|
||||
// Output: {"foo":"bar","user":{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_Objects() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
// User implements zerolog.LogObjectMarshaler
|
||||
u := User{"John", 35, time.Time{}}
|
||||
u2 := User{"Bono", 54, time.Time{}}
|
||||
users := []User{u, u2}
|
||||
|
||||
log.Log().
|
||||
Objects("users", zerolog.AsLogObjectMarshalers(users)).
|
||||
Msg("hello world")
|
||||
// Output: {"users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_ObjectsV() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
// User implements zerolog.LogObjectMarshaler
|
||||
u := User{"John", 35, time.Time{}}
|
||||
u2 := User{"Bono", 54, time.Time{}}
|
||||
|
||||
log.Log().
|
||||
ObjectsV("users", u, u2).
|
||||
Msg("hello world")
|
||||
|
||||
// Output: {"users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_EmbedObject() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
@@ -296,7 +349,7 @@ func ExampleEvent_Interface() {
|
||||
}
|
||||
|
||||
func ExampleEvent_Dur() {
|
||||
d := time.Duration(10 * time.Second)
|
||||
d := 10 * time.Second
|
||||
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
@@ -310,8 +363,8 @@ func ExampleEvent_Dur() {
|
||||
|
||||
func ExampleEvent_Durs() {
|
||||
d := []time.Duration{
|
||||
time.Duration(10 * time.Second),
|
||||
time.Duration(20 * time.Second),
|
||||
10 * time.Second,
|
||||
20 * time.Second,
|
||||
}
|
||||
|
||||
log := zerolog.New(os.Stdout)
|
||||
@@ -324,28 +377,62 @@ func ExampleEvent_Durs() {
|
||||
// Output: {"foo":"bar","durs":[10000,20000],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Dict() {
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
Dict("dict", zerolog.Dict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).Logger()
|
||||
func ExampleEvent_Fields_map() {
|
||||
fields := map[string]interface{}{
|
||||
"bar": "baz",
|
||||
"n": 1,
|
||||
}
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_Fields_slice() {
|
||||
fields := []interface{}{
|
||||
"bar", "baz",
|
||||
"n", 1,
|
||||
}
|
||||
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Dict() {
|
||||
ctx := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar")
|
||||
|
||||
logger := ctx.Dict("dict", ctx.CreateDict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).Logger()
|
||||
|
||||
logger.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Array() {
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
Array("array", zerolog.Arr().
|
||||
Str("baz").
|
||||
Int(1),
|
||||
).Logger()
|
||||
ctx := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar")
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
logger := ctx.Array("array", ctx.CreateArray().
|
||||
Str("baz").
|
||||
Int(1),
|
||||
).Logger()
|
||||
|
||||
logger.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","array":["baz",1],"message":"hello world"}
|
||||
}
|
||||
@@ -380,9 +467,37 @@ func ExampleContext_Object() {
|
||||
|
||||
// Output: {"foo":"bar","user":{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},"message":"hello world"}
|
||||
}
|
||||
func ExampleContext_Objects() {
|
||||
// User implements zerolog.LogObjectMarshaler
|
||||
u := User{"John", 35, time.Time{}}
|
||||
u2 := User{"Bono", 54, time.Time{}}
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
Objects("users", []zerolog.LogObjectMarshaler{u, u2}).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_ObjectsV() {
|
||||
// User implements zerolog.LogObjectMarshaler
|
||||
u := User{"John", 35, time.Time{}}
|
||||
u2 := User{"Bono", 54, time.Time{}}
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
ObjectsV("users", u, u2). // shows variadic version with distinct element arguments
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","users":[{"name":"John","age":35,"created":"0001-01-01T00:00:00Z"},{"name":"Bono","age":54,"created":"0001-01-01T00:00:00Z"}],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_EmbedObject() {
|
||||
|
||||
price := Price{val: 6449, prec: 2, unit: "$"}
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
@@ -391,7 +506,6 @@ func ExampleContext_EmbedObject() {
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","price":"$64.49","message":"hello world"}
|
||||
}
|
||||
|
||||
@@ -413,7 +527,7 @@ func ExampleContext_Interface() {
|
||||
}
|
||||
|
||||
func ExampleContext_Dur() {
|
||||
d := time.Duration(10 * time.Second)
|
||||
d := 10 * time.Second
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
@@ -427,8 +541,8 @@ func ExampleContext_Dur() {
|
||||
|
||||
func ExampleContext_Durs() {
|
||||
d := []time.Duration{
|
||||
time.Duration(10 * time.Second),
|
||||
time.Duration(20 * time.Second),
|
||||
10 * time.Second,
|
||||
20 * time.Second,
|
||||
}
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
@@ -452,6 +566,17 @@ func ExampleContext_IPAddr() {
|
||||
// Output: {"HostIP":"192.168.0.100","message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_IPAddrs() {
|
||||
hostIP := net.IP{192, 168, 0, 100}
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
IPAddrs("HostIP", []net.IP{hostIP}).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"HostIP":["192.168.0.100"],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_IPPrefix() {
|
||||
route := net.IPNet{IP: net.IP{192, 168, 0, 0}, Mask: net.CIDRMask(24, 32)}
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
@@ -463,7 +588,18 @@ func ExampleContext_IPPrefix() {
|
||||
// Output: {"Route":"192.168.0.0/24","message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_MacAddr() {
|
||||
func ExampleContext_IPPrefixes() {
|
||||
route := net.IPNet{IP: net.IP{192, 168, 0, 0}, Mask: net.CIDRMask(24, 32)}
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
IPPrefixes("Route", []net.IPNet{route}).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"Route":["192.168.0.0/24"],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_MACAddr() {
|
||||
mac := net.HardwareAddr{0x00, 0x14, 0x22, 0x01, 0x23, 0x45}
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
MACAddr("hostMAC", mac).
|
||||
@@ -473,3 +609,78 @@ func ExampleContext_MacAddr() {
|
||||
|
||||
// Output: {"hostMAC":"00:14:22:01:23:45","message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Fields_map() {
|
||||
fields := map[string]interface{}{
|
||||
"bar": "baz",
|
||||
"n": 1,
|
||||
}
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Fields_slice() {
|
||||
fields := []interface{}{
|
||||
"bar", "baz",
|
||||
"n", 1,
|
||||
}
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
Fields(fields).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","bar":"baz","n":1,"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_Times() {
|
||||
t1 := time.Time{}
|
||||
t2 := t1.Add(time.Second * 10)
|
||||
t := []time.Time{t1, t2}
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
Times("times", t).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"foo":"bar","times":["0001-01-01T00:00:00Z","0001-01-01T00:00:10Z"],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleEvent_Stringers() {
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
// net.IP values implement fmt.Stringer and can be used with StringersV
|
||||
a := net.IP{127, 0, 0, 1}
|
||||
b := net.IP{127, 0, 0, 2}
|
||||
ips := []net.IP{a, b}
|
||||
|
||||
log.Log().
|
||||
Stringers("ips", zerolog.AsStringers(ips)).
|
||||
Msg("hello world")
|
||||
// Output: {"ips":["127.0.0.1","127.0.0.2"],"message":"hello world"}
|
||||
}
|
||||
|
||||
func ExampleContext_StringersV() {
|
||||
// net.IP values implement fmt.Stringer and can be used with StringersV
|
||||
a := net.IPv4bcast
|
||||
b := net.IPv4allrouter
|
||||
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
StringersV("ips", a, b).
|
||||
Logger()
|
||||
|
||||
log.Log().Msg("hello world")
|
||||
|
||||
// Output: {"ips":["255.255.255.255","224.0.0.2"],"message":"hello world"}
|
||||
}
|
||||
|
||||
+945
-69
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
// +build !go1.12
|
||||
|
||||
package zerolog
|
||||
|
||||
const contextCallerSkipFrameCount = 3
|
||||
@@ -0,0 +1,82 @@
|
||||
package pkgerrors
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
StackSourceFileName = "source"
|
||||
StackSourceLineName = "line"
|
||||
StackSourceFunctionName = "func"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
// Write implement fmt.Formatter interface.
|
||||
func (s *state) Write(b []byte) (n int, err error) {
|
||||
s.b = b
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Width implement fmt.Formatter interface.
|
||||
func (s *state) Width() (wid int, ok bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Precision implement fmt.Formatter interface.
|
||||
func (s *state) Precision() (prec int, ok bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Flag implement fmt.Formatter interface.
|
||||
func (s *state) Flag(c int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func frameField(f errors.Frame, s *state, c rune) string {
|
||||
f.Format(s, c)
|
||||
return string(s.b)
|
||||
}
|
||||
|
||||
// MarshalStack implements pkg/errors stack trace marshaling.
|
||||
//
|
||||
// zerolog.ErrorStackMarshaler = MarshalStack
|
||||
func MarshalStack(err error) interface{} {
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
var sterr stackTracer
|
||||
var ok bool
|
||||
for err != nil {
|
||||
sterr, ok = err.(stackTracer)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
|
||||
u, ok := err.(interface {
|
||||
Unwrap() error
|
||||
})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = u.Unwrap()
|
||||
}
|
||||
if sterr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st := sterr.StackTrace()
|
||||
s := &state{}
|
||||
out := make([]map[string]string, 0, len(st))
|
||||
for _, frame := range st {
|
||||
out = append(out, map[string]string{
|
||||
StackSourceFileName: frameField(frame, s, 's'),
|
||||
StackSourceLineName: frameField(frame, s, 'd'),
|
||||
StackSourceFunctionName: frameField(frame, s, 'n'),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// +build !binary_log
|
||||
|
||||
package pkgerrors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func TestLogStack(t *testing.T) {
|
||||
zerolog.ErrorStackMarshaler = MarshalStack
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
log := zerolog.New(out)
|
||||
|
||||
err := fmt.Errorf("from error: %w", errors.New("error message"))
|
||||
log.Log().Stack().Err(err).Msg("")
|
||||
|
||||
got := out.String()
|
||||
want := `\{"stack":\[\{"func":"TestLogStack","line":"21","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n`
|
||||
if ok, _ := regexp.MatchString(want, got); !ok {
|
||||
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStackFields(t *testing.T) {
|
||||
zerolog.ErrorStackMarshaler = MarshalStack
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
log := zerolog.New(out)
|
||||
|
||||
err := fmt.Errorf("from error: %w", errors.New("error message"))
|
||||
log.Log().Stack().Fields([]interface{}{"error", err}).Msg("")
|
||||
|
||||
got := out.String()
|
||||
want := `\{"error":"from error: error message","stack":\[\{"func":"TestLogStackFields","line":"37","source":"stacktrace_test.go"\},.*\]\}\n`
|
||||
if ok, _ := regexp.MatchString(want, got); !ok {
|
||||
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStackFromContext(t *testing.T) {
|
||||
zerolog.ErrorStackMarshaler = MarshalStack
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
log := zerolog.New(out).With().Stack().Logger() // calling Stack() on log context instead of event
|
||||
|
||||
err := fmt.Errorf("from error: %w", errors.New("error message"))
|
||||
log.Log().Err(err).Msg("") // not explicitly calling Stack()
|
||||
|
||||
got := out.String()
|
||||
want := `\{"stack":\[\{"func":"TestLogStackFromContext","line":"53","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n`
|
||||
if ok, _ := regexp.MatchString(want, got); !ok {
|
||||
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStackFromContextWith(t *testing.T) {
|
||||
zerolog.ErrorStackMarshaler = MarshalStack
|
||||
|
||||
err := fmt.Errorf("from error: %w", errors.New("error message"))
|
||||
out := &bytes.Buffer{}
|
||||
log := zerolog.New(out).With().Stack().Err(err).Logger() // calling Stack() on log context instead of event
|
||||
|
||||
log.Error().Msg("")
|
||||
|
||||
got := out.String()
|
||||
want := `\{"level":"error","stack":\[\{"func":"TestLogStackFromContextWith","line":"66","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n`
|
||||
if ok, _ := regexp.MatchString(want, got); !ok {
|
||||
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogStack(b *testing.B) {
|
||||
zerolog.ErrorStackMarshaler = MarshalStack
|
||||
out := &bytes.Buffer{}
|
||||
log := zerolog.New(out)
|
||||
err := errors.Wrap(errors.New("error message"), "from error")
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
log.Log().Stack().Err(err).Msg("")
|
||||
out.Reset()
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 377 KiB After Width: | Height: | Size: 116 KiB |
+16
-5
@@ -38,7 +38,7 @@ func (s RandomSampler) Sample(lvl Level) bool {
|
||||
}
|
||||
|
||||
// BasicSampler is a sampler that will send every Nth events, regardless of
|
||||
// there level.
|
||||
// their level.
|
||||
type BasicSampler struct {
|
||||
N uint32
|
||||
counter uint32
|
||||
@@ -46,8 +46,15 @@ type BasicSampler struct {
|
||||
|
||||
// Sample implements the Sampler interface.
|
||||
func (s *BasicSampler) Sample(lvl Level) bool {
|
||||
n := s.N
|
||||
if n == 0 {
|
||||
return false
|
||||
}
|
||||
if n == 1 {
|
||||
return true
|
||||
}
|
||||
c := atomic.AddUint32(&s.counter, 1)
|
||||
return c%s.N == s.N-1
|
||||
return c%n == 1
|
||||
}
|
||||
|
||||
// BurstSampler lets Burst events pass per Period then pass the decision to
|
||||
@@ -80,10 +87,10 @@ func (s *BurstSampler) Sample(lvl Level) bool {
|
||||
}
|
||||
|
||||
func (s *BurstSampler) inc() uint32 {
|
||||
now := time.Now().UnixNano()
|
||||
now := TimestampFunc().UnixNano()
|
||||
resetAt := atomic.LoadInt64(&s.resetAt)
|
||||
var c uint32
|
||||
if now > resetAt {
|
||||
if now >= resetAt {
|
||||
c = 1
|
||||
atomic.StoreUint32(&s.counter, c)
|
||||
newResetAt := now + s.Period.Nanoseconds()
|
||||
@@ -100,11 +107,15 @@ func (s *BurstSampler) inc() uint32 {
|
||||
|
||||
// LevelSampler applies a different sampler for each level.
|
||||
type LevelSampler struct {
|
||||
DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler
|
||||
TraceSampler, DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler
|
||||
}
|
||||
|
||||
func (s LevelSampler) Sample(lvl Level) bool {
|
||||
switch lvl {
|
||||
case TraceLevel:
|
||||
if s.TraceSampler != nil {
|
||||
return s.TraceSampler.Sample(lvl)
|
||||
}
|
||||
case DebugLevel:
|
||||
if s.DebugSampler != nil {
|
||||
return s.DebugSampler.Sample(lvl)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !binary_log
|
||||
// +build !binary_log
|
||||
|
||||
package zerolog
|
||||
@@ -28,6 +29,13 @@ var samplers = []struct {
|
||||
},
|
||||
100, 20, 20,
|
||||
},
|
||||
{
|
||||
"BasicSampler_0",
|
||||
func() Sampler {
|
||||
return &BasicSampler{N: 0}
|
||||
},
|
||||
100, 0, 0,
|
||||
},
|
||||
{
|
||||
"RandomSampler",
|
||||
func() Sampler {
|
||||
@@ -35,6 +43,13 @@ var samplers = []struct {
|
||||
},
|
||||
100, 10, 30,
|
||||
},
|
||||
{
|
||||
"RandomSampler_0",
|
||||
func() Sampler {
|
||||
return RandomSampler(0)
|
||||
},
|
||||
100, 0, 0,
|
||||
},
|
||||
{
|
||||
"BurstSampler",
|
||||
func() Sampler {
|
||||
@@ -42,6 +57,13 @@ var samplers = []struct {
|
||||
},
|
||||
100, 20, 20,
|
||||
},
|
||||
{
|
||||
"BurstSampler_0",
|
||||
func() Sampler {
|
||||
return &BurstSampler{Burst: 0, Period: time.Second}
|
||||
},
|
||||
100, 0, 0,
|
||||
},
|
||||
{
|
||||
"BurstSamplerNext",
|
||||
func() Sampler {
|
||||
@@ -82,3 +104,79 @@ func BenchmarkSamplers(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBurst(t *testing.T) {
|
||||
sampler := &BurstSampler{Burst: 1, Period: time.Second}
|
||||
|
||||
t0 := time.Now()
|
||||
now := t0
|
||||
mockedTime := func() time.Time {
|
||||
return now
|
||||
}
|
||||
|
||||
TimestampFunc = mockedTime
|
||||
defer func() { TimestampFunc = time.Now }()
|
||||
|
||||
scenario := []struct {
|
||||
tm time.Time
|
||||
want bool
|
||||
}{
|
||||
{t0, true},
|
||||
{t0.Add(time.Second - time.Nanosecond), false},
|
||||
{t0.Add(time.Second), true},
|
||||
{t0.Add(time.Second + time.Nanosecond), false},
|
||||
}
|
||||
|
||||
for i, step := range scenario {
|
||||
now = step.tm
|
||||
got := sampler.Sample(NoLevel)
|
||||
if got != step.want {
|
||||
t.Errorf("step %d (t=%s): expect %t got %t", i, step.tm, step.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelSampler(t *testing.T) {
|
||||
// Create mock samplers that return true for specific levels
|
||||
traceSampler := &BasicSampler{N: 1} // Always sample
|
||||
debugSampler := &BasicSampler{N: 0} // Never sample
|
||||
infoSampler := &BasicSampler{N: 1} // Always sample
|
||||
warnSampler := &BasicSampler{N: 0} // Never sample
|
||||
errorSampler := &BasicSampler{N: 1} // Always sample
|
||||
|
||||
sampler := LevelSampler{
|
||||
TraceSampler: traceSampler,
|
||||
DebugSampler: debugSampler,
|
||||
InfoSampler: infoSampler,
|
||||
WarnSampler: warnSampler,
|
||||
ErrorSampler: errorSampler,
|
||||
}
|
||||
|
||||
// Test each level
|
||||
if !sampler.Sample(TraceLevel) {
|
||||
t.Error("TraceLevel should be sampled")
|
||||
}
|
||||
if sampler.Sample(DebugLevel) {
|
||||
t.Error("DebugLevel should not be sampled")
|
||||
}
|
||||
if !sampler.Sample(InfoLevel) {
|
||||
t.Error("InfoLevel should be sampled")
|
||||
}
|
||||
if sampler.Sample(WarnLevel) {
|
||||
t.Error("WarnLevel should not be sampled")
|
||||
}
|
||||
if !sampler.Sample(ErrorLevel) {
|
||||
t.Error("ErrorLevel should be sampled")
|
||||
}
|
||||
|
||||
// Test levels not covered by the LevelSampler sampler (FatalLevel, PanicLevel, NoLevel) - should return true
|
||||
if !sampler.Sample(FatalLevel) {
|
||||
t.Error("FatalLevel should return true when no sampler is set")
|
||||
}
|
||||
if !sampler.Sample(PanicLevel) {
|
||||
t.Error("PanicLevel should return true when no sampler is set")
|
||||
}
|
||||
if !sampler.Sample(NoLevel) {
|
||||
t.Error("NoLevel should return true when no sampler is set")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SlogHandler implements the slog.Handler interface using a zerolog.Logger
|
||||
// as the underlying log backend. This allows code that uses the standard
|
||||
// library's slog package to route log output through zerolog.
|
||||
type SlogHandler struct {
|
||||
logger Logger
|
||||
prefix string // group prefix for nested groups
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
// NewSlogHandler creates a new slog.Handler that writes log records to the
|
||||
// given zerolog.Logger. The handler maps slog levels to zerolog levels and
|
||||
// converts slog attributes to zerolog fields.
|
||||
func NewSlogHandler(logger Logger) *SlogHandler {
|
||||
return &SlogHandler{logger: logger}
|
||||
}
|
||||
|
||||
// Enabled reports whether the handler handles records at the given level.
|
||||
// It mirrors Logger.should's level and writer checks (without sampling).
|
||||
func (h *SlogHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
if h.logger.w == nil {
|
||||
return false
|
||||
}
|
||||
zl := slogToZerologLevel(level)
|
||||
if zl < GlobalLevel() {
|
||||
return false
|
||||
}
|
||||
return zl >= h.logger.level
|
||||
}
|
||||
|
||||
// Handle handles the Record. It converts the slog.Record into a zerolog event
|
||||
// and writes it using the underlying zerolog.Logger.
|
||||
func (h *SlogHandler) Handle(ctx context.Context, record slog.Record) error {
|
||||
zlevel := slogToZerologLevel(record.Level)
|
||||
event := h.logger.WithLevel(zlevel)
|
||||
if event == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Propagate slog context to the zerolog event so that hooks
|
||||
// relying on Event.GetCtx() (e.g. tracing) can access it.
|
||||
if ctx != nil {
|
||||
event = event.Ctx(ctx)
|
||||
}
|
||||
|
||||
// Add pre-attached attrs from WithAttrs
|
||||
for _, a := range h.attrs {
|
||||
event = appendSlogAttr(event, a, h.prefix)
|
||||
}
|
||||
|
||||
// Add attrs from the record itself
|
||||
record.Attrs(func(a slog.Attr) bool {
|
||||
event = appendSlogAttr(event, a, h.prefix)
|
||||
return true
|
||||
})
|
||||
|
||||
// Add timestamp from the slog record, but only if the logger doesn't
|
||||
// already have a timestampHook (added via .With().Timestamp()) to
|
||||
// avoid duplicate timestamp keys in the output.
|
||||
if !record.Time.IsZero() && !h.hasTimestampHook() {
|
||||
event.Time(TimestampFieldName, record.Time)
|
||||
}
|
||||
|
||||
event.Msg(record.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasTimestampHook reports whether the logger has a timestampHook installed,
|
||||
// which would cause duplicate timestamp fields if we also emit record.Time.
|
||||
func (h *SlogHandler) hasTimestampHook() bool {
|
||||
for _, hook := range h.logger.hooks {
|
||||
if _, ok := hook.(timestampHook); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithAttrs returns a new Handler with the given attributes pre-attached.
|
||||
// These attributes will be included in every subsequent log record.
|
||||
func (h *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
if len(attrs) == 0 {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
h2.attrs = append(h2.attrs, attrs...)
|
||||
return h2
|
||||
}
|
||||
|
||||
// WithGroup returns a new Handler with the given group name. All subsequent
|
||||
// attributes will be nested under this group name in the output.
|
||||
func (h *SlogHandler) WithGroup(name string) slog.Handler {
|
||||
if name == "" {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
if h2.prefix != "" {
|
||||
h2.prefix = h2.prefix + "." + name
|
||||
} else {
|
||||
h2.prefix = name
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *SlogHandler) clone() *SlogHandler {
|
||||
h2 := &SlogHandler{
|
||||
logger: h.logger,
|
||||
prefix: h.prefix,
|
||||
}
|
||||
if len(h.attrs) > 0 {
|
||||
h2.attrs = make([]slog.Attr, len(h.attrs))
|
||||
copy(h2.attrs, h.attrs)
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
// slogToZerologLevel maps slog levels to zerolog levels.
|
||||
//
|
||||
// slog levels: Debug=-4, Info=0, Warn=4, Error=8
|
||||
// zerolog levels: Trace=-1, Debug=0, Info=1, Warn=2, Error=3, Fatal=4, Panic=5
|
||||
func slogToZerologLevel(level slog.Level) Level {
|
||||
switch {
|
||||
case level < slog.LevelDebug:
|
||||
return TraceLevel
|
||||
case level < slog.LevelInfo:
|
||||
return DebugLevel
|
||||
case level < slog.LevelWarn:
|
||||
return InfoLevel
|
||||
case level < slog.LevelError:
|
||||
return WarnLevel
|
||||
default:
|
||||
return ErrorLevel
|
||||
}
|
||||
}
|
||||
|
||||
// zerologToSlogLevel maps zerolog levels to slog levels.
|
||||
func zerologToSlogLevel(level Level) slog.Level {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
return slog.LevelDebug - 4
|
||||
case DebugLevel:
|
||||
return slog.LevelDebug
|
||||
case InfoLevel:
|
||||
return slog.LevelInfo
|
||||
case WarnLevel:
|
||||
return slog.LevelWarn
|
||||
case ErrorLevel:
|
||||
return slog.LevelError
|
||||
case FatalLevel:
|
||||
return slog.LevelError + 4
|
||||
case PanicLevel:
|
||||
return slog.LevelError + 8
|
||||
default:
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// joinPrefix concatenates a prefix and key with a dot separator.
|
||||
// It avoids allocations when either prefix or key is empty.
|
||||
func joinPrefix(prefix, key string) string {
|
||||
if prefix == "" {
|
||||
return key
|
||||
}
|
||||
if key == "" {
|
||||
return prefix
|
||||
}
|
||||
return prefix + "." + key
|
||||
}
|
||||
|
||||
// appendSlogAttr appends a single slog.Attr to the zerolog event, handling
|
||||
// type-specific encoding to avoid reflection where possible.
|
||||
func appendSlogAttr(event *Event, attr slog.Attr, prefix string) *Event {
|
||||
if event == nil {
|
||||
return event
|
||||
}
|
||||
|
||||
// Resolve the attribute to handle LogValuer types.
|
||||
// This handles slog.KindLogValuer implicitly by unwrapping
|
||||
// any values that implement slog.LogValuer to their resolved form.
|
||||
attr.Value = attr.Value.Resolve()
|
||||
|
||||
// For group kinds, handle grouping before key concatenation
|
||||
if attr.Value.Kind() == slog.KindGroup {
|
||||
attrs := attr.Value.Group()
|
||||
if len(attrs) == 0 {
|
||||
return event
|
||||
}
|
||||
groupPrefix := joinPrefix(prefix, attr.Key)
|
||||
for _, ga := range attrs {
|
||||
event = appendSlogAttr(event, ga, groupPrefix)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// Skip empty keys for non-group attributes
|
||||
if attr.Key == "" {
|
||||
return event
|
||||
}
|
||||
|
||||
key := joinPrefix(prefix, attr.Key)
|
||||
val := attr.Value
|
||||
|
||||
switch val.Kind() {
|
||||
case slog.KindString:
|
||||
event = event.Str(key, val.String())
|
||||
case slog.KindInt64:
|
||||
event = event.Int64(key, val.Int64())
|
||||
case slog.KindUint64:
|
||||
event = event.Uint64(key, val.Uint64())
|
||||
case slog.KindFloat64:
|
||||
event = event.Float64(key, val.Float64())
|
||||
case slog.KindBool:
|
||||
event = event.Bool(key, val.Bool())
|
||||
case slog.KindDuration:
|
||||
event = event.Dur(key, val.Duration())
|
||||
case slog.KindTime:
|
||||
event = event.Time(key, val.Time())
|
||||
case slog.KindAny:
|
||||
v := val.Any()
|
||||
switch cv := v.(type) {
|
||||
case error:
|
||||
event = event.AnErr(key, cv)
|
||||
case time.Duration:
|
||||
event = event.Dur(key, cv)
|
||||
case time.Time:
|
||||
event = event.Time(key, cv)
|
||||
case []byte:
|
||||
event = event.Bytes(key, cv)
|
||||
default:
|
||||
event = event.Interface(key, v)
|
||||
}
|
||||
default:
|
||||
event = event.Interface(key, val.Any())
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// Verify at compile time that SlogHandler satisfies the slog.Handler interface.
|
||||
var _ slog.Handler = (*SlogHandler)(nil)
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
package zerolog_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/internal/cbor"
|
||||
)
|
||||
|
||||
func newSlogLogger(buf *bytes.Buffer) *slog.Logger {
|
||||
zl := zerolog.New(buf)
|
||||
return slog.New(zerolog.NewSlogHandler(zl))
|
||||
}
|
||||
|
||||
// decodeOutput converts the buffer contents to a JSON string,
|
||||
// handling CBOR-encoded output when built with the binary_log tag.
|
||||
func decodeOutput(buf *bytes.Buffer) string {
|
||||
p := buf.Bytes()
|
||||
if len(p) == 0 || p[0] < 0x7F {
|
||||
return buf.String()
|
||||
}
|
||||
return cbor.DecodeObjectToStr(p) + "\n"
|
||||
}
|
||||
|
||||
func decodeJSON(t *testing.T, buf *bytes.Buffer) map[string]interface{} {
|
||||
t.Helper()
|
||||
var m map[string]interface{}
|
||||
s := decodeOutput(buf)
|
||||
if err := json.Unmarshal([]byte(s), &m); err != nil {
|
||||
t.Fatalf("failed to decode JSON %q: %v", s, err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestSlogHandler_BasicInfo(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("hello world")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["level"] != "info" {
|
||||
t.Errorf("expected level info, got %v", m["level"])
|
||||
}
|
||||
if m["message"] != "hello world" {
|
||||
t.Errorf("expected message 'hello world', got %v", m["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_Debug(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf).Level(zerolog.DebugLevel)
|
||||
logger := slog.New(zerolog.NewSlogHandler(zl))
|
||||
|
||||
logger.Debug("debug msg")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["level"] != "debug" {
|
||||
t.Errorf("expected level debug, got %v", m["level"])
|
||||
}
|
||||
if m["message"] != "debug msg" {
|
||||
t.Errorf("expected message 'debug msg', got %v", m["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_Warn(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Warn("warn msg")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["level"] != "warn" {
|
||||
t.Errorf("expected level warn, got %v", m["level"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_Error(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Error("error msg")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["level"] != "error" {
|
||||
t.Errorf("expected level error, got %v", m["level"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithStringAttr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", "key", "value")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["key"] != "value" {
|
||||
t.Errorf("expected key=value, got %v", m["key"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithIntAttr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", slog.Int("count", 42))
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["count"] != float64(42) {
|
||||
t.Errorf("expected count=42, got %v", m["count"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithBoolAttr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", slog.Bool("flag", true))
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["flag"] != true {
|
||||
t.Errorf("expected flag=true, got %v", m["flag"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithFloat64Attr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", slog.Float64("pi", 3.14))
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["pi"] != 3.14 {
|
||||
t.Errorf("expected pi=3.14, got %v", m["pi"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithTimeAttr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
ts := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
|
||||
logger.Info("test", slog.Time("created", ts))
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["created"] == nil {
|
||||
t.Error("expected created field to be present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithDurationAttr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", slog.Duration("elapsed", 5*time.Second))
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["elapsed"] == nil {
|
||||
t.Error("expected elapsed field to be present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithErrorAttr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", slog.Any("err", errors.New("something failed")))
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["err"] != "something failed" {
|
||||
t.Errorf("expected err='something failed', got %v", m["err"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithAttrs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
child := handler.WithAttrs([]slog.Attr{
|
||||
slog.String("component", "auth"),
|
||||
slog.Int("version", 2),
|
||||
})
|
||||
logger := slog.New(child)
|
||||
|
||||
logger.Info("request handled")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["component"] != "auth" {
|
||||
t.Errorf("expected component=auth, got %v", m["component"])
|
||||
}
|
||||
if m["version"] != float64(2) {
|
||||
t.Errorf("expected version=2, got %v", m["version"])
|
||||
}
|
||||
if m["message"] != "request handled" {
|
||||
t.Errorf("expected message 'request handled', got %v", m["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithAttrsEmpty(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
// WithAttrs with empty slice should return same handler
|
||||
child := handler.WithAttrs(nil)
|
||||
if child != handler {
|
||||
t.Error("expected WithAttrs(nil) to return same handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithGroup(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
child := handler.WithGroup("request")
|
||||
logger := slog.New(child)
|
||||
|
||||
logger.Info("handled", "method", "GET", "status", 200)
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["request.method"] != "GET" {
|
||||
t.Errorf("expected request.method=GET, got %v", m["request.method"])
|
||||
}
|
||||
if m["request.status"] != float64(200) {
|
||||
t.Errorf("expected request.status=200, got %v", m["request.status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithGroupEmpty(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
// WithGroup with empty name should return same handler
|
||||
child := handler.WithGroup("")
|
||||
if child != handler {
|
||||
t.Error("expected WithGroup('') to return same handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithNestedGroups(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
child := handler.WithGroup("http").WithGroup("request")
|
||||
logger := slog.New(child)
|
||||
|
||||
logger.Info("handled", "method", "POST")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["http.request.method"] != "POST" {
|
||||
t.Errorf("expected http.request.method=POST, got %v", m["http.request.method"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithGroupAndAttrs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
child := handler.WithGroup("server").WithAttrs([]slog.Attr{
|
||||
slog.String("host", "localhost"),
|
||||
})
|
||||
logger := slog.New(child)
|
||||
|
||||
logger.Info("started", "port", 8080)
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["server.host"] != "localhost" {
|
||||
t.Errorf("expected server.host=localhost, got %v", m["server.host"])
|
||||
}
|
||||
if m["server.port"] != float64(8080) {
|
||||
t.Errorf("expected server.port=8080, got %v", m["server.port"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_GroupAttrInRecord(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", slog.Group("user",
|
||||
slog.String("name", "alice"),
|
||||
slog.Int("age", 30),
|
||||
))
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["user.name"] != "alice" {
|
||||
t.Errorf("expected user.name=alice, got %v", m["user.name"])
|
||||
}
|
||||
if m["user.age"] != float64(30) {
|
||||
t.Errorf("expected user.age=30, got %v", m["user.age"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_LevelFiltering(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf).Level(zerolog.WarnLevel)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
// Debug should be filtered
|
||||
if handler.Enabled(nil, slog.LevelDebug) {
|
||||
t.Error("expected debug to be filtered at warn level")
|
||||
}
|
||||
// Info should be filtered
|
||||
if handler.Enabled(nil, slog.LevelInfo) {
|
||||
t.Error("expected info to be filtered at warn level")
|
||||
}
|
||||
// Warn should pass
|
||||
if !handler.Enabled(nil, slog.LevelWarn) {
|
||||
t.Error("expected warn to be enabled at warn level")
|
||||
}
|
||||
// Error should pass
|
||||
if !handler.Enabled(nil, slog.LevelError) {
|
||||
t.Error("expected error to be enabled at warn level")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_FilteredMessageNotWritten(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf).Level(zerolog.ErrorLevel)
|
||||
logger := slog.New(zerolog.NewSlogHandler(zl))
|
||||
|
||||
logger.Info("should not appear")
|
||||
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("expected no output for filtered message, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_MultipleAttrs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("multi",
|
||||
slog.String("a", "1"),
|
||||
slog.Int("b", 2),
|
||||
slog.Bool("c", true),
|
||||
slog.Float64("d", 3.5),
|
||||
)
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["a"] != "1" {
|
||||
t.Errorf("expected a=1, got %v", m["a"])
|
||||
}
|
||||
if m["b"] != float64(2) {
|
||||
t.Errorf("expected b=2, got %v", m["b"])
|
||||
}
|
||||
if m["c"] != true {
|
||||
t.Errorf("expected c=true, got %v", m["c"])
|
||||
}
|
||||
if m["d"] != 3.5 {
|
||||
t.Errorf("expected d=3.5, got %v", m["d"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_LogValuer(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("test", "addr", testLogValuer{host: "example.com", port: 443})
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
// LogValuer resolves to a group
|
||||
if m["addr.host"] != "example.com" {
|
||||
t.Errorf("expected addr.host=example.com, got %v", m["addr.host"])
|
||||
}
|
||||
if m["addr.port"] != float64(443) {
|
||||
t.Errorf("expected addr.port=443, got %v", m["addr.port"])
|
||||
}
|
||||
}
|
||||
|
||||
type testLogValuer struct {
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
func (v testLogValuer) LogValue() slog.Value {
|
||||
return slog.GroupValue(
|
||||
slog.String("host", v.host),
|
||||
slog.Int("port", v.port),
|
||||
)
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithAttrsImmutability(t *testing.T) {
|
||||
var buf1, buf2 bytes.Buffer
|
||||
zl1 := zerolog.New(&buf1)
|
||||
zl2 := zerolog.New(&buf2)
|
||||
|
||||
handler := zerolog.NewSlogHandler(zl1)
|
||||
child1 := handler.WithAttrs([]slog.Attr{slog.String("from", "child1")})
|
||||
_ = zerolog.NewSlogHandler(zl2).WithAttrs([]slog.Attr{slog.String("from", "child2")})
|
||||
|
||||
slog.New(child1).Info("test")
|
||||
|
||||
m := decodeJSON(t, &buf1)
|
||||
if m["from"] != "child1" {
|
||||
t.Errorf("expected from=child1, got %v", m["from"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_LevelMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
slogLevel slog.Level
|
||||
wantLevel string
|
||||
}{
|
||||
{slog.LevelDebug - 4, "trace"},
|
||||
{slog.LevelDebug, "debug"},
|
||||
{slog.LevelInfo, "info"},
|
||||
{slog.LevelWarn, "warn"},
|
||||
{slog.LevelError, "error"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf).Level(zerolog.TraceLevel)
|
||||
logger := slog.New(zerolog.NewSlogHandler(zl))
|
||||
|
||||
logger.Log(nil, tt.slogLevel, "test")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["level"] != tt.wantLevel {
|
||||
t.Errorf("slog level %d: expected zerolog level %q, got %q",
|
||||
tt.slogLevel, tt.wantLevel, m["level"])
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_EmptyMessage(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newSlogLogger(&buf)
|
||||
|
||||
logger.Info("", "key", "val")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["key"] != "val" {
|
||||
t.Errorf("expected key=val, got %v", m["key"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_WithContext(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf).With().Str("service", "api").Logger()
|
||||
logger := slog.New(zerolog.NewSlogHandler(zl))
|
||||
|
||||
logger.Info("request")
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m["service"] != "api" {
|
||||
t.Errorf("expected service=api, got %v", m["service"])
|
||||
}
|
||||
if m["message"] != "request" {
|
||||
t.Errorf("expected message 'request', got %v", m["message"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_EnabledRespectsGlobalLevel(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zl := zerolog.New(&buf).Level(zerolog.DebugLevel)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
// Logger level is debug, so info should be enabled
|
||||
if !handler.Enabled(nil, slog.LevelInfo) {
|
||||
t.Fatal("expected info to be enabled before setting global level")
|
||||
}
|
||||
|
||||
// Set global level to error
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
defer zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
||||
|
||||
// Now info should be disabled even though logger level allows it
|
||||
if handler.Enabled(nil, slog.LevelInfo) {
|
||||
t.Error("expected info to be disabled when GlobalLevel is error")
|
||||
}
|
||||
// Error should still be enabled
|
||||
if !handler.Enabled(nil, slog.LevelError) {
|
||||
t.Error("expected error to be enabled when GlobalLevel is error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_EnabledNilWriter(t *testing.T) {
|
||||
zl := zerolog.Nop()
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
if handler.Enabled(nil, slog.LevelError) {
|
||||
t.Error("expected disabled for nop logger")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_HandlePropagatesContext(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
type ctxKey struct{}
|
||||
ctx := context.WithValue(context.Background(), ctxKey{}, "test-value")
|
||||
|
||||
var gotCtx context.Context
|
||||
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
gotCtx = e.GetCtx()
|
||||
})
|
||||
|
||||
zl := zerolog.New(&buf).Hook(hook)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "test", 0)
|
||||
_ = handler.Handle(ctx, record)
|
||||
|
||||
if gotCtx == nil {
|
||||
t.Fatal("expected context to be propagated to event")
|
||||
}
|
||||
if gotCtx.Value(ctxKey{}) != "test-value" {
|
||||
t.Error("expected context value to be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_NoDuplicateTimestamp(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Create logger with Timestamp() hook - this adds "time" automatically
|
||||
zl := zerolog.New(&buf).With().Timestamp().Logger()
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "test", 0)
|
||||
_ = handler.Handle(context.Background(), record)
|
||||
|
||||
output := decodeOutput(&buf)
|
||||
// Count occurrences of the timestamp field name - should appear exactly once
|
||||
count := 0
|
||||
for i := 0; i < len(output); i++ {
|
||||
if i+4 <= len(output) && output[i:i+4] == "time" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
t.Errorf("expected at most 1 timestamp field, got %d in output: %s", count, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlogHandler_TimestampWithoutHook(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Logger without Timestamp() hook - Handle should add the timestamp
|
||||
zl := zerolog.New(&buf)
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
|
||||
ts := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
|
||||
record := slog.NewRecord(ts, slog.LevelInfo, "test", 0)
|
||||
_ = handler.Handle(context.Background(), record)
|
||||
|
||||
m := decodeJSON(t, &buf)
|
||||
if m[zerolog.TimestampFieldName] == nil {
|
||||
t.Error("expected timestamp field when logger has no timestamp hook")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// See http://cee.mitre.org/language/1.0-beta1/clt.html#syslog
|
||||
// or https://www.rsyslog.com/json-elasticsearch/
|
||||
const ceePrefix = "@cee:"
|
||||
|
||||
// SyslogWriter is an interface matching a syslog.Writer struct.
|
||||
type SyslogWriter interface {
|
||||
io.Writer
|
||||
@@ -19,39 +23,67 @@ type SyslogWriter interface {
|
||||
}
|
||||
|
||||
type syslogWriter struct {
|
||||
w SyslogWriter
|
||||
w SyslogWriter
|
||||
prefix string
|
||||
}
|
||||
|
||||
// SyslogLevelWriter wraps a SyslogWriter and call the right syslog level
|
||||
// method matching the zerolog level.
|
||||
func SyslogLevelWriter(w SyslogWriter) LevelWriter {
|
||||
return syslogWriter{w}
|
||||
return syslogWriter{w, ""}
|
||||
}
|
||||
|
||||
// SyslogCEEWriter wraps a SyslogWriter with a SyslogLevelWriter that adds a
|
||||
// MITRE CEE prefix for JSON syslog entries, compatible with rsyslog
|
||||
// and syslog-ng JSON logging support.
|
||||
// See https://www.rsyslog.com/json-elasticsearch/
|
||||
func SyslogCEEWriter(w SyslogWriter) LevelWriter {
|
||||
return syslogWriter{w, ceePrefix}
|
||||
}
|
||||
|
||||
func (sw syslogWriter) Write(p []byte) (n int, err error) {
|
||||
return sw.w.Write(p)
|
||||
var pn int
|
||||
if sw.prefix != "" {
|
||||
pn, err = sw.w.Write([]byte(sw.prefix))
|
||||
if err != nil {
|
||||
return pn, err
|
||||
}
|
||||
}
|
||||
n, err = sw.w.Write(p)
|
||||
return pn + n, err
|
||||
}
|
||||
|
||||
// WriteLevel implements LevelWriter interface.
|
||||
func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
case DebugLevel:
|
||||
err = sw.w.Debug(string(p))
|
||||
err = sw.w.Debug(sw.prefix + string(p))
|
||||
case InfoLevel:
|
||||
err = sw.w.Info(string(p))
|
||||
err = sw.w.Info(sw.prefix + string(p))
|
||||
case WarnLevel:
|
||||
err = sw.w.Warning(string(p))
|
||||
err = sw.w.Warning(sw.prefix + string(p))
|
||||
case ErrorLevel:
|
||||
err = sw.w.Err(string(p))
|
||||
err = sw.w.Err(sw.prefix + string(p))
|
||||
case FatalLevel:
|
||||
err = sw.w.Emerg(string(p))
|
||||
err = sw.w.Emerg(sw.prefix + string(p))
|
||||
case PanicLevel:
|
||||
err = sw.w.Crit(string(p))
|
||||
err = sw.w.Crit(sw.prefix + string(p))
|
||||
case NoLevel:
|
||||
err = sw.w.Info(string(p))
|
||||
err = sw.w.Info(sw.prefix + string(p))
|
||||
default:
|
||||
panic("invalid level")
|
||||
}
|
||||
// Any CEE prefix is not part of the message, so we don't include its length
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (sw syslogWriter) Close() error {
|
||||
if c, ok := sw.w.(io.Closer); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+176
-3
@@ -3,8 +3,13 @@
|
||||
|
||||
package zerolog
|
||||
|
||||
import "testing"
|
||||
import "reflect"
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type syslogEvent struct {
|
||||
level string
|
||||
@@ -15,7 +20,11 @@ type syslogTestWriter struct {
|
||||
}
|
||||
|
||||
func (w *syslogTestWriter) Write(p []byte) (int, error) {
|
||||
return 0, nil
|
||||
return len(p), nil
|
||||
}
|
||||
func (w *syslogTestWriter) Trace(m string) error {
|
||||
w.events = append(w.events, syslogEvent{"Trace", m})
|
||||
return nil
|
||||
}
|
||||
func (w *syslogTestWriter) Debug(m string) error {
|
||||
w.events = append(w.events, syslogEvent{"Debug", m})
|
||||
@@ -45,6 +54,7 @@ func (w *syslogTestWriter) Crit(m string) error {
|
||||
func TestSyslogWriter(t *testing.T) {
|
||||
sw := &syslogTestWriter{}
|
||||
log := New(SyslogLevelWriter(sw))
|
||||
log.Trace().Msg("trace")
|
||||
log.Debug().Msg("debug")
|
||||
log.Info().Msg("info")
|
||||
log.Warn().Msg("warn")
|
||||
@@ -61,3 +71,166 @@ func TestSyslogWriter(t *testing.T) {
|
||||
t.Errorf("Invalid syslog message routing: want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
type testCEEwriter struct {
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
// Only implement one method as we're just testing the prefixing
|
||||
func (c testCEEwriter) Debug(m string) error { return nil }
|
||||
|
||||
func (c testCEEwriter) Info(m string) error {
|
||||
_, err := c.buf.Write([]byte(m))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c testCEEwriter) Warning(m string) error { return nil }
|
||||
|
||||
func (c testCEEwriter) Err(m string) error { return nil }
|
||||
|
||||
func (c testCEEwriter) Emerg(m string) error { return nil }
|
||||
|
||||
func (c testCEEwriter) Crit(m string) error { return nil }
|
||||
|
||||
func (c testCEEwriter) Write(b []byte) (int, error) {
|
||||
return c.buf.Write(b)
|
||||
}
|
||||
|
||||
func TestSyslogWriter_WithCEE(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
sw := testCEEwriter{&buf}
|
||||
log := New(SyslogCEEWriter(sw))
|
||||
log.Info().Str("key", "value").Msg("message string")
|
||||
got := buf.String()
|
||||
want := "@cee:{"
|
||||
if !strings.HasPrefix(got, want) {
|
||||
t.Errorf("Bad CEE message start: want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
type errorSyslogWriter struct {
|
||||
*syslogTestWriter
|
||||
writeError error
|
||||
}
|
||||
|
||||
func (w *errorSyslogWriter) Write(p []byte) (int, error) {
|
||||
if w.writeError != nil {
|
||||
return 0, w.writeError
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestSyslogWriter_Write(t *testing.T) {
|
||||
// Test Write method without prefix
|
||||
sw := &syslogTestWriter{}
|
||||
writer := SyslogLevelWriter(sw)
|
||||
|
||||
data := []byte("test message")
|
||||
n, err := writer.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("Write failed: %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Errorf("Write returned wrong length: got %d, want %d", n, len(data))
|
||||
}
|
||||
|
||||
// Test Write method with CEE prefix
|
||||
sw2 := &syslogTestWriter{}
|
||||
writer2 := SyslogCEEWriter(sw2)
|
||||
|
||||
data2 := []byte("test message")
|
||||
n2, err2 := writer2.Write(data2)
|
||||
if err2 != nil {
|
||||
t.Errorf("Write with CEE failed: %v", err2)
|
||||
}
|
||||
expectedLen := len(ceePrefix) + len(data2)
|
||||
if n2 != expectedLen {
|
||||
t.Errorf("Write with CEE returned wrong length: got %d, want %d", n2, expectedLen)
|
||||
}
|
||||
|
||||
// Test Write method with CEE prefix and error on prefix write
|
||||
sw3 := &errorSyslogWriter{syslogTestWriter: &syslogTestWriter{}, writeError: io.EOF}
|
||||
writer3 := SyslogCEEWriter(sw3)
|
||||
|
||||
_, err3 := writer3.Write(data2)
|
||||
if err3 != io.EOF {
|
||||
t.Errorf("Write with CEE error failed: got %v, want %v", err3, io.EOF)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyslogWriter_WriteLevel_AllLevels(t *testing.T) {
|
||||
sw := &syslogTestWriter{}
|
||||
writer := SyslogLevelWriter(sw)
|
||||
|
||||
// Test all levels to ensure full coverage
|
||||
writer.WriteLevel(TraceLevel, []byte(`{"level":"trace","message":"trace"}`+"\n"))
|
||||
writer.WriteLevel(DebugLevel, []byte(`{"level":"debug","message":"debug"}`+"\n"))
|
||||
writer.WriteLevel(InfoLevel, []byte(`{"level":"info","message":"info"}`+"\n"))
|
||||
writer.WriteLevel(WarnLevel, []byte(`{"level":"warn","message":"warn"}`+"\n"))
|
||||
writer.WriteLevel(ErrorLevel, []byte(`{"level":"error","message":"error"}`+"\n"))
|
||||
writer.WriteLevel(FatalLevel, []byte(`{"level":"fatal","message":"fatal"}`+"\n"))
|
||||
writer.WriteLevel(PanicLevel, []byte(`{"level":"panic","message":"panic"}`+"\n"))
|
||||
writer.WriteLevel(NoLevel, []byte(`{"message":"nolevel"}`+"\n"))
|
||||
|
||||
want := []syslogEvent{
|
||||
{"Debug", `{"level":"debug","message":"debug"}` + "\n"},
|
||||
{"Info", `{"level":"info","message":"info"}` + "\n"},
|
||||
{"Warning", `{"level":"warn","message":"warn"}` + "\n"},
|
||||
{"Err", `{"level":"error","message":"error"}` + "\n"},
|
||||
{"Emerg", `{"level":"fatal","message":"fatal"}` + "\n"},
|
||||
{"Crit", `{"level":"panic","message":"panic"}` + "\n"},
|
||||
{"Info", `{"message":"nolevel"}` + "\n"},
|
||||
}
|
||||
if got := sw.events; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Invalid syslog message routing: want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
type closableSyslogWriter struct {
|
||||
*syslogTestWriter
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (w *closableSyslogWriter) Close() error {
|
||||
w.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSyslogWriter_Close(t *testing.T) {
|
||||
// Test with closable writer
|
||||
sw := &closableSyslogWriter{syslogTestWriter: &syslogTestWriter{}}
|
||||
writer := SyslogLevelWriter(sw).(syslogWriter) // Cast to concrete type to access Close
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
if !sw.closed {
|
||||
t.Error("Close was not called on underlying writer")
|
||||
}
|
||||
|
||||
// Test with non-closable writer
|
||||
sw2 := &syslogTestWriter{}
|
||||
writer2 := SyslogLevelWriter(sw2).(syslogWriter) // Cast to concrete type to access Close
|
||||
|
||||
err = writer2.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed for non-closable writer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyslogWriter_WriteLevel_InvalidLevel(t *testing.T) {
|
||||
sw := &syslogTestWriter{}
|
||||
writer := SyslogLevelWriter(sw)
|
||||
|
||||
// Test invalid level - should panic
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("Expected panic for invalid level")
|
||||
} else if r != "invalid level" {
|
||||
t.Errorf("Expected panic 'invalid level', got %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
writer.WriteLevel(Level(100), []byte("test"))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -12,30 +17,39 @@ type LevelWriter interface {
|
||||
WriteLevel(level Level, p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type levelWriterAdapter struct {
|
||||
// LevelWriterAdapter adapts an io.Writer to support the LevelWriter interface.
|
||||
type LevelWriterAdapter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (lw levelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
// WriteLevel simply writes everything to the adapted writer, ignoring the level.
|
||||
func (lw LevelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
return lw.Write(p)
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (lw LevelWriterAdapter) Close() error {
|
||||
if closer, ok := lw.Writer.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type syncWriter struct {
|
||||
mu sync.Mutex
|
||||
lw LevelWriter
|
||||
}
|
||||
|
||||
// SyncWriter wraps w so that each call to Write is synchronized with a mutex.
|
||||
// This syncer can be the call to writer's Write method is not thread safe.
|
||||
// Note that os.File Write operation is using write() syscall which is supposed
|
||||
// to be thread-safe on POSIX systems. So there is no need to use this with
|
||||
// os.File on such systems as zerolog guaranties to issue a single Write call
|
||||
// per log event.
|
||||
// This syncer can be used to wrap the call to writer's Write method if it is
|
||||
// not thread safe. Note that you do not need this wrapper for os.File Write
|
||||
// operations on POSIX and Windows systems as they are already thread-safe.
|
||||
func SyncWriter(w io.Writer) io.Writer {
|
||||
if lw, ok := w.(LevelWriter); ok {
|
||||
return &syncWriter{lw: lw}
|
||||
}
|
||||
return &syncWriter{lw: levelWriterAdapter{w}}
|
||||
return &syncWriter{lw: LevelWriterAdapter{w}}
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface.
|
||||
@@ -52,36 +66,59 @@ func (s *syncWriter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
return s.lw.WriteLevel(l, p)
|
||||
}
|
||||
|
||||
func (s *syncWriter) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if closer, ok := s.lw.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type multiLevelWriter struct {
|
||||
writers []LevelWriter
|
||||
}
|
||||
|
||||
func (t multiLevelWriter) Write(p []byte) (n int, err error) {
|
||||
for _, w := range t.writers {
|
||||
n, err = w.Write(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n != len(p) {
|
||||
err = io.ErrShortWrite
|
||||
return
|
||||
if _n, _err := w.Write(p); err == nil {
|
||||
n = _n
|
||||
if _err != nil {
|
||||
err = _err
|
||||
} else if _n != len(p) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (t multiLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
for _, w := range t.writers {
|
||||
n, err = w.WriteLevel(l, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n != len(p) {
|
||||
err = io.ErrShortWrite
|
||||
return
|
||||
if _n, _err := w.WriteLevel(l, p); err == nil {
|
||||
n = _n
|
||||
if _err != nil {
|
||||
err = _err
|
||||
} else if _n != len(p) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Calls close on all the underlying writers that are io.Closers. If any of the
|
||||
// Close methods return an error, the remainder of the closers are not closed
|
||||
// and the error is returned.
|
||||
func (t multiLevelWriter) Close() error {
|
||||
for _, w := range t.writers {
|
||||
if closer, ok := w.(io.Closer); ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiLevelWriter creates a writer that duplicates its writes to all the
|
||||
@@ -93,8 +130,226 @@ func MultiLevelWriter(writers ...io.Writer) LevelWriter {
|
||||
if lw, ok := w.(LevelWriter); ok {
|
||||
lwriters = append(lwriters, lw)
|
||||
} else {
|
||||
lwriters = append(lwriters, levelWriterAdapter{w})
|
||||
lwriters = append(lwriters, LevelWriterAdapter{w})
|
||||
}
|
||||
}
|
||||
return multiLevelWriter{lwriters}
|
||||
}
|
||||
|
||||
// TestingLog is the logging interface of testing.TB.
|
||||
type TestingLog interface {
|
||||
Log(args ...interface{})
|
||||
Logf(format string, args ...interface{})
|
||||
Helper()
|
||||
}
|
||||
|
||||
// TestWriter is a writer that writes to testing.TB.
|
||||
type TestWriter struct {
|
||||
T TestingLog
|
||||
|
||||
// Frame skips caller frames to capture the original file and line numbers.
|
||||
Frame int
|
||||
}
|
||||
|
||||
// NewTestWriter creates a writer that logs to the testing.TB.
|
||||
func NewTestWriter(t TestingLog) TestWriter {
|
||||
return TestWriter{T: t}
|
||||
}
|
||||
|
||||
// Write to testing.TB.
|
||||
func (t TestWriter) Write(p []byte) (n int, err error) {
|
||||
t.T.Helper()
|
||||
|
||||
n = len(p)
|
||||
|
||||
// Strip trailing newline because t.Log always adds one.
|
||||
p = bytes.TrimRight(p, "\n")
|
||||
|
||||
// Try to correct the log file and line number to the caller.
|
||||
if t.Frame > 0 {
|
||||
_, origFile, origLine, _ := runtime.Caller(1)
|
||||
_, frameFile, frameLine, ok := runtime.Caller(1 + t.Frame)
|
||||
if ok {
|
||||
erase := strings.Repeat("\b", len(path.Base(origFile))+len(strconv.Itoa(origLine))+3)
|
||||
t.T.Logf("%s%s:%d: %s", erase, path.Base(frameFile), frameLine, p)
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
t.T.Log(string(p))
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ConsoleTestWriter creates an option that correctly sets the file frame depth for testing.TB log.
|
||||
func ConsoleTestWriter(t TestingLog) func(w *ConsoleWriter) {
|
||||
return func(w *ConsoleWriter) {
|
||||
w.Out = TestWriter{T: t, Frame: 6}
|
||||
}
|
||||
}
|
||||
|
||||
// FilteredLevelWriter writes only logs at Level or above to Writer.
|
||||
//
|
||||
// It should be used only in combination with MultiLevelWriter when you
|
||||
// want to write to multiple destinations at different levels. Otherwise
|
||||
// you should just set the level on the logger and filter events early.
|
||||
// When using MultiLevelWriter then you set the level on the logger to
|
||||
// the lowest of the levels you use for writers.
|
||||
type FilteredLevelWriter struct {
|
||||
Writer LevelWriter
|
||||
Level Level
|
||||
}
|
||||
|
||||
// Write writes to the underlying Writer.
|
||||
func (w *FilteredLevelWriter) Write(p []byte) (int, error) {
|
||||
return w.Writer.Write(p)
|
||||
}
|
||||
|
||||
// WriteLevel calls WriteLevel of the underlying Writer only if the level is equal
|
||||
// or above the Level.
|
||||
func (w *FilteredLevelWriter) WriteLevel(level Level, p []byte) (int, error) {
|
||||
if level >= w.Level {
|
||||
return w.Writer.WriteLevel(level, p)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (w *FilteredLevelWriter) Close() error {
|
||||
if closer, ok := w.Writer.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var triggerWriterPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
},
|
||||
}
|
||||
|
||||
// TriggerLevelWriter buffers log lines at the ConditionalLevel or below
|
||||
// until a trigger level (or higher) line is emitted. Log lines with level
|
||||
// higher than ConditionalLevel are always written out to the destination
|
||||
// writer. If trigger never happens, buffered log lines are never written out.
|
||||
//
|
||||
// It can be used to configure "log level per request".
|
||||
type TriggerLevelWriter struct {
|
||||
// Destination writer. If LevelWriter is provided (usually), its WriteLevel is used
|
||||
// instead of Write.
|
||||
io.Writer
|
||||
|
||||
// ConditionalLevel is the level (and below) at which lines are buffered until
|
||||
// a trigger level (or higher) line is emitted. Usually this is set to DebugLevel.
|
||||
ConditionalLevel Level
|
||||
|
||||
// TriggerLevel is the lowest level that triggers the sending of the conditional
|
||||
// level lines. Usually this is set to ErrorLevel.
|
||||
TriggerLevel Level
|
||||
|
||||
buf *bytes.Buffer
|
||||
triggered bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *TriggerLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
// At first trigger level or above log line, we flush the buffer and change the
|
||||
// trigger state to triggered.
|
||||
if !w.triggered && l >= w.TriggerLevel {
|
||||
err := w.trigger()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Unless triggered, we buffer everything at and below ConditionalLevel.
|
||||
if !w.triggered && l <= w.ConditionalLevel {
|
||||
if w.buf == nil {
|
||||
w.buf = triggerWriterPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// We prefix each log line with a byte with the level.
|
||||
// Hopefully we will never have a level value which equals a newline
|
||||
// (which could interfere with reconstruction of log lines in the trigger method).
|
||||
w.buf.WriteByte(byte(l))
|
||||
w.buf.Write(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Anything above ConditionalLevel is always passed through.
|
||||
// Once triggered, everything is passed through.
|
||||
if lw, ok := w.Writer.(LevelWriter); ok {
|
||||
return lw.WriteLevel(l, p)
|
||||
}
|
||||
return w.Write(p)
|
||||
}
|
||||
|
||||
// trigger expects lock to be held.
|
||||
func (w *TriggerLevelWriter) trigger() error {
|
||||
if w.triggered {
|
||||
return nil
|
||||
}
|
||||
w.triggered = true
|
||||
|
||||
if w.buf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := w.buf.Bytes()
|
||||
for len(p) > 0 {
|
||||
// We do not use bufio.Scanner here because we already have full buffer
|
||||
// in the memory and we do not want extra copying from the buffer to
|
||||
// scanner's token slice, nor we want to hit scanner's token size limit,
|
||||
// and we also want to preserve newlines.
|
||||
i := bytes.IndexByte(p, '\n')
|
||||
line := p[0 : i+1]
|
||||
p = p[i+1:]
|
||||
// We prefixed each log line with a byte with the level.
|
||||
level := Level(line[0])
|
||||
line = line[1:]
|
||||
var err error
|
||||
if lw, ok := w.Writer.(LevelWriter); ok {
|
||||
_, err = lw.WriteLevel(level, line)
|
||||
} else {
|
||||
_, err = w.Write(line)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Trigger forces flushing the buffer and change the trigger state to
|
||||
// triggered, if the writer has not already been triggered before.
|
||||
func (w *TriggerLevelWriter) Trigger() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
return w.trigger()
|
||||
}
|
||||
|
||||
// Close closes the writer and returns the buffer to the pool.
|
||||
func (w *TriggerLevelWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.buf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We return the buffer only if it has not grown above the limit.
|
||||
// This prevents accumulation of large buffers in the pool just
|
||||
// because occasionally a large buffer might be needed.
|
||||
if w.buf.Cap() <= TriggerLevelWriterBufferReuseLimit {
|
||||
w.buf.Reset()
|
||||
triggerWriterPool.Put(w.buf)
|
||||
}
|
||||
w.buf = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+523
-2
@@ -1,13 +1,47 @@
|
||||
// +build !binary_log
|
||||
// +build !windows
|
||||
//go:build !binary_log && !windows
|
||||
// +build !binary_log,!windows
|
||||
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type closableBuffer struct {
|
||||
*bytes.Buffer
|
||||
closed bool
|
||||
closeError error
|
||||
}
|
||||
|
||||
func (cb *closableBuffer) Close() error {
|
||||
cb.closed = true
|
||||
return cb.closeError
|
||||
}
|
||||
|
||||
type errorWriter struct {
|
||||
writeError error
|
||||
shortWrite bool
|
||||
}
|
||||
|
||||
func (ew *errorWriter) Write(p []byte) (int, error) {
|
||||
if ew.writeError != nil {
|
||||
return 0, ew.writeError
|
||||
}
|
||||
if ew.shortWrite {
|
||||
return len(p) - 1, nil // Return short write
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (ew *errorWriter) WriteLevel(level Level, p []byte) (int, error) {
|
||||
return ew.Write(p)
|
||||
}
|
||||
|
||||
func TestMultiSyslogWriter(t *testing.T) {
|
||||
sw := &syslogTestWriter{}
|
||||
log := New(MultiLevelWriter(SyslogLevelWriter(sw)))
|
||||
@@ -27,3 +61,490 @@ func TestMultiSyslogWriter(t *testing.T) {
|
||||
t.Errorf("Invalid syslog message routing: want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
var writeCalls int
|
||||
|
||||
type mockedWriter struct {
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
func (c mockedWriter) Write(p []byte) (int, error) {
|
||||
writeCalls++
|
||||
|
||||
if c.wantErr {
|
||||
return -1, errors.New("Expected error")
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Tests that a new writer is only used if it actually works.
|
||||
func TestResilientMultiWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
writers []io.Writer
|
||||
}{
|
||||
{
|
||||
name: "All valid writers",
|
||||
writers: []io.Writer{
|
||||
mockedWriter{
|
||||
wantErr: false,
|
||||
},
|
||||
mockedWriter{
|
||||
wantErr: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "All invalid writers",
|
||||
writers: []io.Writer{
|
||||
mockedWriter{
|
||||
wantErr: true,
|
||||
},
|
||||
mockedWriter{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "First invalid writer",
|
||||
writers: []io.Writer{
|
||||
mockedWriter{
|
||||
wantErr: true,
|
||||
},
|
||||
mockedWriter{
|
||||
wantErr: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "First valid writer",
|
||||
writers: []io.Writer{
|
||||
mockedWriter{
|
||||
wantErr: false,
|
||||
},
|
||||
mockedWriter{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
writers := tt.writers
|
||||
multiWriter := MultiLevelWriter(writers...)
|
||||
|
||||
logger := New(multiWriter).With().Timestamp().Logger().Level(InfoLevel)
|
||||
logger.Info().Msg("Test msg")
|
||||
|
||||
if len(writers) != writeCalls {
|
||||
t.Errorf("Expected %d writers to have been called but only %d were.", len(writers), writeCalls)
|
||||
}
|
||||
writeCalls = 0
|
||||
}
|
||||
}
|
||||
|
||||
type testingLog struct {
|
||||
testing.TB
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *testingLog) Log(args ...interface{}) {
|
||||
if _, err := t.buf.WriteString(fmt.Sprint(args...)); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testingLog) Logf(format string, args ...interface{}) {
|
||||
if _, err := t.buf.WriteString(fmt.Sprintf(format, args...)); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
write []byte
|
||||
want []byte
|
||||
}{{
|
||||
name: "newline",
|
||||
write: []byte("newline\n"),
|
||||
want: []byte("newline"),
|
||||
}, {
|
||||
name: "oneline",
|
||||
write: []byte("oneline"),
|
||||
want: []byte("oneline"),
|
||||
}, {
|
||||
name: "twoline",
|
||||
write: []byte("twoline\n\n"),
|
||||
want: []byte("twoline"),
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tb := &testingLog{TB: t} // Capture TB log buffer.
|
||||
w := TestWriter{T: tb}
|
||||
|
||||
n, err := w.Write(tt.write)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if n != len(tt.write) {
|
||||
t.Errorf("Expected %d write length but got %d", len(tt.write), n)
|
||||
}
|
||||
p := tb.buf.Bytes()
|
||||
if !bytes.Equal(tt.want, p) {
|
||||
t.Errorf("Expected %q, got %q.", tt.want, p)
|
||||
}
|
||||
|
||||
log := New(NewConsoleWriter(ConsoleTestWriter(t)))
|
||||
log.Info().Str("name", tt.name).Msg("Success!")
|
||||
|
||||
tb.buf.Reset()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFilteredLevelWriter(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
writer := FilteredLevelWriter{
|
||||
Writer: LevelWriterAdapter{&buf},
|
||||
Level: InfoLevel,
|
||||
}
|
||||
_, err := writer.WriteLevel(DebugLevel, []byte("no"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = writer.WriteLevel(InfoLevel, []byte("yes"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
p := buf.Bytes()
|
||||
if want := "yes"; !bytes.Equal([]byte(want), p) {
|
||||
t.Errorf("Expected %q, got %q.", want, p)
|
||||
}
|
||||
}
|
||||
|
||||
type testWrite struct {
|
||||
Level
|
||||
Line []byte
|
||||
}
|
||||
|
||||
func TestTriggerLevelWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
write []testWrite
|
||||
want []byte
|
||||
all []byte
|
||||
}{{
|
||||
[]testWrite{
|
||||
{DebugLevel, []byte("no\n")},
|
||||
{InfoLevel, []byte("yes\n")},
|
||||
},
|
||||
[]byte("yes\n"),
|
||||
[]byte("yes\nno\n"),
|
||||
}, {
|
||||
[]testWrite{
|
||||
{DebugLevel, []byte("yes1\n")},
|
||||
{InfoLevel, []byte("yes2\n")},
|
||||
{ErrorLevel, []byte("yes3\n")},
|
||||
{DebugLevel, []byte("yes4\n")},
|
||||
},
|
||||
[]byte("yes2\nyes1\nyes3\nyes4\n"),
|
||||
[]byte("yes2\nyes1\nyes3\nyes4\n"),
|
||||
}}
|
||||
|
||||
for k, tt := range tests {
|
||||
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
writer := TriggerLevelWriter{Writer: LevelWriterAdapter{&buf}, ConditionalLevel: DebugLevel, TriggerLevel: ErrorLevel}
|
||||
t.Cleanup(func() { writer.Close() })
|
||||
for _, w := range tt.write {
|
||||
_, err := writer.WriteLevel(w.Level, w.Line)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
p := buf.Bytes()
|
||||
if want := tt.want; !bytes.Equal([]byte(want), p) {
|
||||
t.Errorf("Expected %q, got %q.", want, p)
|
||||
}
|
||||
err := writer.Trigger()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
p = buf.Bytes()
|
||||
if want := tt.all; !bytes.Equal([]byte(want), p) {
|
||||
t.Errorf("Expected %q, got %q.", want, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelWriterAdapter_Close(t *testing.T) {
|
||||
// Test with closable writer
|
||||
buf := &bytes.Buffer{}
|
||||
adapter := LevelWriterAdapter{Writer: buf}
|
||||
|
||||
// bytes.Buffer doesn't implement io.Closer, so Close should return nil
|
||||
err := adapter.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close should not return error for non-closable writer: %v", err)
|
||||
}
|
||||
|
||||
// Test with closable writer
|
||||
closableBuf := &closableBuffer{Buffer: &bytes.Buffer{}}
|
||||
adapter2 := LevelWriterAdapter{Writer: closableBuf}
|
||||
|
||||
err = adapter2.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close should not return error: %v", err)
|
||||
}
|
||||
if !closableBuf.closed {
|
||||
t.Error("Close should have been called on closable writer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncWriter(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// Test SyncWriter with regular io.Writer
|
||||
syncWriter := SyncWriter(buf)
|
||||
|
||||
// Test Write
|
||||
data := []byte("test data")
|
||||
n, err := syncWriter.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("Write failed: %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Errorf("Write returned wrong length: got %d, want %d", n, len(data))
|
||||
}
|
||||
if got := buf.String(); got != string(data) {
|
||||
t.Errorf("Write wrote wrong data: got %q, want %q", got, string(data))
|
||||
}
|
||||
|
||||
// Test SyncWriter with LevelWriter - use it with a logger
|
||||
levelBuf := &bytes.Buffer{}
|
||||
levelWriter := LevelWriterAdapter{levelBuf}
|
||||
syncLevelWriter := SyncWriter(levelWriter)
|
||||
|
||||
logger := New(syncLevelWriter)
|
||||
logger.Info().Msg("test message")
|
||||
|
||||
expected := `{"level":"info","message":"test message"}` + "\n"
|
||||
if got := levelBuf.String(); got != expected {
|
||||
t.Errorf("SyncWriter with LevelWriter failed: got %q, want %q", got, expected)
|
||||
}
|
||||
|
||||
// Test SyncWriter Close with closable writer
|
||||
closableBuf := &closableBuffer{Buffer: &bytes.Buffer{}, closed: false}
|
||||
closableSyncWriter := SyncWriter(closableBuf)
|
||||
|
||||
if closeable, ok := closableSyncWriter.(io.Closer); !ok {
|
||||
t.Error("SyncWriter should implement Close method")
|
||||
} else {
|
||||
err := closeable.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
}
|
||||
if !closableBuf.closed {
|
||||
t.Error("Close should have been called on closable writer")
|
||||
}
|
||||
|
||||
// Test SyncWriter Close with closable writer that returns error
|
||||
errorBuf := &closableBuffer{Buffer: &bytes.Buffer{}, closed: false, closeError: io.EOF}
|
||||
errorSyncWriter := SyncWriter(errorBuf)
|
||||
|
||||
if closeable, ok := errorSyncWriter.(io.Closer); !ok {
|
||||
t.Error("SyncWriter should implement Close method")
|
||||
} else {
|
||||
err := closeable.Close()
|
||||
if err != io.EOF {
|
||||
t.Errorf("Close should have returned EOF error, got: %v", err)
|
||||
}
|
||||
}
|
||||
if !errorBuf.closed {
|
||||
t.Error("Close should have been called on closable writer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiLevelWriter_Write(t *testing.T) {
|
||||
// Test successful writes
|
||||
buf1 := &bytes.Buffer{}
|
||||
buf2 := &bytes.Buffer{}
|
||||
|
||||
multiWriter := MultiLevelWriter(buf1, buf2)
|
||||
|
||||
data := []byte("test data")
|
||||
n, err := multiWriter.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("Write failed: %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Errorf("Write returned wrong length: got %d, want %d", n, len(data))
|
||||
}
|
||||
|
||||
if got1 := buf1.String(); got1 != string(data) {
|
||||
t.Errorf("First writer got wrong data: got %q, want %q", got1, string(data))
|
||||
}
|
||||
if got2 := buf2.String(); got2 != string(data) {
|
||||
t.Errorf("Second writer got wrong data: got %q, want %q", got2, string(data))
|
||||
}
|
||||
|
||||
// Test with error writer
|
||||
errorWriter1 := &errorWriter{writeError: io.EOF}
|
||||
buf3 := &bytes.Buffer{}
|
||||
|
||||
errorMultiWriter := MultiLevelWriter(errorWriter1, buf3)
|
||||
|
||||
_, err = errorMultiWriter.Write(data)
|
||||
if err != io.EOF {
|
||||
t.Errorf("Write should have returned EOF error, got: %v", err)
|
||||
}
|
||||
|
||||
// Test with short write
|
||||
shortWriter := &errorWriter{shortWrite: true}
|
||||
buf4 := &bytes.Buffer{}
|
||||
|
||||
shortMultiWriter := MultiLevelWriter(shortWriter, buf4)
|
||||
|
||||
_, err = shortMultiWriter.Write(data)
|
||||
if err != io.ErrShortWrite {
|
||||
t.Errorf("Write should have returned ErrShortWrite, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiLevelWriter_WriteLevel(t *testing.T) {
|
||||
// Test successful writes
|
||||
buf1 := &bytes.Buffer{}
|
||||
buf2 := &bytes.Buffer{}
|
||||
|
||||
multiWriter := MultiLevelWriter(buf1, buf2)
|
||||
|
||||
data := []byte("test level data")
|
||||
n, err := multiWriter.WriteLevel(InfoLevel, data)
|
||||
if err != nil {
|
||||
t.Errorf("WriteLevel failed: %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Errorf("WriteLevel returned wrong length: got %d, want %d", n, len(data))
|
||||
}
|
||||
|
||||
if got1 := buf1.String(); got1 != string(data) {
|
||||
t.Errorf("First writer got wrong data: got %q, want %q", got1, string(data))
|
||||
}
|
||||
if got2 := buf2.String(); got2 != string(data) {
|
||||
t.Errorf("Second writer got wrong data: got %q, want %q", got2, string(data))
|
||||
}
|
||||
|
||||
// Test with error writer
|
||||
errorWriter1 := &errorWriter{writeError: io.EOF}
|
||||
buf3 := &bytes.Buffer{}
|
||||
|
||||
errorMultiWriter := MultiLevelWriter(errorWriter1, buf3)
|
||||
|
||||
_, err = errorMultiWriter.WriteLevel(InfoLevel, data)
|
||||
if err != io.EOF {
|
||||
t.Errorf("WriteLevel should have returned EOF error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiLevelWriter_Close(t *testing.T) {
|
||||
buf1 := &closableBuffer{Buffer: &bytes.Buffer{}, closed: false}
|
||||
buf2 := &bytes.Buffer{} // non-closable
|
||||
|
||||
multiWriter := MultiLevelWriter(buf1, buf2)
|
||||
|
||||
// Cast to concrete type to access Close
|
||||
mw := multiWriter.(multiLevelWriter)
|
||||
|
||||
err := mw.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
if !buf1.closed {
|
||||
t.Error("First closable writer should have been closed")
|
||||
}
|
||||
|
||||
// Test multiLevelWriter Close with error
|
||||
errorBuf1 := &closableBuffer{Buffer: &bytes.Buffer{}, closed: false, closeError: io.EOF}
|
||||
errorBuf2 := &bytes.Buffer{} // non-closable
|
||||
|
||||
errorMultiWriter := MultiLevelWriter(errorBuf1, errorBuf2)
|
||||
emw := errorMultiWriter.(multiLevelWriter)
|
||||
|
||||
err = emw.Close()
|
||||
if err != io.EOF {
|
||||
t.Errorf("Close should have returned EOF error, got: %v", err)
|
||||
}
|
||||
if !errorBuf1.closed {
|
||||
t.Error("First closable writer should have been closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTestWriter(t *testing.T) {
|
||||
writer := NewTestWriter(t)
|
||||
|
||||
if writer.T != t {
|
||||
t.Error("NewTestWriter should set the testing interface")
|
||||
}
|
||||
if writer.Frame != 0 {
|
||||
t.Errorf("NewTestWriter should set Frame to 0, got %d", writer.Frame)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredLevelWriter_Write(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
filteredWriter := FilteredLevelWriter{
|
||||
Writer: LevelWriterAdapter{buf},
|
||||
Level: InfoLevel,
|
||||
}
|
||||
|
||||
data := []byte("test data")
|
||||
n, err := filteredWriter.Write(data)
|
||||
if err != nil {
|
||||
t.Errorf("Write failed: %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Errorf("Write returned wrong length: got %d, want %d", n, len(data))
|
||||
}
|
||||
|
||||
if got := buf.String(); got != string(data) {
|
||||
t.Errorf("Write should always write: got %q, want %q", got, string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredLevelWriter_Close(t *testing.T) {
|
||||
buf := &closableBuffer{Buffer: &bytes.Buffer{}, closed: false}
|
||||
filteredWriter := FilteredLevelWriter{
|
||||
Writer: LevelWriterAdapter{buf},
|
||||
Level: InfoLevel,
|
||||
}
|
||||
|
||||
err := filteredWriter.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
if !buf.closed {
|
||||
t.Error("Underlying closable writer should have been closed")
|
||||
}
|
||||
|
||||
// Test FilteredLevelWriter Close with error
|
||||
errorBuf := &closableBuffer{Buffer: &bytes.Buffer{}, closed: false, closeError: io.EOF}
|
||||
errorFilteredWriter := FilteredLevelWriter{
|
||||
Writer: LevelWriterAdapter{errorBuf},
|
||||
Level: InfoLevel,
|
||||
}
|
||||
|
||||
err = errorFilteredWriter.Close()
|
||||
if err != io.EOF {
|
||||
t.Errorf("Close should have returned EOF error, got: %v", err)
|
||||
}
|
||||
if !errorBuf.closed {
|
||||
t.Error("Underlying closable writer should have been closed")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user