1
0
mirror of https://github.com/rs/zerolog synced 2026-06-08 17:13:30 +00:00

Compare commits

..

34 Commits

Author SHA1 Message Date
Ali Asghar 116c8060e0 event: restore Err() logging when ErrorStackMarshaler returns nil (#763)
Commit f6fbd33 added `case nil: return e` in Event.Err() so that when
ErrorStackMarshaler returns nil (the error has no stack trace), the
function returns early without logging the error itself.

This was a regression. The previous behaviour was to fall through the
switch and always call AnErr(ErrorFieldName, err), ensuring the error
message is logged even when no stack is attached.

Libraries such as elastic/ecs-logging-go-zerolog (and plain fmt.Errorf
errors) rely on the old behaviour: calling Stack().Err(err) should
always log the error field; the stack field is additional and optional.

Change the `nil` case from `return e` to a comment (fall-through) so
that AnErr is called unconditionally. Update the test that was written
to match the incorrect behaviour.

Fixes #762

Signed-off-by: alliasgher <alliasgher123@gmail.com>
2026-04-21 00:17:57 +01:00
Olivier Poitrey 13966551e7 Bump CI Go matrix minimum from 1.21 to 1.23
go.mod requires go >= 1.23 and the codebase uses Go 1.22+ features
(log/slog, range-over-integer), so Go 1.21 CI jobs cannot pass.
2026-03-27 20:13:06 +01:00
dependabot[bot] 4b65a2f6f6 Bump actions/cache from 4 to 5 (#741)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 18:51:13 +00:00
dependabot[bot] b83579670f Bump actions/setup-go from 5 to 6 (#742)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 18:50:38 +00:00
Marc Brooks 134caf82aa Added sanitization of journald keys (#751)
Also, raised text coverage for the journald sub-package to 92.2%
Fixes #668
2026-03-27 18:50:13 +00:00
Marc Brooks e133b6a517 Added variadic StrsV, ObjectsV, and StringersV (#752)
* Added variadic StrsV, ObjectsV, and StringersV

This allows you to just list all the string, object, or stringer that you want added to a Event or Context.

The variadic versions are suffixed with V so as to not be a breaking change.

If you have an existing array of objects that implement the LogObjectMarshaler or fmt.Stringer interfaces, unfortunately go doesn't consider those slices as identical types, so you have to allocate an array and copy the entries, there are generic helper methods in global_118.go zerolog.AsLogObjectMarshalers and zerolog.AsStringers if you're using go 1.18 or later.

Somewhat addresses #551

* Address CoPilot comments.

* Resolve odd Stringers on nil array
2026-03-27 18:49:07 +00:00
dependabot[bot] 82017d8fff Bump github.com/coreos/go-systemd/v22 from 22.6.0 to 22.7.0 (#753)
Bumps [github.com/coreos/go-systemd/v22](https://github.com/coreos/go-systemd) from 22.6.0 to 22.7.0.
- [Release notes](https://github.com/coreos/go-systemd/releases)
- [Commits](https://github.com/coreos/go-systemd/compare/v22.6.0...v22.7.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-systemd/v22
  dependency-version: 22.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 18:48:46 +00:00
Varun Chawla 2f5b8a91be fix: UpdateContext skips Nop and zero-value loggers (#754)
UpdateContext previously used pointer equality (l == disabledLogger) to
detect disabled loggers. This only caught the package-level singleton
returned by Ctx() but missed loggers created via Nop() or zero-value
Logger{}, which could lead to data races when a shared Nop logger was
used from multiple goroutines.

Replace the pointer comparison with a check for nil writer or Disabled
level, consistent with how Logger.should() determines if logging is
active. This prevents unnecessary context mutations on loggers that
will never produce output.

Fixes #643

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:52:29 +01:00
Varun Chawla d64c9a7138 Add slog.Handler implementation for zerolog (#755)
Closes #571

I needed slog interop in a project where zerolog handles all log output, but some dependencies use `log/slog`. Rather than losing zerolog's performance by switching to slog's built-in JSON handler, this adds a `SlogHandler` that implements `slog.Handler` and routes everything through zerolog.

### What this does

`zerolog.NewSlogHandler(logger)` returns a `slog.Handler` backed by the given `zerolog.Logger`. You can use it like:

```go
zl := zerolog.New(os.Stderr).With().Timestamp().Logger()
slog.SetDefault(slog.New(zerolog.NewSlogHandler(zl)))

slog.Info("request handled", "method", "GET", "status", 200)
// Output: {"level":"info","method":"GET","status":200,"time":...,"message":"request handled"}
```

**Level mapping:**
- `slog.LevelDebug-4` and below -> `zerolog.TraceLevel`
- `slog.LevelDebug` -> `zerolog.DebugLevel`
- `slog.LevelInfo` -> `zerolog.InfoLevel`
- `slog.LevelWarn` -> `zerolog.WarnLevel`
- `slog.LevelError` -> `zerolog.ErrorLevel`

**Supported features:**
- All slog attribute types encoded with zerolog's typed methods (no reflection for primitives)
- `WithAttrs` for pre-attaching fields to child handlers
- `WithGroup` for namespacing keys with dot-separated prefixes
- Nested groups work correctly
- `LogValuer` resolution
- Level filtering respects the zerolog Logger's configured level
- Zerolog contextual fields from `With()` are preserved

**Files:**
- `slog.go` - the handler implementation (~200 lines)
- `slog_test.go` - 26 tests covering levels, all attr types, groups, filtering, LogValuer, immutability

All existing tests continue to pass (the `RandomSampler` flake in `sampler_test.go` is pre-existing).

The `go.mod` already requires Go 1.23, so `log/slog` is available without any changes.
2026-03-17 00:43:02 +00:00
Marc Brooks a0d61dc2c7 fix: return dict to Event pool (#749)
Return dict to event pool in Array.Dict()
2026-01-12 22:51:46 +00:00
Marc Brooks f6fbd330be Test coverage improvements (#748)
* Test coverage improvements

Implements #397 and #591, might help with #473
Test coverage for core is 97.6% with only real fringe cases remaining.
Improve `Fields` `isNilValue()` portability.
Added a new global handler `FatalExitFunc` to allow intercepting `Fatal()` messages.
Fixed CBOR float constants (removed the CBOR prefix)

Added tests for:
  - global `FatalExitFunc` to allow intercepting Fatal messages (both for testing and public use).
  - `Logger`
    -  `DisableSampling`
    -  `.With()` copying existing context if present.
    -  `.With().Fields()` all forms of `ErrorStackMarshaler` returns.
    - `.WithLevel()` for `FatalLevel`, `PanicLevel`, and `DisabledLevel`.
    - `.Err()` with `nil` and non-`nil` `error` and test the resulting log level.
    - `.should()` covering `nil` writer.
    - `.Output()` gets context values.
    - `.UpdateContext()` on a disabled logger doesn't panic and is a nop.
    - `.With()` all forms of `ErrorStackMarshaler` returns.
  - with a `nil`writer.
  - `.Hook()` passing no hooks.
- `Array`
  - `.MarshalZerologArray` is a nop that won't panic.
- `Context`
  - ` .Err()` and `.AnErr()` for `nil` errors and all forms of `ErrorStackMarshaler` returns.
  - `Event`
    - `.Caller()` to ensure we don't panic or add invalid information if `runtime.Caller()` fails
    -` .Err()` and `.AnErr()` for `nil` errors and all forms of `ErrorStackMarshaler` returns.
  - `Fields`
    - ` .appendFields()`  all forms of `ErrorStackMarshaler` returns.
  - `HookLevel`
    - `.Run()` methods.
  - `LevelSampler`
    - `.Sample() methods.
  - `Syslog`
    - `.Write()`, `.WriteLevel()`, and `.Close()` methods.
    - `.WriteLevel()` with an `InvalidLevel`.
  - `Writer`
    - `.Write()` short write and error cases.
    - `MultiLevelWriter` `.WriteLevel()` and for `.Write()` error and  `.Close()` cases.
  - test of unmarshalling a level byte returns correct error.
  - CBOR decodeStream
    - `.decodeFloat()`, `.binaryFmt()`, `.DecodeIfBinaryToString()`, `.DecodeObjectToStr()`, `.DecodeIfBinaryToBytes()`, `.decodeTagData()`, and `.decodeSimpleFloat()`
    - handling of invalid UTF-8 sequences
    - handling of UTC times.
    - handling of timestamps
    - handling of various map lengths

Restructure `Event` `.caller()` so we test for ok and eliminate untestable coverage hole.

Restructure `Context` `.Err()` when the `ErrorStackMarshaler` returns a `nil` so there's code to cover.

Inverted logic for`Event` `.Caller()`'s call to `runtime.Caller()` for simpler testing.

Inverted logic for `Array` `.putArray()` and `Event` `.putEvent()` so there isn't uncoverable code.

Restructure `Field` `.appendFieldList()` to early return when `ErrorStackMarshaler` returns a `nil`

Added comments for things we can't get coverage on.

Did a go fmt ./...

Coverage of core is now 100% on `Array`, `Context`, `Ctx`, `Event`, `Field`, `Hook`, and `Syslog`.

Coverage of `Globals`, `Log`, `Sampler`, and `Writer` is almost all except some real edge-cases.

JSON encoder coverage is 100%

CBOR encoder coverage is 96.3% with base, cbor, string, time and types at 100% and decode_stream (which is lacks coverage on some panic states, and two incorrect coverage-tool lapses)

* Fix CBOR tests for StackMarshaler

Forgot to use the `decodeIfBinaryToString()`
2026-01-12 15:03:52 +00:00
Marc Brooks 2094837a2c Ensure that Event and Array objects returned to pool are cleared (#747)
* Added documentation for DurationFieldFormat

Added documentation for the new option added in PR#733
Added code markers some references to logger func/types.
Also, did a format-document pass to get rid of trailing blanks,
missing/extra blank lines, and other markdown warnings.

* Added documentation for Concurrency Safety

Added documentation to the Concurrency Safety section about not referencing 
Event objects after they are written.
Closes #731

* Ensure that Event and Array objects returned to pool are cleared

Help ensure no corruptible state remains in objects being returned to the pool.
Always clear the contextual properties to ensure the recycled (or erroneously referenced)
Event or Array are not accidentally used/shared.
Sets the truncates the buffer (setting length, not capacity, to zero).
Make it really obvious when you hold references as mentioned in #733
Clarified the documentation about holding references to CreateDict() events.
2026-01-11 15:57:13 +00:00
Marc Brooks 0cf9361666 Fix missing context.Context in Object/EmbedObject (#737)
Copies over event settings for ctx, hooks, and stack

- Fix missing context for Event.appendObjects passes event's stack, ctx, and hooks (ch).
- Add Array.Errs support.
- Context.Errs, Event.Errs,  use new Array.Errs()
- Array.Err handles nil correctly.
- Fix Event.Err code ignoring modified event in ErrorStackMarshaler handling.
- Fix Fields.appendFieldList when ErrorMarshalFunc returns a nil.
- Fields.appendFieldList does no longer adds the ErrorStackFieldName field when ErrorStackMarshaler returns nil.
- Don't call the ErrorMarshalFunc on nil errors.
- Removed unreachable code NOP in Context.Array
- Rewrite ErrorMarshalFunc testing.
- Made diode tests more stable.
- Log copy error in test.
- Address review comments
- Added CreateArray() and CreateDict() for both Context and Event and deprecate the Arr() and Dict().
Array now carries then stack/context/hooks so they are available to LogObjectMarshal of Array elements.
Added unit tests for error marshal that returns an interface{}
2026-01-05 18:19:15 +00:00
dependabot[bot] 0d69d7537a Bump github.com/coreos/go-systemd/v22 from 22.5.0 to 22.6.0 (#743)
Bumps [github.com/coreos/go-systemd/v22](https://github.com/coreos/go-systemd) from 22.5.0 to 22.6.0.
- [Release notes](https://github.com/coreos/go-systemd/releases)
- [Commits](https://github.com/coreos/go-systemd/compare/v22.5.0...v22.6.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-systemd/v22
  dependency-version: 22.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 18:17:14 +00:00
dependabot[bot] 3d20da0255 Bump actions/checkout from 4 to 6 (#744)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 18:17:01 +00:00
Sebastian Rühl 4685edb80b feat: add new global DurationFieldFormat (#733)
This deprecates DurationFieldInteger in favor of DurationFieldFormat with the value of DurationFormatInt
2026-01-05 18:16:40 +00:00
Maksim Bolgarin 05348ba6f6 docs: add logze to related projects (#724) 2025-12-23 15:31:39 +00:00
dependabot[bot] a728d746c0 Bump github.com/mattn/go-colorable from 0.1.13 to 0.1.14 (#703)
Bumps [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) from 0.1.13 to 0.1.14.
- [Commits](https://github.com/mattn/go-colorable/compare/v0.1.13...v0.1.14)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-colorable
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 15:08:25 +00:00
Marc Brooks a64148507a Adds Objects field type to logger fields, events, and context (#736)
* Improve code coverage before adding Objects

* Refactor ErrorMarshal tests.

Added testing of the error marshal in Fields Reduce the duplication of the test data and make it clearer what the "wants" represent.

* Add Objects support to Fields, Event, Context

* Add support for Object Fields

Passed as []LogObjectMarshaler.

* Fixed syntax of ExampleContext_Objects

Seems 1.24 accepts variadic for arrays and understands covariance but 1.21 doesn't.

* Fix problem with []uint8 serialized as a Base64 string

* Better syntax in the example use of Objects
2025-12-18 18:50:34 +00:00
Marc Brooks 42f33fff03 Add Type() to the Array (#738)
Fixes #729
2025-12-18 01:06:38 +01:00
Marc Brooks 8148645974 Chore improve test coverage (#735)
* Add array handling of IPAddr and IPPrefix

Updated documentation, benchmark, and tests including catching up some missing.
Cleaned up couple lint messages

* Added cbor encoding for []net.IP and []net.IPNet

* Fix binary_test

Typos galore, thanks CI

* Remove feral x character

* Added IPAddrs and IPPrefixes tests for json encoder

* Increase code coverage and test cleanup

Added Type to the BenchmarkLogFieldType and BenchmarkContexdtFieldType test cases, also to log_test.go
Moved the test-fixture data for them and also shared them with new event test for nil events.
Added a couple strings for encodeByteTest for FF, CR, LF
Added complete cbor testing for AppendInt*, AppendInts*, AppendUint*, and AppendUints* methods.
Added test for float IsNaN and Inf(0) and Inf(-1).
Added all min/max/smallest value tests for ints and floats
Extended the Array test to include IPPrefix, MACAddr, Interface, and object marshaled.
Better test of MarshalZerologObject.
Added test for encoder AppendStrings and AppendStringers.

* Remove merge detritus.

* Remove code duplication when appending objects

* Add missing Stringers support to Context

* Add test cases for Err and Object in Array

* Add missing benchmark for Events and Contexts

Events: Any, Bytes, Hex, Float32, Floats32, Stringer, Stringers, Timestamp
Context: Any, Bytes, Hex, Float32, Floats32, Stringers
(also renamed Float,Floats to Float64,Floats64)

* Add test of MarshalZeroLogObject of error

* Build out Event test cases

Test of nil Event
- write
- Array, Dict
- Fields
- Int8, Ints8, Int16, Ints16, Int32, Ints32, Int64, Ints64
- Uint8, Uints8, Uint16, Uints16, Uint32, Uints32, Uint64, Uints64
- RawCBOR, RawJSON, EmbedObject
- Stringers
- Caller, CallerSkip, Stack
- Send, Msg, Msgf, MsgFunc
- (renamed tests of Float, Floats to Float64, Floats64)

Added test of MsgFunc

* Add more tests

- Dur (With)
- Any (With, Fields, Fields map, Disabled)
- Interface
- Stack (Fields)

Fix issue with testing a nil loggableError

* Split out the With tests again type arrays

Simplifies the "want" maintenance

* Add missing test fixture data

Bytes, Floats32, Ints8, Ints16, Ints32, Ints64, Uints8, Uints16, Uints32, Uints64, RawJSON, RawCBOR
Stringers is now an array (not a single Stringer)
(also renamed Float, Floats to Float64, Floats64)

* Add examples for IPAddrs, IPPrefixes, Times

Missing examples that increase coverage

* Build out the complete ErrorMarshalFunc tests

* Add AppendStrings to CBOR tests

* Add CBOR AppendTimes, AppendDuration, AppendDurations tests

* Add CBOR tests

- BeginMarker, EndMarker, AppendArrayDim, AppendLineBreak
- AppendObjectData
- AppendIterface
- AppendType
- AppendHex
- Empty arrays for
  - Bools
  - Floats32, Floats64
- Large arrays for
  - Bools,
  - Uints8, Uints16, Uints32, Uints64
  - Floats32, Floats64
  - IPAddrs, IPPrefixes
- Small arrays for
  - Ints, Ints8, Ints16, Ints32, Ints64
  - Uints, Uints32, Uints64
  - Floats32, Floats64

* Add CBOR test for AppendKey

* Added more string encoding tests

Test escape character handling.

* Fix test import cycle

* Rename hex to hexCharacters to avoid namespace.

* Remove leftover comment

* Add test cases to share between CBOR and JSON tests

* Switched CBOR to shared test cases

* Add JSON test for AppendKey

* Switch to shared test cases

Added test for AppendStringer

* Added JSON test

- AppendNil
- AppendBeginMarker, AppendEndMarker
- AppendArrayStart, AppendArrayEnd
- AppendArrayDelim
- AppendLineBreak
- AppendObjectData
- AppendInterface
- AppendBool, AppendBools

* Add JSON time tests

- AppendTime, AppendTimes
- AppendTime (past/present) (integer/float)
- AppendDuration, AppendDurations

* Added JSON tests for plurals

Split out the Int and Float tests into distinct files.
Add
 - AppendInt
 - AppendInts8, AppendInts16, AppendInts32, AppendInts64, AppendInts
 - AppendUints8, AppendUints16, AppendUints32, AppendUints64, AppendUints
- AppendFloats32, AppendFloats64
2025-12-15 20:55:37 +01:00
Marc Brooks 5391dd7c34 Add array handling of IPAddr and IPPrefix (#734)
* Add array handling of IPAddr and IPPrefix

Updated documentation, benchmark, and tests including catching up some missing.
Cleaned up couple lint messages

* Added cbor encoding for []net.IP and []net.IPNet

* Fix binary_test

Typos galore, thanks CI

* Remove feral x character

* Added IPAddrs and IPPrefixes tests for json encoder
2025-12-06 13:12:45 +01:00
wangcundashang 9dacc014f3 chore: fix some comments (#718)
Signed-off-by: wangcundashang <wangcundashang@qq.com>
2025-04-18 13:14:43 +02:00
Olivier Poitrey a21d6107dc Remvoe ExampleBinaryNew referring to nothing 2025-03-24 16:31:31 +01:00
Olivier Poitrey db9d1bebd9 Update go versions covered by CI 2025-03-21 01:06:46 +01:00
Olivier Poitrey 5f4b880a01 Delete _config.yml 2025-03-21 01:03:04 +01:00
Olivier Poitrey ffb27080ca Remove CNAME file 2025-03-20 17:01:12 -07:00
Olivier Poitrey cc4dde7383 Create CONTRIBUTING.md 2025-03-21 00:58:35 +01:00
Andrey36652 04ea0f4371 Implement Close() for zerolog.FilteredLevelWriter (#715) 2025-03-12 13:16:56 +01:00
crazy-pe 039860087c fix: reset condition in burst sampler (#711) (#712) 2025-02-26 15:21:09 +00:00
sted 1869fa55be FormatPartValueByName for flexible custom formatting for ConsoleWriter (#541) 2025-01-03 23:53:36 +00:00
Abdullah Alaadine 31e7995c5b remove unnecessary nil checks (#701)
Co-authored-by: Abdullah Alaadine <abdullah.aladdine@montymobile.com>
2024-12-27 02:41:46 +00:00
Oleg Schwann 582f820cf0 Get BasicSampler(0), RandomSampler(0), and BurstSampler(0) to behave the same and not write anything (#696) 2024-11-14 16:50:12 +01:00
dependabot[bot] 6abadab488 Bump github.com/rs/xid from 1.5.0 to 1.6.0 (#684) 2024-08-26 13:12:21 +02:00
63 changed files with 8590 additions and 1465 deletions
+5 -5
View File
@@ -4,17 +4,17 @@ jobs:
test:
strategy:
matrix:
go-version: [1.18.x, 1.21.x]
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@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/cache@v4
uses: actions/checkout@v6
- uses: actions/cache@v5
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
@@ -23,7 +23,7 @@ jobs:
- name: Test
run: go test -race -bench . -benchmem ./...
- name: Test CBOR
run: go test -tags binary_log ./...
run: go test -tags binary_log -race ./...
coverage:
runs-on: ubuntu-latest
steps:
+2
View File
@@ -23,3 +23,5 @@ _testmain.go
*.exe
*.test
*.prof
coverage.out
-1
View File
@@ -1 +0,0 @@
zerolog.io
+43
View File
@@ -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!
+205 -129
View File
@@ -18,17 +18,18 @@ Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog)
## Features
* [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)
- [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
@@ -59,8 +60,9 @@ func main() {
// 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 *trace*
> Note: The default log level for `log.Print` is _trace_
### Contextual Logging
@@ -81,7 +83,7 @@ func main() {
Str("Scale", "833 cents").
Float64("Interval", 833.09).
Msg("Fibonacci is everywhere")
log.Debug().
Str("Name", "Tom").
Send()
@@ -118,15 +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)
* trace (`zerolog.TraceLevel`, -1)
- 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
@@ -212,17 +214,17 @@ You can log errors using the `Err` method
package main
import (
"errors"
"errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
err := errors.New("seems we have an error here")
log.Error().Err(err).Msg("")
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}
@@ -232,45 +234,45 @@ func main() {
#### Error Logging with Stacktrace
Using `github.com/pkg/errors`, you can add a formatted stacktrace to your errors.
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/pkg/errors"
"github.com/rs/zerolog/pkgerrors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
err := outer()
log.Error().Stack().Err(err).Msg("")
err := outer()
log.Error().Stack().Err(err).Msg("")
}
func inner() error {
return errors.New("seems we have an error here")
return errors.New("seems we have an error here")
}
func middle() error {
err := inner()
if err != nil {
return err
}
return nil
err := inner()
if err != nil {
return err
}
return nil
}
func outer() error {
err := middle()
if err != nil {
return err
}
return nil
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}
@@ -308,7 +310,6 @@ func main() {
> NOTE: Using `Msgf` generates one allocation even when the logger is disabled.
### Create logger instance to manage different outputs
```go
@@ -366,6 +367,37 @@ 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
```go
@@ -426,8 +458,8 @@ If your writer might be slow or not thread-safe and you need your log producers
```go
wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
fmt.Printf("Logger Dropped %d messages", missed)
})
fmt.Printf("Logger Dropped %d messages", missed)
})
log := zerolog.New(wr)
log.Print("test")
```
@@ -506,7 +538,7 @@ stdlog.Print("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`
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:
@@ -531,7 +563,7 @@ func someFunc(ctx context.Context) {
```
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
`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:
@@ -609,17 +641,17 @@ 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.
`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!")
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)
@@ -631,43 +663,45 @@ func main() {
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. 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.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`).
* `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.
- `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 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.
- `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.)
@@ -679,20 +713,48 @@ In addition to the default JSON encoding, `zerolog` can produce binary logs usin
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`
* [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`
- [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://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
@@ -704,50 +766,50 @@ 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
@@ -767,7 +829,7 @@ In this case, many consumers will take the last value, but this is not guarantee
### Concurrency safety
Be careful when calling UpdateContext. It is not concurrency safe. Use the With method to create a child logger:
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) {
@@ -780,3 +842,17 @@ func handler(w http.ResponseWriter, r *http.Request) {
})
}
```
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
View File
@@ -1 +0,0 @@
remote_theme: rs/gh-readme
+57 -21
View File
@@ -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,22 +38,28 @@ 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 {
@@ -59,11 +75,7 @@ func (a *Array) write(dst []byte) []byte {
// Object marshals an object that implement the LogObjectMarshaler
// 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
}
@@ -94,16 +106,12 @@ func (a *Array) RawJSON(val []byte) *Array {
// Err serializes and appends the err to the array.
func (a *Array) Err(err error) *Array {
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:
if m == nil || isNilValue(m) {
a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf))
} else {
if !isNilValue(m) {
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
}
case string:
@@ -115,6 +123,27 @@ func (a *Array) Err(err error) *Array {
return a
}
// 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)
@@ -201,7 +230,7 @@ func (a *Array) Time(t time.Time) *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, FloatingPointPrecision)
a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
return a
}
@@ -214,19 +243,19 @@ 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
@@ -236,5 +265,12 @@ func (a *Array) MACAddr(ha net.HardwareAddr) *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
}
+29 -2
View File
@@ -1,6 +1,7 @@
package zerolog
import (
"fmt"
"net"
"testing"
"time"
@@ -25,15 +26,41 @@ func TestArray(t *testing.T) {
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}).
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),
)
want := `[true,1,2,3,4,5,6,7,8,9,10,11.98122,12.987654321,"a","b","1f",{"some":"json"},"0001-01-01T00:00:00Z","192.168.0.10",0,{"bar":"baz","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
}
+155 -157
View File
@@ -1,10 +1,8 @@
package zerolog
import (
"context"
"errors"
"io"
"net"
"testing"
"time"
)
@@ -86,22 +84,10 @@ 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}
obj1 := fixtureObj{"a", "b", 2}
obj2 := fixtureObj{"c", "d", 3}
obj3 := fixtureObj{"e", "f", 4}
logger := New(io.Discard)
b.ResetTimer()
b.ReportAllocs()
@@ -115,115 +101,124 @@ 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{
{"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},
}
errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e")}
ctx := context.Background()
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(ctx)
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(io.Discard)
b.ResetTimer()
for name := range types {
@@ -242,122 +237,125 @@ func BenchmarkContextFieldType(b *testing.B) {
oldFormat := 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"}
stringer := net.IP{127, 0, 0, 1}
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{
{"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},
}
errs := []error{errors.New("a"), errors.New("b"), errors.New("c"), errors.New("d"), errors.New("e")}
ctx := context.Background()
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", stringer)
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(ctx)
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(io.Discard)
b.ResetTimer()
for name := range types {
+99 -35
View File
@@ -1,3 +1,4 @@
//go:build binary_log
// +build binary_log
package zerolog
@@ -6,20 +7,12 @@ import (
"bytes"
"errors"
"fmt"
"net"
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).
@@ -213,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()))
@@ -239,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)
@@ -249,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()))
@@ -296,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: "$"}
@@ -401,14 +416,15 @@ func ExampleEvent_Fields_slice() {
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"}
@@ -416,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"}
@@ -581,3 +598,50 @@ func ExampleContext_Fields_slice() {
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"}
}
+23 -8
View File
@@ -47,6 +47,10 @@ const (
// 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 {
@@ -85,6 +89,9 @@ type ConsoleWriter struct {
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
@@ -94,9 +101,9 @@ type ConsoleWriter struct {
// NewConsoleWriter creates and initializes a new ConsoleWriter.
func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter {
w := ConsoleWriter{
Out: os.Stdout,
TimeFormat: consoleDefaultTimeFormat,
PartsOrder: consoleDefaultPartsOrder(),
Out: os.Stdout,
TimeFormat: consoleDefaultTimeFormat,
PartsOrder: consoleDefaultPartsOrder(),
}
for _, opt := range options {
@@ -282,8 +289,9 @@ func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer
// 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 w.PartsExclude != nil && len(w.PartsExclude) > 0 {
if len(w.PartsExclude) > 0 {
for _, exclude := range w.PartsExclude {
if exclude == p {
return
@@ -317,14 +325,21 @@ func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{},
f = w.FormatCaller
}
default:
if w.FormatFieldValue == nil {
f = consoleDefaultFormatFieldValue
} else {
if w.FormatPartValueByName != nil {
fvn = w.FormatPartValueByName
} else if w.FormatFieldValue != nil {
f = w.FormatFieldValue
} else {
f = consoleDefaultFormatFieldValue
}
}
var s = f(evt[p])
var s string
if f == nil {
s = fvn(evt[p], p)
} else {
s = f(evt[p])
}
if len(s) > 0 {
if buf.Len() > 0 {
+54 -7
View File
@@ -2,9 +2,11 @@ package zerolog_test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -30,6 +32,34 @@ func ExampleConsoleWriter_customFormatters() {
// 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
@@ -287,16 +317,33 @@ func TestConsoleWriter(t *testing.T) {
ts := time.Unix(0, 0)
d := ts.UTC().Format(time.RFC3339)
evt := `{"time": "` + d + `", "level": "debug", "message": "Foobar", "foo": "bar", "caller": "` + cwd + `/foo/bar.go"}`
// t.Log(evt)
_, err = w.Write([]byte(evt))
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)
}
@@ -423,14 +470,14 @@ func TestConsoleWriterConfiguration(t *testing.T) {
})
t.Run("Sets TimeFormat and TimeLocation", func(t *testing.T) {
locs := []*time.Location{ time.Local, time.UTC }
locs := []*time.Location{time.Local, time.UTC}
for _, location := range locs {
buf := &bytes.Buffer{}
w := zerolog.ConsoleWriter{
Out: buf,
NoColor: true,
TimeFormat: time.RFC3339,
Out: buf,
NoColor: true,
TimeFormat: time.RFC3339,
TimeLocation: location,
}
+98 -43
View File
@@ -3,7 +3,6 @@ package zerolog
import (
"context"
"fmt"
"io"
"math"
"net"
"time"
@@ -23,7 +22,7 @@ func (c Context) Logger() Logger {
// 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.context = appendFields(c.l.context, fields, c.l.stack, c.l.ctx, c.l.hooks)
return c
}
@@ -35,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)
@@ -44,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{io.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{io.Discard}, 0)
e := c.l.scratchEvent()
e.EmbedObject(obj)
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
putEvent(e)
@@ -80,11 +114,20 @@ 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 {
@@ -96,6 +139,24 @@ func (c Context) Stringer(key string, val fmt.Stringer) Context {
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)
@@ -118,6 +179,7 @@ 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 {
switch m := ErrorMarshalFunc(err).(type) {
case nil:
@@ -125,11 +187,10 @@ func (c Context) AnErr(key string, err error) Context {
case LogObjectMarshaler:
return c.Object(key, m)
case error:
if m == nil || isNilValue(m) {
if isNilValue(m) {
return c
} else {
return c.Str(key, m.Error())
}
return c.Str(key, m.Error())
case string:
return c.Str(key, m)
default:
@@ -140,24 +201,7 @@ 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 {
switch m := ErrorMarshalFunc(err).(type) {
case LogObjectMarshaler:
arr = arr.Object(m)
case error:
if m == nil || isNilValue(m) {
arr = arr.Interface(nil)
} else {
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)
}
@@ -166,12 +210,11 @@ 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:
if m != nil && !isNilValue(m) {
c = c.Str(ErrorStackFieldName, m.Error())
}
c = c.Str(ErrorStackFieldName, m.Error())
case string:
c = c.Str(ErrorStackFieldName, m)
default:
@@ -377,15 +420,15 @@ func (c Context) Times(key string, t []time.Time) Context {
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, FloatingPointPrecision)
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, FloatingPointPrecision)
c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
return c
}
@@ -461,19 +504,31 @@ func (c Context) Stack() Context {
return c
}
// IPAddr adds IPv4 or IPv6 Address to the context
// 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
View File
@@ -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)
}
}
+5 -6
View File
@@ -25,12 +25,11 @@ type ctxKey struct{}
// 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")
// })
//
// 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.
+4 -3
View File
@@ -5,6 +5,7 @@ import (
"context"
"io"
"reflect"
"strings"
"testing"
"github.com/rs/zerolog/internal/cbor"
@@ -78,7 +79,7 @@ type logObjectMarshalerImpl struct {
}
func (t logObjectMarshalerImpl) MarshalZerologObject(e *Event) {
e.Str("name", "custom_value").Int("age", t.age)
e.Str("name", strings.ToLower(t.name)).Int("age", -t.age)
}
func Test_InterfaceLogObjectMarshaler(t *testing.T) {
@@ -89,13 +90,13 @@ func Test_InterfaceLogObjectMarshaler(t *testing.T) {
log2 := Ctx(ctx)
withLog := log2.With().Interface("obj", &logObjectMarshalerImpl{
name: "foo",
name: "FOO",
age: 29,
}).Logger()
withLog.Info().Msg("test")
if got, want := cbor.DecodeIfBinaryToString(buf.Bytes()), `{"level":"info","obj":{"name":"custom_value","age":29},"message":"test"}`+"\n"; got != want {
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)
}
}
+75 -4
View File
@@ -7,6 +7,7 @@ import (
"log"
"os"
"os/exec"
"sync"
"testing"
"time"
@@ -60,15 +61,25 @@ func TestFatal(t *testing.T) {
if err != nil {
t.Fatal(err)
}
slurp, err := io.ReadAll(stderr)
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 {
@@ -76,6 +87,66 @@ func TestFatal(t *testing.T) {
}
}
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)
+2 -2
View File
@@ -13,8 +13,8 @@ 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, precision int) []byte
AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, useInt bool, precision int) []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, precision int) []byte
AppendFloat64(dst []byte, val float64, precision int) []byte
+224
View File
@@ -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)
}
})
})
})
}
}
+144 -55
View File
@@ -32,6 +32,15 @@ type Event struct {
}
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
@@ -39,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
@@ -57,14 +65,15 @@ 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.stack = false
e.skipFrame = 0
return e
}
@@ -164,31 +173,58 @@ func (e *Event) Fields(fields interface{}) *Event {
if e == nil {
return e
}
e.buf = appendFields(e.buf, fields, e.stack)
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(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 {
@@ -199,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)
@@ -228,6 +264,33 @@ func (e *Event) Object(key string, obj LogObjectMarshaler) *Event {
return e
}
// 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() {
@@ -258,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
@@ -266,8 +331,16 @@ func (e *Event) Strs(key string, vals []string) *Event {
return e
}
// Stringer adds the field key with val.String() (or null if val is nil)
// to the *Event context.
// 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
@@ -276,9 +349,11 @@ func (e *Event) Stringer(key string, val fmt.Stringer) *Event {
return e
}
// Stringers adds the field key with vals where each individual val
// is used as val.String() (or null if val is empty) to the *Event
// context.
// 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
@@ -287,6 +362,16 @@ func (e *Event) Stringers(key string, vals []fmt.Stringer) *Event {
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
@@ -344,11 +429,10 @@ func (e *Event) AnErr(key string, err error) *Event {
case LogObjectMarshaler:
return e.Object(key, m)
case error:
if m == nil || isNilValue(m) {
if isNilValue(m) {
return e
} else {
return e.Str(key, m.Error())
}
return e.Str(key, m.Error())
case string:
return e.Str(key, m)
default:
@@ -362,20 +446,7 @@ func (e *Event) Errs(key string, errs []error) *Event {
if e == nil {
return e
}
arr := Arr()
for _, err := range errs {
switch m := ErrorMarshalFunc(err).(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)
}
@@ -391,21 +462,23 @@ func (e *Event) Err(err error) *Event {
if e == nil {
return e
}
if e.stack && ErrorStackMarshaler != nil {
switch m := ErrorStackMarshaler(err).(type) {
case nil:
// ErrorStackMarshaler returned nil — the error has no stack trace to
// attach. Fall through and still log the error via AnErr below.
case LogObjectMarshaler:
e.Object(ErrorStackFieldName, m)
e = e.Object(ErrorStackFieldName, m)
case error:
if m != nil && !isNilValue(m) {
e.Str(ErrorStackFieldName, m.Error())
}
e = e.Str(ErrorStackFieldName, m.Error())
case string:
e.Str(ErrorStackFieldName, m)
e = e.Str(ErrorStackFieldName, m)
default:
e.Interface(ErrorStackFieldName, m)
e = e.Interface(ErrorStackFieldName, m)
}
}
return e.AnErr(ErrorFieldName, err)
}
@@ -431,8 +504,8 @@ func (e *Event) Ctx(ctx context.Context) *Event {
}
// 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,
// 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 {
@@ -713,7 +786,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, FloatingPointPrecision)
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
return e
}
@@ -724,7 +797,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, FloatingPointPrecision)
e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
return e
}
@@ -739,7 +812,7 @@ 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, FloatingPointPrecision)
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
return e
}
@@ -794,15 +867,13 @@ func (e *Event) caller(skip int) *Event {
if e == nil {
return e
}
pc, file, line, ok := runtime.Caller(skip + e.skipFrame)
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), CallerMarshalFunc(pc, file, 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
@@ -811,7 +882,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
@@ -820,7 +900,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
+690 -22
View File
@@ -1,10 +1,14 @@
//go:build !binary_log
// +build !binary_log
package zerolog
import (
"bytes"
"context"
"errors"
"io"
"os"
"strings"
"testing"
)
@@ -12,7 +16,7 @@ import (
type nilError struct{}
func (nilError) Error() string {
return ""
return "nope"
}
func TestEvent_AnErr(t *testing.T) {
@@ -28,9 +32,13 @@ func TestEvent_AnErr(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel)
e.AnErr("err", tt.err)
_ = e.write()
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)
}
@@ -38,28 +46,688 @@ func TestEvent_AnErr(t *testing.T) {
}
}
func TestEvent_ObjectWithNil(t *testing.T) {
var buf bytes.Buffer
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel)
_ = e.Object("obj", nil)
_ = e.write()
func TestEvent_writeWithNil(t *testing.T) {
var e *Event = nil
got := e.write()
want := `{"obj":null}`
got := strings.TrimSpace(buf.String())
if got != want {
t.Errorf("Event.Object() = %q, want %q", got, want)
var want *Event = nil
if got != nil {
t.Errorf("Event.write() = %v, want %v", got, want)
}
}
func TestEvent_EmbedObjectWithNil(t *testing.T) {
var buf bytes.Buffer
e := newEvent(LevelWriterAdapter{&buf}, DebugLevel)
_ = e.EmbedObject(nil)
_ = e.write()
type loggableObject struct {
member string
}
want := "{}"
got := strings.TrimSpace(buf.String())
if got != want {
t.Errorf("Event.EmbedObject() = %q, want %q", got, want)
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()
// When ErrorStackMarshaler returns nil (no stack trace available for this
// error), Err() must still log the error value via AnErr. Without a stack
// field, the output is the same as if Stack() had not been called.
// Regression test for https://github.com/rs/zerolog/issues/762:
// commit f6fbd33 introduced a `case nil: return e` branch that silently
// swallowed the error instead of falling through to AnErr.
want := `{"error":"test error","message":"test message"}` + "\n"
if got != want {
t.Errorf("Event.Err() with nil stack marshaler = %q, want %q", got, want)
}
}
+62 -42
View File
@@ -1,24 +1,31 @@
package zerolog
import (
"context"
"encoding/json"
"io"
"net"
"reflect"
"sort"
"time"
"unsafe"
)
func isNilValue(i interface{}) bool {
return (*[2]uintptr)(unsafe.Pointer(&i))[1] == 0
func isNilValue(e error) bool {
switch reflect.TypeOf(e).Kind() {
case reflect.Ptr:
return reflect.ValueOf(e).IsNil()
default:
return false
}
}
func appendFields(dst []byte, fields interface{}, stack bool) []byte {
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)
dst = appendFieldList(dst, fields, stack, ctx, hooks)
case map[string]interface{}:
keys := make([]string, 0, len(fields))
for key := range fields {
@@ -28,13 +35,22 @@ func appendFields(dst []byte, fields interface{}, stack bool) []byte {
kv := make([]interface{}, 2)
for _, key := range keys {
kv[0], kv[1] = key, fields[key]
dst = appendFieldList(dst, kv, stack)
dst = appendFieldList(dst, kv, stack, ctx, hooks)
}
}
return dst
}
func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
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 {
@@ -42,14 +58,6 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
} else {
continue
}
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)
continue
}
switch val := val.(type) {
case string:
dst = enc.AppendString(dst, val)
@@ -57,16 +65,12 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
dst = enc.AppendBytes(dst, val)
case error:
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:
if m == nil || isNilValue(m) {
dst = enc.AppendNil(dst)
} else {
if !isNilValue(m) {
dst = enc.AppendString(dst, m.Error())
}
case string:
@@ -76,16 +80,20 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
}
if stack && ErrorStackMarshaler != nil {
dst = enc.AppendKey(dst, ErrorStackFieldName)
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:
if m != nil && !isNilValue(m) {
dst = enc.AppendString(dst, m.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)
}
}
@@ -93,16 +101,12 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
dst = enc.AppendArrayStart(dst)
for i, err := range val {
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:
if m == nil || isNilValue(m) {
dst = enc.AppendNil(dst)
} else {
if !isNilValue(m) {
dst = enc.AppendString(dst, m.Error())
}
case string:
@@ -112,7 +116,16 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []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)
@@ -145,7 +158,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
case time.Time:
dst = enc.AppendTime(dst, val, TimeFieldFormat)
case time.Duration:
dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
case *string:
if val != nil {
dst = enc.AppendString(dst, *val)
@@ -238,7 +251,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
}
case *time.Duration:
if val != nil {
dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
} else {
dst = enc.AppendNil(dst)
}
@@ -258,8 +271,7 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []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:
@@ -273,19 +285,27 @@ func appendFieldList(dst []byte, kvList []interface{}, stack bool) []byte {
case []time.Time:
dst = enc.AppendTimes(dst, val, TimeFieldFormat)
case []time.Duration:
dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldInteger, FloatingPointPrecision)
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
+159
View File
@@ -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),
}
}
+18
View File
@@ -24,6 +24,16 @@ const (
// 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 (
@@ -107,12 +117,16 @@ var (
// 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
@@ -120,6 +134,10 @@ var (
// 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
+30
View File
@@ -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
}
+9 -6
View File
@@ -1,12 +1,15 @@
module github.com/rs/zerolog
go 1.15
go 1.23
require (
github.com/coreos/go-systemd/v22 v22.5.0
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.19 // indirect
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.5.0
golang.org/x/sys v0.12.0 // indirect
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
)
+10 -20
View File
@@ -1,23 +1,13 @@
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.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=
+97
View File
@@ -50,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")
@@ -171,6 +175,99 @@ 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(io.Discard)
b.ResetTimer()
+22
View File
@@ -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))
}
}
+7 -7
View File
@@ -55,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
@@ -72,7 +72,7 @@ var IntegerTimeFieldFormat = time.RFC3339
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:
+1 -1
View File
@@ -490,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
View File
@@ -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)
}
})
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (Encoder) AppendString(dst []byte, s string) []byte {
// 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 len(vals) == 0 {
if vals == nil || len(vals) == 0 {
return e.AppendArrayEnd(e.AppendArrayStart(dst))
}
dst = e.AppendArrayStart(dst)
+165
View File
@@ -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)
}
})
}
}
+23 -5
View File
@@ -4,6 +4,17 @@ 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
@@ -27,8 +38,7 @@ func (e Encoder) appendFloatTimestamp(dst []byte, t time.Time) []byte {
dst = append(dst, major|minor)
secs := t.Unix()
nanos := t.Nanosecond()
var val float64
val = float64(secs)*1.0 + float64(nanos)*1e-9
val := float64(secs)*1.0 + float64(nanos)*1e-9
return e.AppendFloat64(dst, val, -1)
}
@@ -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, unused int) []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))
}
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, unused int) []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 {
@@ -87,7 +105,7 @@ func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Dur
dst = appendCborTypePrefix(dst, major, uint64(l))
}
for _, d := range vals {
dst = e.AppendDuration(dst, d, unit, useInt, unused)
dst = e.AppendDuration(dst, d, unit, format, useInt, unused)
}
return dst
}
+277 -28
View File
@@ -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{
+40 -2
View File
@@ -447,7 +447,7 @@ func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
return e.AppendString(dst, reflect.TypeOf(i).String())
}
// AppendIPAddr encodes and inserts an IP Address (IPv4 or IPv6).
// 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, majorTypeTags|additionalTypeIntUint16)
dst = append(dst, byte(additionalTypeTagNetworkAddr>>8))
@@ -455,7 +455,26 @@ func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
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, majorTypeTags|additionalTypeIntUint16)
dst = append(dst, byte(additionalTypeTagNetworkPrefix>>8))
@@ -469,6 +488,25 @@ func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
return e.AppendUint8(dst, uint8(maskLen))
}
// 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, majorTypeTags|additionalTypeIntUint16)
File diff suppressed because it is too large Load Diff
+30
View File
@@ -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))
}
}
+2 -2
View File
@@ -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
+10 -8
View File
@@ -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)
}
}
}
+433
View File
@@ -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)
}
}
}
+356
View File
@@ -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
}
+4 -4
View File
@@ -5,7 +5,7 @@ import (
"unicode/utf8"
)
const hex = "0123456789abcdef"
const hexCharacters = "0123456789abcdef"
var noEscapeTable = [256]bool{}
@@ -66,7 +66,7 @@ func (Encoder) AppendString(dst []byte, s string) []byte {
// 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 len(vals) == 0 {
if vals == nil || len(vals) == 0 {
return append(dst, '[', ']')
}
dst = append(dst, '[')
@@ -88,7 +88,7 @@ func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte {
return e.AppendString(dst, val.String())
}
//// appendStringComplex is used by appendString to take over an in
// appendStringComplex is used by appendString to take over an in
// progress JSON string encoding that encountered a character that needs
// to be encoded.
func appendStringComplex(dst []byte, s string, i int) []byte {
@@ -137,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
+42 -63
View File
@@ -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)
}
}
}
+19 -8
View File
@@ -7,10 +7,13 @@ import (
const (
// Import from zerolog/global.go
timeFormatUnix = ""
timeFormatUnixMs = "UNIXMS"
timeFormatUnixMicro = "UNIXMICRO"
timeFormatUnixNano = "UNIXNANO"
timeFormatUnix = ""
timeFormatUnixMs = "UNIXMS"
timeFormatUnixMicro = "UNIXMICRO"
timeFormatUnixNano = "UNIXNANO"
durationFormatFloat = "float"
durationFormatInt = "int"
durationFormatString = "string"
)
// AppendTime formats the input time with the given format
@@ -88,24 +91,32 @@ func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte {
// 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, precision int) []byte {
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, useInt bool, precision int) []byte {
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, useInt, precision)
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, useInt, precision)
dst = e.AppendDuration(append(dst, ','), d, unit, format, useInt, precision)
}
}
dst = append(dst, ']')
+373
View File
@@ -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")
}
})
}
}
+37 -6
View File
@@ -418,18 +418,49 @@ func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
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())
}
+235 -344
View File
@@ -1,14 +1,141 @@
package json
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"math"
"math/rand"
"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)) },
@@ -70,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
@@ -88,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
@@ -121,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
@@ -155,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
@@ -173,7 +401,7 @@ func Test_appendMac(t *testing.T) {
}
}
func Test_appendType(t *testing.T) {
func TestAppendType2(t *testing.T) {
typeTests := []struct {
label string
input interface{}
@@ -195,7 +423,7 @@ func Test_appendType(t *testing.T) {
}
}
func Test_appendObjectData(t *testing.T) {
func TestAppendObjectData(t *testing.T) {
tests := []struct {
dst []byte
obj []byte
@@ -214,340 +442,3 @@ func Test_appendObjectData(t *testing.T) {
})
}
}
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
}
// 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)
}
}
}
+377
View File
@@ -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
}
+33 -4
View File
@@ -11,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".
@@ -31,6 +32,12 @@ import (
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
@@ -69,6 +76,24 @@ func levelToJPrio(zLevel string) journal.Priority {
return defaultJournalDPrio
}
// 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)
@@ -87,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
@@ -111,7 +136,11 @@ 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
+201 -5
View File
@@ -1,18 +1,21 @@
//go:build linux
// +build linux
package journald_test
package journald
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
"github.com/coreos/go-systemd/v22/journal"
"github.com/rs/zerolog"
"github.com/rs/zerolog/journald"
)
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:
}
@@ -49,9 +52,37 @@ Thu 2018-04-26 22:30:20.768136 PDT [s=3284d695bde946e4b5017c77a399237f;i=329f0;b
_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 := journald.NewJournalDWriter()
wr := NewJournalDWriter()
want := len(input)
got, err := wr.Write(input)
@@ -68,7 +99,7 @@ func TestMultiWrite(t *testing.T) {
var (
w1 = new(bytes.Buffer)
w2 = new(bytes.Buffer)
w3 = journald.NewJournalDWriter()
w3 = NewJournalDWriter()
)
zerolog.ErrorHandler = func(err error) {
@@ -84,3 +115,168 @@ func TestMultiWrite(t *testing.T) {
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
}
+80 -70
View File
@@ -2,85 +2,85 @@
//
// 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
//
@@ -89,11 +89,11 @@
// 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"}
//
// In this case, many consumers will take the last value,
// but this is not guaranteed; check yours if in doubt.
@@ -102,15 +102,15 @@
//
// 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()
// 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 {
// ...
// })
// }
// // Add context fields, for example User-Agent from HTTP headers
// logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
// ...
// })
// }
package zerolog
import (
@@ -294,7 +294,7 @@ func (l Logger) With() Context {
// 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 {
@@ -382,18 +382,24 @@ func (l *Logger) Err(err error) *Event {
return l.Info()
}
// Fatal starts a new message with fatal level. The os.Exit(1) function
// is called by the Msg method, which terminates the program immediately.
// 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) {
if closer, ok := l.w.(io.Closer); ok {
// Close the writer to flush any buffered message. Otherwise the message
// will be lost as os.Exit() terminates the program immediately.
// could be lost if FatalExitFunc() terminates the program immediately or
// os.Exit(1) is called if not FatalExitFunc isn't set (default).
closer.Close()
}
os.Exit(1)
if FatalExitFunc != nil {
FatalExitFunc()
} else {
os.Exit(1) // untestable: terminates the program, cannot be covered
}
})
}
@@ -487,25 +493,29 @@ func (l *Logger) newEvent(level Level, done func(string)) *Event {
}
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
e.ctx = l.ctx
if level != NoLevel && LevelFieldName != "" {
e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
}
if l.context != nil && len(l.context) > 1 {
if len(l.context) > 1 {
e.buf = enc.AppendObjectData(e.buf, l.context)
}
if l.stack {
e.Stack()
}
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.w == nil {
if l.disabled() {
return false
}
if lvl < l.level || lvl < GlobalLevel() {
+92 -8
View File
@@ -1,8 +1,11 @@
//go:build !binary_log
// +build !binary_log
package log_test
import (
"bytes"
"context"
"errors"
"flag"
"os"
@@ -119,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.
@@ -147,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"}
}
+156 -32
View File
@@ -192,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"}
@@ -242,16 +243,17 @@ 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).
Dict(zerolog.Dict().
Str("bar", "baz").
Int("n", 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,{"bar":"baz","n":1}],"message":"hello world"}
@@ -288,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)
@@ -380,27 +410,29 @@ func ExampleEvent_Fields_slice() {
}
func ExampleContext_Dict() {
log := zerolog.New(os.Stdout).With().
Str("foo", "bar").
Dict("dict", zerolog.Dict().
Str("bar", "baz").
Int("n", 1),
).Logger()
ctx := zerolog.New(os.Stdout).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")
// 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"}
}
@@ -435,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().
@@ -446,7 +506,6 @@ func ExampleContext_EmbedObject() {
Logger()
log.Log().Msg("hello world")
// Output: {"foo":"bar","price":"$64.49","message":"hello world"}
}
@@ -507,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().
@@ -518,6 +588,17 @@ func ExampleContext_IPPrefix() {
// Output: {"Route":"192.168.0.0/24","message":"hello world"}
}
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().
@@ -560,3 +641,46 @@ func ExampleContext_Fields_slice() {
// 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"}
}
+491 -77
View File
@@ -3,8 +3,10 @@ package zerolog
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net"
"reflect"
"runtime"
@@ -121,17 +123,26 @@ func TestWith(t *testing.T) {
Float32("float32", 11.101).
Float64("float64", 12.30303).
Time("time", time.Time{}).
Ctx(context.Background())
Dur("dur", 3).
Ctx(context.Background()).
Any("any", "test").
Interface("interface", struct {
Pub string
Tag string `json:"tag"`
}{"a", "b"}).
Type("type", math.Phi)
_, file, line, _ := runtime.Caller(0)
caller := fmt.Sprintf("%s:%d", file, line+3)
log := ctx.Caller().Logger()
log.Log().Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11.101,"float64":12.30303,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()),
`{"string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11.101,"float64":12.30303,"time":"0001-01-01T00:00:00Z","dur":0.000003,"any":"test","interface":{"Pub":"a","tag":"b"},"type":"float64","caller":"`+caller+`"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
// Validate CallerWithSkipFrameCount.
out.Reset()
ctx = New(out).With()
_, file, line, _ = runtime.Caller(0)
caller = fmt.Sprintf("%s:%d", file, line+5)
log = ctx.CallerWithSkipFrameCount(3).Logger()
@@ -140,7 +151,62 @@ func TestWith(t *testing.T) {
}()
// The above line is a little contrived, but the line above should be the line due
// to the extra frame skip.
if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11.101,"float64":12.30303,"time":"0001-01-01T00:00:00Z","caller":"`+caller+`"}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestStackedWiths(t *testing.T) {
out := &bytes.Buffer{}
ctx := New(out).With().
Bool("bool", true)
ctx = ctx.Logger().With().
Int("int", 1)
log := ctx.Logger()
log.Log().Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()),
`{"bool":true,"int":1}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestWithPlurals(t *testing.T) {
out := &bytes.Buffer{}
ctx := New(out).
With().
Objects("objs", []LogObjectMarshaler{fixtureObj{"a", "z", 1}}).
Objects("objs_nil", nil).
ObjectsV("objs_v", fixtureObj{"A", "Z", 2}).
ObjectsV("objs_v_empty").
Strs("strs", []string{"foo", "bar"}).
Strs("strs_nil", nil).
StrsV("strs_v", "baz", "fizz").
StrsV("strs_v_empty").
Stringers("stringers", []fmt.Stringer{net.IP{127, 0, 0, 1}, time.Time{}.AddDate(0, 1, 2), 1 * time.Second, nil}).
Stringers("stringers_nil", nil).
StringersV("stringers_v", net.IP{127, 0, 0, 1}, time.Time{}.AddDate(0, 1, 2), 1*time.Second, nil).
StringersV("stringers_v_empty").
Errs("errs", []error{errors.New("some error"), errors.New("some other error"), nil, loggableError{fmt.Errorf("oops")}, nonLoggableError{fmt.Errorf("whoops"), 401}}).
Errs("errs_nil", nil).
Bools("bool", []bool{true, false}).
Ints("int", []int{1, 2}).
Ints8("int8", []int8{2, 3}).
Ints16("int16", []int16{3, 4}).
Ints32("int32", []int32{4, 5}).
Ints64("int64", []int64{5, 6}).
Uints("uint", []uint{6, 7}).
Uints8("uint8", []uint8{7, 8}).
Uints16("uint16", []uint16{8, 9}).
Uints32("uint32", []uint32{9, 10}).
Uints64("uint64", []uint64{10, 11}).
Floats32("float32", []float32{1.1, 2.2}).
Floats64("float64", []float64{2.2, 3.3}).
Times("time", []time.Time{time.Time{}.AddDate(0, 1, 2), time.Time{}.AddDate(4, 5, 6)}).
Durs("dur", []time.Duration{1 * time.Second, 2 * time.Second})
log := ctx.Logger()
log.Log().Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()),
`{"objs":[{"Pub":"a","Tag":"z","priv":1}],"objs_nil":[],"objs_v":[{"Pub":"A","Tag":"Z","priv":2}],"objs_v_empty":[],"strs":["foo","bar"],"strs_nil":[],"strs_v":["baz","fizz"],"strs_v_empty":[],"stringers":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_nil":[],"stringers_v":["127.0.0.1","0001-02-03 00:00:00 +0000 UTC","1s",null],"stringers_v_empty":[],"errs":["some error","some other error",null,{"l":"OOPS"},"whoops"],"errs_nil":[],"bool":[true,false],"int":[1,2],"int8":[2,3],"int16":[3,4],"int32":[4,5],"int64":[5,6],"uint":[6,7],"uint8":[7,8],"uint16":[8,9],"uint32":[9,10],"uint64":[10,11],"float32":[1.1,2.2],"float64":[2.2,3.3],"time":["0001-02-03T00:00:00Z","0005-06-07T00:00:00Z"],"dur":[1000,2000]}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
@@ -156,6 +222,7 @@ func TestWithReset(t *testing.T) {
Hex("hex", []byte{0x12, 0xef}).
Uint64("uint64", 10).
Float64("float64", 12.30303).
Stack().
Ctx(context.Background())
log := ctx.Logger()
log.Log().Msg("")
@@ -186,11 +253,92 @@ func TestFieldsMap(t *testing.T) {
"float32": float32(11),
"float64": float64(12),
"ipv6": net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34},
"ipnet": net.IPNet{IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}, Mask: net.CIDRMask(64, 128)},
"macaddr": net.HardwareAddr{0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E},
"dur": 1 * time.Second,
"time": time.Time{},
"obj": obj{"a", "b", 1},
"obj": fixtureObj{"a", "b", 1},
"objs": []LogObjectMarshaler{fixtureObj{"a", "b", 1}, fixtureObj{"c", "d", 2}},
"any": struct{ A string }{"test"},
"raw": json.RawMessage(`{"some":"json"}`),
}).Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"bool":true,"bytes":"bar","dur":1000,"error":"some error","float32":11,"float64":12,"int":1,"int16":3,"int32":4,"int64":5,"int8":2,"ipv6":"2001:db8:85a3::8a2e:370:7334","nil":null,"obj":{"Pub":"a","Tag":"b","priv":1},"string":"foo","time":"0001-01-01T00:00:00Z","uint":6,"uint16":8,"uint32":9,"uint64":10,"uint8":7}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()), `{"any":{"A":"test"},"bool":true,"bytes":"bar","dur":1000,"error":"some error","float32":11,"float64":12,"int":1,"int16":3,"int32":4,"int64":5,"int8":2,"ipnet":"2001:db8:85a3::8a2e:370:7334/64","ipv6":"2001:db8:85a3::8a2e:370:7334","macaddr":"00:1a:2b:3c:4d:5e","nil":null,"obj":{"Pub":"a","Tag":"b","priv":1},"objs":[{"Pub":"a","Tag":"b","priv":1},{"Pub":"c","Tag":"d","priv":2}],"raw":{"some":"json"},"string":"foo","time":"0001-01-01T00:00:00Z","uint":6,"uint16":8,"uint32":9,"uint64":10,"uint8":7}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestFieldsMap_Arrays(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().Fields(map[string]interface{}{
"strings": []string{"foo"},
"bools": []bool{true},
"errors": []error{errors.New("some error")},
"ints": []int{1},
"ints8": []int8{1},
"ints16": []int16{3},
"ints32": []int32{4},
"ints64": []int64{5},
"uints": []uint{6},
"uint8s": []uint8{44},
"uints16": []uint16{8},
"uints32": []uint32{9},
"uints64": []uint64{10},
"floats32": []float32{11},
"floats64": []float64{12},
"ipv6s": []net.IP{{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}},
"ipnets": []net.IPNet{{IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}, Mask: net.CIDRMask(64, 128)}},
"macaddrs": []net.HardwareAddr{{0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E}},
"durs": []time.Duration{1 * time.Second},
"times": []time.Time{{}},
"objs": []fixtureObj{{"a", "b", 1}},
}).Msg("")
// special case: []uint8 are logged as base64 string so we use "," for 44
if got, want := decodeIfBinaryToString(out.Bytes()), `{"bools":[true],"durs":[1000],"errors":["some error"],"floats32":[11],"floats64":[12],"ints":[1],"ints16":[3],"ints32":[4],"ints64":[5],"ints8":[1],"ipnets":["2001:db8:85a3::8a2e:370:7334/64"],"ipv6s":["2001:db8:85a3::8a2e:370:7334"],"macaddrs":["ABorPE1e"],"objs":[{"Pub":"a","tag":"b"}],"strings":["foo"],"times":["0001-01-01T00:00:00Z"],"uint8s":",","uints":[6],"uints16":[8],"uints32":[9],"uints64":[10]}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestWithErr(t *testing.T) {
var err error = nil
out := &bytes.Buffer{}
ctx := New(out).With().
Fields(map[string]interface{}{
"nil": nil,
"nilerror": err,
"error": errors.New("some error"),
"loggable": loggableError{errors.New("loggable")},
"non-loggable": nonLoggableError{fmt.Errorf("oops"), 401},
})
log := ctx.Logger()
log.Log().Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":"some error","loggable":{"l":"LOGGABLE"},"nil":null,"nilerror":null,"non-loggable":"oops"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestFieldsErr(t *testing.T) {
var err error = nil
out := &bytes.Buffer{}
log := New(out)
log.Log().Fields(map[string]interface{}{
"nil": nil,
"nilerror": err,
"error": errors.New("some error"),
"loggable": loggableError{errors.New("loggable")},
"non-loggable": nonLoggableError{fmt.Errorf("oops"), 401},
}).Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":"some error","loggable":{"l":"LOGGABLE"},"nil":null,"nilerror":null,"non-loggable":"oops"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestFieldsErrs(t *testing.T) {
var err error = nil
out := &bytes.Buffer{}
log := New(out)
log.Log().Fields(map[string]interface{}{
"errors": []error{errors.New("some error"), nil, err, loggableError{errors.New("loggable")}, nonLoggableError{fmt.Errorf("oops"), 404}},
}).Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"errors":["some error",null,null,{"l":"LOGGABLE"},"oops"]}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
@@ -291,7 +439,7 @@ func TestFieldsSlice(t *testing.T) {
"ipv6", net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34},
"dur", 1 * time.Second,
"time", time.Time{},
"obj", obj{"a", "b", 1},
"obj", fixtureObj{"a", "b", 1},
}).Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"nil":null,"string":"foo","bytes":"bar","error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"float32":11,"float64":12,"ipv6":"2001:db8:85a3::8a2e:370:7334","dur":1000,"time":"0001-01-01T00:00:00Z","obj":{"Pub":"a","Tag":"b","priv":1}}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
@@ -318,7 +466,7 @@ func TestFieldsNotMapSlice(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().
Fields(obj{"a", "b", 1}).
Fields(fixtureObj{"a", "b", 1}).
Fields("string").
Fields(1).
Msg("")
@@ -335,9 +483,9 @@ func TestFields(t *testing.T) {
caller := fmt.Sprintf("%s:%d", file, line+3)
log.Log().
Caller().
Object("obj", fixtureObj{"a", "z", 1}).
Str("string", "foo").
Stringer("stringer", net.IP{127, 0, 0, 1}).
Stringer("stringer_nil", nil).
Bytes("bytes", []byte("bar")).
Hex("hex", []byte{0x12, 0xef}).
RawJSON("json", []byte(`{"some":"json"}`)).
@@ -356,18 +504,57 @@ func TestFields(t *testing.T) {
Uint16("uint16", 8).
Uint32("uint32", 9).
Uint64("uint64", 10).
IPAddr("IPv4", net.IP{192, 168, 0, 100}).
IPAddr("IPv6", net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}).
MACAddr("Mac", net.HardwareAddr{0x00, 0x14, 0x22, 0x01, 0x23, 0x45}).
IPPrefix("Prefix", net.IPNet{IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}).
IPAddr("ipv4", net.IP{192, 168, 0, 100}).
IPAddr("ipv6", net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}).
MACAddr("mac", net.HardwareAddr{0x00, 0x14, 0x22, 0x01, 0x23, 0x45}).
IPPrefix("pfxv4", net.IPNet{IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}).
IPPrefix("pfxv6", net.IPNet{IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}, Mask: net.CIDRMask(64, 128)}).
Float32("float32", 11.1234).
Float64("float64", 12.321321321).
Dur("dur", 1*time.Second).
Time("time", time.Time{}).
TimeDiff("diff", now, now.Add(-10*time.Second)).
Ctx(context.Background()).
Type("type", "hello").
Any("any", struct{ A string }{"test"}).
Any("logobject", fixtureObj{"a", "z", 1}).
Stack().
Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","string":"foo","stringer":"127.0.0.1","stringer_nil":null,"bytes":"bar","hex":"12ef","json":{"some":"json"},"cbor":"data:application/cbor;base64,gwGCAgOCBAU=","func":"func_output","error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"IPv4":"192.168.0.100","IPv6":"2001:db8:85a3::8a2e:370:7334","Mac":"00:14:22:01:23:45","Prefix":"192.168.0.100/24","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","obj":{"Pub":"a","Tag":"z","priv":1},"string":"foo","stringer":"127.0.0.1","bytes":"bar","hex":"12ef","json":{"some":"json"},"cbor":"data:application/cbor;base64,gwGCAgOCBAU=","func":"func_output","error":"some error","bool":true,"int":1,"int8":2,"int16":3,"int32":4,"int64":5,"uint":6,"uint8":7,"uint16":8,"uint32":9,"uint64":10,"ipv4":"192.168.0.100","ipv6":"2001:db8:85a3::8a2e:370:7334","mac":"00:14:22:01:23:45","pfxv4":"192.168.0.100/24","pfxv6":"2001:db8:85a3::8a2e:370:7334/64","float32":11.1234,"float64":12.321321321,"dur":1000,"time":"0001-01-01T00:00:00Z","diff":10000,"type":"string","any":{"A":"test"},"logobject":{"Pub":"a","Tag":"z","priv":1}}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestFieldsArrayNil(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().
Objects("objs", nil).
ObjectsV("objs_v").
Strs("strs", nil).
StrsV("strs_v").
Stringers("stringers", nil).
StringersV("stringers_v").
Errs("err", nil).
Bools("bool", nil).
Ints("int", nil).
Ints8("int8", nil).
Ints16("int16", nil).
Ints32("int32", nil).
Ints64("int64", nil).
Uints("uint", nil).
Uints8("uint8", nil).
Uints16("uint16", nil).
Uints32("uint32", nil).
Uints64("uint64", nil).
Floats32("float32", nil).
Floats64("float64", nil).
Durs("dur", nil).
Times("time", nil).
IPAddrs("ip", nil).
IPPrefixes("pfx", nil).
Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"objs":[],"objs_v":[],"strs":[],"strs_v":[],"stringers":[],"stringers_v":[],"err":[],"bool":[],"int":[],"int8":[],"int16":[],"int32":[],"int64":[],"uint":[],"uint8":[],"uint16":[],"uint32":[],"uint64":[],"float32":[],"float64":[],"dur":[],"time":[],"ip":[],"pfx":[]}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
@@ -376,8 +563,12 @@ func TestFieldsArrayEmpty(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().
Strs("string", []string{}).
Stringers("stringer", []fmt.Stringer{}).
Objects("objs", []LogObjectMarshaler{}).
ObjectsV("objs_v").
Strs("strs", []string{}).
StrsV("strs_v").
Stringers("stringers", []fmt.Stringer{}).
StringersV("stringers_v").
Errs("err", []error{}).
Bools("bool", []bool{}).
Ints("int", []int{}).
@@ -394,8 +585,10 @@ func TestFieldsArrayEmpty(t *testing.T) {
Floats64("float64", []float64{}).
Durs("dur", []time.Duration{}).
Times("time", []time.Time{}).
IPAddrs("ip", []net.IP{}).
IPPrefixes("pfx", []net.IPNet{}).
Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":[],"stringer":[],"err":[],"bool":[],"int":[],"int8":[],"int16":[],"int32":[],"int64":[],"uint":[],"uint8":[],"uint16":[],"uint32":[],"uint64":[],"float32":[],"float64":[],"dur":[],"time":[]}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()), `{"objs":[],"objs_v":[],"strs":[],"strs_v":[],"stringers":[],"stringers_v":[],"err":[],"bool":[],"int":[],"int8":[],"int16":[],"int32":[],"int64":[],"uint":[],"uint8":[],"uint16":[],"uint32":[],"uint64":[],"float32":[],"float64":[],"dur":[],"time":[],"ip":[],"pfx":[]}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
@@ -404,8 +597,12 @@ func TestFieldsArraySingleElement(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().
Strs("string", []string{"foo"}).
Objects("obj", []LogObjectMarshaler{fixtureObj{"a", "z", 1}}).
ObjectsV("obj_v", fixtureObj{"A", "Z", 2}).
Strs("str", []string{"foo"}).
StrsV("str_v", "baz").
Stringers("stringer", []fmt.Stringer{net.IP{127, 0, 0, 1}}).
StringersV("stringer_v", net.IPv6loopback).
Errs("err", []error{errors.New("some error")}).
Bools("bool", []bool{true}).
Ints("int", []int{1}).
@@ -422,8 +619,10 @@ func TestFieldsArraySingleElement(t *testing.T) {
Floats64("float64", []float64{12}).
Durs("dur", []time.Duration{1 * time.Second}).
Times("time", []time.Time{{}}).
IPAddrs("ip", []net.IP{{192, 168, 0, 100}}).
IPPrefixes("pfx", []net.IPNet{{IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}}).
Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":["foo"],"stringer":["127.0.0.1"],"err":["some error"],"bool":[true],"int":[1],"int8":[2],"int16":[3],"int32":[4],"int64":[5],"uint":[6],"uint8":[7],"uint16":[8],"uint32":[9],"uint64":[10],"float32":[11],"float64":[12],"dur":[1000],"time":["0001-01-01T00:00:00Z"]}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()), `{"obj":[{"Pub":"a","Tag":"z","priv":1}],"obj_v":[{"Pub":"A","Tag":"Z","priv":2}],"str":["foo"],"str_v":["baz"],"stringer":["127.0.0.1"],"stringer_v":["::1"],"err":["some error"],"bool":[true],"int":[1],"int8":[2],"int16":[3],"int32":[4],"int64":[5],"uint":[6],"uint8":[7],"uint16":[8],"uint32":[9],"uint64":[10],"float32":[11],"float64":[12],"dur":[1000],"time":["0001-01-01T00:00:00Z"],"ip":["192.168.0.100"],"pfx":["192.168.0.100/24"]}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
@@ -432,8 +631,12 @@ func TestFieldsArrayMultipleElement(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
log.Log().
Strs("string", []string{"foo", "bar"}).
Stringers("stringer", []fmt.Stringer{nil, net.IP{127, 0, 0, 1}}).
Objects("objs", []LogObjectMarshaler{fixtureObj{"a", "z", 1}, fixtureObj{"b", "y", 2}}).
ObjectsV("objs_v", fixtureObj{"A", "Z", 3}, fixtureObj{"B", "Y", 4}).
Strs("strs", []string{"foo", "bar"}).
StrsV("strs_v", "baz", "fizz").
Stringers("stringers", []fmt.Stringer{nil, net.IP{127, 0, 0, 1}}).
StringersV("stringers_v", net.IPv4bcast, net.IPv6loopback).
Errs("err", []error{errors.New("some error"), nil}).
Bools("bool", []bool{true, false}).
Ints("int", []int{1, 0}).
@@ -450,8 +653,10 @@ func TestFieldsArrayMultipleElement(t *testing.T) {
Floats64("float64", []float64{12, 0}).
Durs("dur", []time.Duration{1 * time.Second, 0}).
Times("time", []time.Time{{}, {}}).
IPAddrs("ip", []net.IP{{192, 168, 0, 100}, {0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}}).
IPPrefixes("pfx", []net.IPNet{{IP: net.IP{192, 168, 0, 100}, Mask: net.CIDRMask(24, 32)}, {IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34}, Mask: net.CIDRMask(64, 128)}}).
Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"string":["foo","bar"],"stringer":[null,"127.0.0.1"],"err":["some error",null],"bool":[true,false],"int":[1,0],"int8":[2,0],"int16":[3,0],"int32":[4,0],"int64":[5,0],"uint":[6,0],"uint8":[7,0],"uint16":[8,0],"uint32":[9,0],"uint64":[10,0],"float32":[11,0],"float64":[12,0],"dur":[1000,0],"time":["0001-01-01T00:00:00Z","0001-01-01T00:00:00Z"]}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()), `{"objs":[{"Pub":"a","Tag":"z","priv":1},{"Pub":"b","Tag":"y","priv":2}],"objs_v":[{"Pub":"A","Tag":"Z","priv":3},{"Pub":"B","Tag":"Y","priv":4}],"strs":["foo","bar"],"strs_v":["baz","fizz"],"stringers":[null,"127.0.0.1"],"stringers_v":["255.255.255.255","::1"],"err":["some error",null],"bool":[true,false],"int":[1,0],"int8":[2,0],"int16":[3,0],"int32":[4,0],"int64":[5,0],"uint":[6,0],"uint8":[7,0],"uint16":[8,0],"uint32":[9,0],"uint64":[10,0],"float32":[11,0],"float64":[12,0],"dur":[1000,0],"time":["0001-01-01T00:00:00Z","0001-01-01T00:00:00Z"],"ip":["192.168.0.100","2001:db8:85a3::8a2e:370:7334"],"pfx":["192.168.0.100/24","2001:db8:85a3::8a2e:370:7334/64"]}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
@@ -461,8 +666,14 @@ func TestFieldsDisabled(t *testing.T) {
log := New(out).Level(InfoLevel)
now := time.Now()
log.Debug().
Objects("obj", []LogObjectMarshaler{fixtureObj{"a", "z", 1}}).
ObjectsV("obj_v", fixtureObj{"A", "Z", 2}).
Str("string", "foo").
Strs("strs", []string{"foo"}).
StrsV("strs_v", "baz").
Stringer("stringer", net.IP{127, 0, 0, 1}).
Stringers("stringers", []fmt.Stringer{net.IP{127, 0, 0, 1}}).
StringersV("stringers_v", net.IPv6loopback).
Bytes("bytes", []byte("bar")).
Hex("hex", []byte{0x12, 0xef}).
AnErr("some_err", nil).
@@ -484,7 +695,13 @@ func TestFieldsDisabled(t *testing.T) {
Dur("dur", 1*time.Second).
Time("time", time.Time{}).
TimeDiff("diff", now, now.Add(-10*time.Second)).
IPAddr("ip", net.IP{127, 0, 0, 1}).
IPPrefix("ip", net.IPNet{IP: net.IP{127, 0, 0, 1}, Mask: net.CIDRMask(24, 32)}).
MACAddr("mac", net.HardwareAddr{0x00, 0x14, 0x22, 0x01, 0x23, 0x45}).
Ctx(context.Background()).
Any("any", struct{ A string }{"test"}).
Interface("interface", fixtureObj{"a", "z", 1}).
Err(errors.New("some error")).
Msg("")
if got, want := decodeIfBinaryToString(out.Bytes()), ""; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
@@ -595,6 +812,28 @@ func TestSampling(t *testing.T) {
}
}
func TestDisableSampling(t *testing.T) {
// Save original state
original := samplingDisabled()
defer DisableSampling(original)
out := &bytes.Buffer{}
log := New(out).Sample(&BasicSampler{N: 2})
// Enable sampling disable
DisableSampling(true)
log.Log().Int("i", 1).Msg("")
log.Log().Int("i", 2).Msg("")
log.Log().Int("i", 3).Msg("")
log.Log().Int("i", 4).Msg("")
// All messages should be logged since sampling is disabled
if got, want := decodeIfBinaryToString(out.Bytes()), "{\"i\":1}\n{\"i\":2}\n{\"i\":3}\n{\"i\":4}\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
func TestDiscard(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
@@ -630,6 +869,10 @@ func (lw *levelWriter) WriteLevel(lvl Level, p []byte) (int, error) {
return len(p), nil
}
func (lw *levelWriter) Close() error {
return nil
}
func TestLevelWriter(t *testing.T) {
lw := &levelWriter{
ops: []struct {
@@ -653,10 +896,15 @@ func TestLevelWriter(t *testing.T) {
log.WithLevel(InfoLevel).Msg("7")
log.WithLevel(WarnLevel).Msg("8")
log.WithLevel(ErrorLevel).Msg("9")
log.WithLevel(FatalLevel).Msg("10")
log.WithLevel(PanicLevel).Msg("11")
log.WithLevel(NoLevel).Msg("nolevel-2")
log.WithLevel(-1).Msg("-1") // Same as TraceLevel
log.WithLevel(-2).Msg("-2") // Will log
log.WithLevel(-3).Msg("-3") // Will not log
log.WithLevel(-1).Msg("-1") // Same as TraceLevel
log.WithLevel(-2).Msg("-2") // Will log
log.WithLevel(-3).Msg("-3") // Will not log
log.WithLevel(Disabled).Msg("Disabled") // Will not log
log.Err(nil).Msg("e-1") // Will log at InfoLevel
log.Err(errors.New("some error")).Msg("e-2") // Will log at ErrorLevel
want := []struct {
l Level
@@ -673,15 +921,168 @@ func TestLevelWriter(t *testing.T) {
{InfoLevel, `{"level":"info","message":"7"}` + "\n"},
{WarnLevel, `{"level":"warn","message":"8"}` + "\n"},
{ErrorLevel, `{"level":"error","message":"9"}` + "\n"},
{FatalLevel, `{"level":"fatal","message":"10"}` + "\n"},
{PanicLevel, `{"level":"panic","message":"11"}` + "\n"},
{NoLevel, `{"message":"nolevel-2"}` + "\n"},
{Level(-1), `{"level":"trace","message":"-1"}` + "\n"},
{Level(-2), `{"level":"-2","message":"-2"}` + "\n"},
{InfoLevel, `{"level":"info","message":"e-1"}` + "\n"},
{ErrorLevel, `{"level":"error","error":"some error","message":"e-2"}` + "\n"},
}
if got := lw.ops; !reflect.DeepEqual(got, want) {
t.Errorf("invalid ops:\ngot:\n%v\nwant:\n%v", got, want)
}
}
func TestDisabledLevel(t *testing.T) {
lw := &levelWriter{
ops: []struct {
l Level
p string
}{},
}
// Allow extra-verbose logs.
SetGlobalLevel(TraceLevel - 1)
log := New(lw).Level(Disabled)
log.Error().Msg("0") // will not log
log.Log().Msg("nolevel-1") // will not log
log.WithLevel(ErrorLevel).Msg("3") // will not log
log.WithLevel(NoLevel).Msg("nolevel-2") // will not log
log.WithLevel(Disabled).Msg("Disabled") // will not log
want := []struct {
l Level
p string
}{}
if got := lw.ops; !reflect.DeepEqual(got, want) {
t.Errorf("invalid ops:\ngot:\n%v\nwant:\n%v", got, want)
}
}
func TestPanicLevel(t *testing.T) {
lw := &levelWriter{
ops: []struct {
l Level
p string
}{},
}
// Allow extra-verbose logs.
SetGlobalLevel(TraceLevel - 1)
log := New(lw).Level(TraceLevel - 1)
// Catch the panic from log.Panic().Msg("1")
defer func() {
if r := recover(); r == nil {
t.Error("expected panic from log.Panic()")
}
}()
log.Panic().Msg("1")
log.WithLevel(PanicLevel).Msg("2")
want := []struct {
l Level
p string
}{
{PanicLevel, `{"level":"panic","message":"1"}` + "\n"},
{PanicLevel, `{"level":"panic","message":"2"}` + "\n"},
}
if got := lw.ops; !reflect.DeepEqual(got, want) {
t.Errorf("invalid ops:\ngot:\n%v\nwant:\n%v", got, want)
}
}
func TestFatalLevel(t *testing.T) {
lw := &levelWriter{
ops: []struct {
l Level
p string
}{},
}
// Allow extra-verbose logs.
SetGlobalLevel(TraceLevel - 1)
log := New(lw).Level(TraceLevel - 1)
// Set FatalExitFunc to panic so we can catch it
oldFatalExitFunc := FatalExitFunc
FatalExitFunc = func() { panic("fatal exit") }
defer func() { FatalExitFunc = oldFatalExitFunc }()
// Catch the panic from log.Fatal().Msg("1")
defer func() {
if r := recover(); r == nil || r != "fatal exit" {
t.Errorf("expected panic 'fatal exit' from log.Fatal(), got %v", r)
}
}()
log.Fatal().Msg("1")
log.WithLevel(FatalLevel).Msg("2")
want := []struct {
l Level
p string
}{
{FatalLevel, `{"level":"fatal","message":"1"}` + "\n"},
{FatalLevel, `{"level":"fatal","message":"2"}` + "\n"},
}
if got := lw.ops; !reflect.DeepEqual(got, want) {
t.Errorf("invalid ops:\ngot:\n%v\nwant:\n%v", got, want)
}
}
func TestFatalDisabled(t *testing.T) {
out := &bytes.Buffer{}
log := New(out).Level(PanicLevel) // Disable FatalLevel
// Set FatalExitFunc to set a flag
var fatalCalled bool
oldFatalExitFunc := FatalExitFunc
FatalExitFunc = func() { fatalCalled = true }
defer func() { FatalExitFunc = oldFatalExitFunc }()
// Call Fatal, which should be disabled, call done, and return nil
e := log.Fatal()
if e != nil {
t.Error("Expected nil event when Fatal is disabled")
}
if !fatalCalled {
t.Error("Expected FatalExitFunc to be called when Fatal is disabled")
}
if out.Len() > 0 {
t.Errorf("Expected no output when Fatal is disabled, got: %s", out.String())
}
}
func TestPanicDisabled(t *testing.T) {
out := &bytes.Buffer{}
log := New(out).Level(Disabled) // Disable all levels
// Call Panic, which should be disabled, call done, and panic with ""
defer func() {
if r := recover(); r == nil || r != "" {
t.Errorf("Expected panic with empty string when Panic is disabled, got %v", r)
}
}()
e := log.Panic()
if e != nil {
t.Error("Expected nil event when Panic is disabled")
}
if out.Len() > 0 {
t.Errorf("Expected no output when Panic is disabled, got: %s", out.String())
}
}
func TestLoggerShouldWithNilWriter(t *testing.T) {
// Create a logger with nil writer to test the should method's nil check
log := Logger{w: nil, level: TraceLevel}
e := log.Info()
if e != nil {
t.Error("Expected nil event when writer is nil")
}
}
func TestContextTimestamp(t *testing.T) {
TimestampFunc = func() time.Time {
return time.Date(2001, time.February, 3, 4, 5, 6, 7, time.UTC)
@@ -742,60 +1143,13 @@ func TestOutputWithTimestamp(t *testing.T) {
}
}
type loggableError struct {
error
}
func (l loggableError) MarshalZerologObject(e *Event) {
e.Str("message", l.error.Error()+": loggableError")
}
func TestErrorMarshalFunc(t *testing.T) {
func TestOutputWithContext(t *testing.T) {
ignoredOut := &bytes.Buffer{}
out := &bytes.Buffer{}
log := New(out)
log := New(ignoredOut).With().Str("foo", "bar").Logger().Output(out)
log.Log().Msg("hello world")
// test default behaviour
log.Log().Err(errors.New("err")).Msg("msg")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":"err","message":"msg"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
out.Reset()
log.Log().Err(loggableError{errors.New("err")}).Msg("msg")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":{"message":"err: loggableError"},"message":"msg"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
out.Reset()
// test overriding the ErrorMarshalFunc
originalErrorMarshalFunc := ErrorMarshalFunc
defer func() {
ErrorMarshalFunc = originalErrorMarshalFunc
}()
ErrorMarshalFunc = func(err error) interface{} {
return err.Error() + ": marshaled string"
}
log.Log().Err(errors.New("err")).Msg("msg")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":"err: marshaled string","message":"msg"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
out.Reset()
ErrorMarshalFunc = func(err error) interface{} {
return errors.New(err.Error() + ": new error")
}
log.Log().Err(errors.New("err")).Msg("msg")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":"err: new error","message":"msg"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
out.Reset()
ErrorMarshalFunc = func(err error) interface{} {
return loggableError{err}
}
log.Log().Err(errors.New("err")).Msg("msg")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"error":{"message":"err: loggableError"},"message":"msg"}`+"\n"; got != want {
if got, want := decodeIfBinaryToString(out.Bytes()), `{"foo":"bar","message":"hello world"}`+"\n"; got != want {
t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want)
}
}
@@ -804,9 +1158,12 @@ func TestCallerMarshalFunc(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
var pc uintptr
var file string
var line int
// test default behaviour this is really brittle due to the line numbers
// actually mattering for validation
pc, file, line, _ := runtime.Caller(0)
pc, file, line, _ = runtime.Caller(0)
caller := fmt.Sprintf("%s:%d", file, line+2)
log.Log().Caller().Msg("msg")
if got, want := decodeIfBinaryToString(out.Bytes()), `{"caller":"`+caller+`","message":"msg"}`+"\n"; got != want {
@@ -906,6 +1263,55 @@ func TestUpdateEmptyContext(t *testing.T) {
}
}
func TestUpdateContextOnDisabledLogger(t *testing.T) {
var buf bytes.Buffer
log := disabledLogger
log.UpdateContext(func(c Context) Context {
return c.Str("foo", "bar")
})
log.Info().Msg("no panic")
want := ""
if got := decodeIfBinaryToString(buf.Bytes()); got != want {
t.Errorf("invalid log output:\ngot: %q\nwant: %q", got, want)
}
}
func TestUpdateContextOnNopLogger(t *testing.T) {
// Nop() creates a new disabled logger that is a different pointer than
// the package-level disabledLogger. UpdateContext should still treat it
// as disabled and skip the update function. See issue #643.
log := Nop()
called := false
log.UpdateContext(func(c Context) Context {
called = true
return c.Str("foo", "bar")
})
if called {
t.Error("UpdateContext should not call update func on a Nop logger")
}
}
func TestUpdateContextOnZeroValueLogger(t *testing.T) {
// A zero-value Logger has a nil writer and should be treated as
// disabled by UpdateContext. See issue #643.
var log Logger
called := false
log.UpdateContext(func(c Context) Context {
called = true
return c.Str("foo", "bar")
})
if called {
t.Error("UpdateContext should not call update func on a zero-value logger")
}
}
func TestLevel_String(t *testing.T) {
tests := []struct {
name string
@@ -1033,6 +1439,14 @@ func TestUnmarshalTextLevel(t *testing.T) {
}
}
func TestUnmarshalTextLevelNil(t *testing.T) {
var l *Level
err := l.UnmarshalText([]byte("info"))
if err == nil || err.Error() != "can't unmarshal a nil *Level" {
t.Errorf("UnmarshalText() on nil *Level error = %v, want 'can't unmarshal a nil *Level'", err)
}
}
func TestHTMLNoEscaping(t *testing.T) {
out := &bytes.Buffer{}
log := New(out)
+4 -1
View File
@@ -47,6 +47,9 @@ 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
}
@@ -87,7 +90,7 @@ func (s *BurstSampler) inc() uint32 {
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()
+98
View File
@@ -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")
}
}
+247
View File
@@ -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
View File
@@ -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")
}
}
+129 -1
View File
@@ -5,6 +5,7 @@ package zerolog
import (
"bytes"
"io"
"reflect"
"strings"
"testing"
@@ -19,7 +20,7 @@ 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})
@@ -106,3 +107,130 @@ func TestSyslogWriter_WithCEE(t *testing.T) {
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"))
}
+9
View File
@@ -213,6 +213,15 @@ func (w *FilteredLevelWriter) WriteLevel(level Level, p []byte) (int, error) {
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))
+298
View File
@@ -12,6 +12,36 @@ import (
"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)))
@@ -250,3 +280,271 @@ func TestTriggerLevelWriter(t *testing.T) {
})
}
}
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")
}
}