Skip to content

Commit cd87e28

Browse files
committed
fix(logsentry): remap Sentry-reserved "type" context key to "type_"
Sentry reserves the key "type" inside every context object to denote the context type. A golog value logged under "type" would be consumed as the "log" context's type instead of shown as data. The removed Event.Extra map had no reserved keys, so this collision is unique to the context encoding. Remap "type" -> "type_" when assembling the context (covering both per-message values and config extra), document it on the Writer, and test both sources.
1 parent ab34309 commit cd87e28

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

logsentry/writer.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"maps"
87
"runtime/debug"
98
"strings"
109
"sync"
@@ -109,6 +108,10 @@ func (c *WriterConfig) FlushUnderlying() {
109108
//
110109
// TRACE/DEBUG -> DEBUG, INFO -> INFO, WARN -> WARNING, ERROR -> ERROR, FATAL -> FATAL
111110
//
111+
// Sentry reserves the key "type" inside every context object to denote the
112+
// context type, so a value logged under the key "type" is remapped to "type_"
113+
// to keep it visible as data (see [copyContextValues]).
114+
//
112115
// Example usage through golog:
113116
//
114117
// logger.Error("Database error").Str("query", sql).Err(err).Log()
@@ -160,7 +163,8 @@ func (w *Writer) BeginMessage(config golog.Config, timestamp time.Time, level go
160163
// - The accumulated message text
161164
// - The mapped Sentry level
162165
// - The original timestamp
163-
// - All key-value pairs as a "log" context (from both config.extra and values)
166+
// - All key-value pairs as a "log" context (from both config.extra and
167+
// values), with the Sentry-reserved "type" key remapped to "type_"
164168
// - Optional stack trace (if enabled in Sentry options)
165169
// - A fingerprint based on the message for grouping
166170
//
@@ -192,8 +196,8 @@ func (w *Writer) CommitMessage() {
192196
// sentry-go v0.46.0 removed Event.Extra; attach the key-value pairs
193197
// as a named context instead (sentry.Context is map[string]any).
194198
logCtx := make(sentry.Context, len(w.config.extra)+len(w.values))
195-
maps.Copy(logCtx, w.config.extra)
196-
maps.Copy(logCtx, w.values)
199+
copyContextValues(logCtx, w.config.extra)
200+
copyContextValues(logCtx, w.values)
197201
if len(logCtx) > 0 {
198202
event.Contexts["log"] = logCtx
199203
}
@@ -209,6 +213,34 @@ func (w *Writer) CommitMessage() {
209213
}
210214
}
211215

216+
const (
217+
// reservedContextKey is the key Sentry reserves inside every context
218+
// object to identify the context's type. Within our "log" context it
219+
// defaults to "log" when absent; a value logged under this key would be
220+
// consumed as the context type instead of being shown as data.
221+
// See https://develop.sentry.dev/sdk/data-model/event-payloads/contexts/.
222+
reservedContextKey = "type"
223+
224+
// remappedContextKey is where a logged "type" value is stored instead, so
225+
// it survives as ordinary context data. The Event.Extra map removed in
226+
// sentry-go v0.46.0 had no reserved keys, so this collision is unique to
227+
// the context-based encoding.
228+
remappedContextKey = "type_"
229+
)
230+
231+
// copyContextValues copies src into dst, remapping the Sentry-reserved
232+
// [reservedContextKey] ("type") to [remappedContextKey] ("type_") so a golog
233+
// value logged under "type" is preserved as data rather than swallowed by
234+
// Sentry as the context type.
235+
func copyContextValues(dst, src map[string]any) {
236+
for k, v := range src {
237+
if k == reservedContextKey {
238+
k = remappedContextKey
239+
}
240+
dst[k] = v
241+
}
242+
}
243+
212244
// filterFrames removes golog internal frames from stack traces to provide
213245
// cleaner Sentry debugging information by focusing on application code.
214246
func filterFrames(frames []sentry.Frame) []sentry.Frame {

logsentry/writer_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,34 @@ func TestWriterContextValues(t *testing.T) {
108108
}
109109
}
110110

111+
// TestWriterReservedTypeKey verifies that a value logged under the Sentry-
112+
// reserved "type" key is remapped to "type_" so it survives as context data,
113+
// for both per-message values and config-level extra.
114+
func TestWriterReservedTypeKey(t *testing.T) {
115+
transport := &captureTransport{}
116+
hub := newTestHub(t, transport)
117+
118+
config := NewWriterConfig(hub, golog.NewDefaultFormat(), golog.AllLevelsActive, false,
119+
map[string]any{"type": "from-extra"})
120+
logger := golog.NewLogger(golog.NewConfig(&golog.DefaultLevels, golog.AllLevelsActive, config))
121+
122+
logger.Error("boom").Str("type", "invoice").Log()
123+
hub.Flush(time.Second)
124+
125+
if len(transport.events) != 1 {
126+
t.Fatalf("expected 1 event, got %d", len(transport.events))
127+
}
128+
logCtx := transport.events[0].Contexts["log"]
129+
130+
if _, present := logCtx["type"]; present {
131+
t.Errorf(`log["type"] should be absent (reserved); got %#v`, logCtx["type"])
132+
}
133+
// Per-message value wins over config extra under the remapped key.
134+
if got := logCtx["type_"]; got != "invoice" {
135+
t.Errorf(`log["type_"] = %#v, want %q`, got, "invoice")
136+
}
137+
}
138+
111139
// TestWriterTimeDefaultFormat verifies time formatting falls back to
112140
// golog.DefaultTimeFormat when Format.TimeFormat is empty, in the time's
113141
// original location when Format.Location is nil.

0 commit comments

Comments
 (0)