Conversation
ResolveComponentLevel split the component on "." and rejoined the remaining parts on every lookup, so resolving rtc.room.track allocated a slice and a string per level. Walk backwards from the end of the string instead, reslicing to the last "." after each miss. The map lookups take those substrings directly, so the walk no longer allocates. What remains is ParseZapLevel, whose []byte conversion escapes into zapcore's UnmarshalText.
Marshaling a Config read its fields alongside whatever Update was writing. MarshalYAML now snapshots them under the same lock. The snapshot needs a type without the mutex: the usual `type config Config` cast either returns before the encoder reads the fields, which defers the read back outside the lock, or copies the mutex to avoid that. So the yaml-visible fields are mirrored on configYAML, and TestConfigYAMLFields fails if the two drift. ComponentLevels is cloned because the encoder walks the returned value after the lock is released.
🦋 Changeset detectedLatest commit: d77b4a5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
boks1971
approved these changes
Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent changes to
logger.Config.ResolveComponentLevelwithout allocationsIt split the component on
"."and rejoined the remaining parts on every lookup, so resolvingrtc.room.trackallocated a slice and a string per level. It now walks backwards from the end of the string, reslicing to the last"."after each miss, and the map lookups take those substrings directly.Measured with
testing.AllocsPerRun: the walk is 0 allocs. The one that remains isParseZapLevel, whose[]byte(level)conversion escapes because zapcore'sUnmarshalTexthands the bytes tofmt.Errorf. Killing that means duplicating zapcore's level table in a string switch — not done here.Edge cases match the old behavior exactly: empty string, leading dot, and trailing dot all resolve as before.
yaml.MarshaleronConfigMarshaling read the fields alongside whatever
Updatewas writing.MarshalYAMLnow snapshots under the same lock.The snapshot needs a type without the mutex. The usual
type config Configcast doesn't give one: returning(*config)(c)defers the field reads until after the lock is released, and dereferencing to snapshot tripscopylocks. Encoding into ayaml.Nodeinside the lock looked like a way out, but yaml's encoder dereferences the pointer and callsin.Interface()on the struct (encode.go:117), which copies the locked mutex where neithervetnor-racecan see it. So the yaml-visible fields are mirrored onconfigYAML, withTestConfigYAMLFieldsreflecting over both types to fail if a field or tag drifts.ComponentLevelsis cloned because the encoder walks the returned value after the lock drops.Note that yaml.v3 does not take the address of struct fields, so a config holding
logger.Configby value skips this method and falls back to field reflection — same output, no locked snapshot. Holding*logger.Configgets the lock.Considered and rejected
Moving the fields into an embedded struct would define them once and collapse
Updateto a single assignment. Go 1.27 allows promoted fields in composite literals, soConfig{Level: "info"}keeps compiling — but only for callers whose own module declaresgo 1.27; the library's directive doesn't matter. Callers on 1.26 would need to bump or rewrite the literal, and that's 93 sites across ~20 repos here, all still on 1.26. Worth revisiting once the fleet moves, at which point it costs consumers nothing.Test plan
go test -race ./logger/...,go vet ./logger/...Unmarshal, and a mutate-after-marshal check that the snapshot held