Skip to content

Commit 7aeeb8d

Browse files
author
ufuk
committed
feat: exporting logs to file
1 parent e5c93b0 commit 7aeeb8d

File tree

8 files changed

+262
-67
lines changed

8 files changed

+262
-67
lines changed

example.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ server:
2222
# The logger section sets the logging level for the service.
2323
logger:
2424
level: info
25+
file: ""
2526

2627
# The profiler section enables or disables the pprof profiler and
2728
# sets the port number for the profiler endpoint.

go.mod

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ require (
8181
github.com/moby/docker-image-spec v1.3.1 // indirect
8282
github.com/moby/sys/user v0.1.0 // indirect
8383
github.com/rivo/uniseg v0.4.4 // indirect
84-
github.com/rogpeppe/go-internal v1.12.0 // indirect
8584
github.com/sethvargo/go-retry v0.2.4 // indirect
8685
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
8786
)
@@ -142,7 +141,6 @@ require (
142141
github.com/prometheus/client_model v0.3.0 // indirect
143142
github.com/prometheus/common v0.42.0 // indirect
144143
github.com/prometheus/procfs v0.12.0 // indirect
145-
github.com/remychantenay/slog-otel v1.3.1
146144
github.com/sagikazarmark/locafero v0.4.0 // indirect
147145
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
148146
github.com/shirou/gopsutil/v3 v3.24.1 // indirect
@@ -166,7 +164,6 @@ require (
166164
golang.org/x/sys v0.20.0 // indirect
167165
golang.org/x/text v0.15.0 // indirect
168166
golang.org/x/tools v0.20.0 // indirect
169-
google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 // indirect
170167
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
171168
gopkg.in/ini.v1 v1.67.0 // indirect
172169
)

go.sum

Lines changed: 7 additions & 51 deletions
Large diffs are not rendered by default.

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type (
8888
Log struct {
8989
Level string `mapstructure:"level"` // Logging level
9090
Output string `mapstructure:"output"` // Logging output format, e.g., text, json
91+
File string `mapstructure:"file"` // Logging whether to file or not
9192
}
9293

9394
// Tracer contains configuration for distributed tracing.

pkg/cmd/serve.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"io"
78
"log/slog"
89
"os"
910
"os/signal"
1011
"strings"
1112
"syscall"
1213
"time"
1314

14-
slogotel "github.com/remychantenay/slog-otel"
1515
"github.com/sony/gobreaker"
1616
"github.com/spf13/cobra"
1717
"github.com/spf13/viper"
@@ -170,22 +170,40 @@ func serve() func(cmd *cobra.Command, args []string) error {
170170

171171
var handler slog.Handler
172172

173+
var ioWriter io.Writer
174+
175+
ioWriter = os.Stdout
176+
177+
if cfg.Log.File != "" {
178+
if err := os.MkdirAll(cfg.Log.File, 0755); err != nil {
179+
panic(err)
180+
}
181+
182+
file, err := os.OpenFile(cfg.Log.File+"/app.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
183+
if err != nil {
184+
panic(err)
185+
}
186+
defer file.Close()
187+
ioWriter = io.MultiWriter(file, os.Stdout)
188+
189+
}
190+
173191
switch cfg.Log.Output {
174192
case "json":
175-
handler = slogotel.OtelHandler{
176-
Next: slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
193+
handler = telemetry.OtelHandler{
194+
Next: slog.NewJSONHandler(ioWriter, &slog.HandlerOptions{
177195
Level: getLogLevel(cfg.Log.Level),
178196
}),
179197
}
180198
case "text":
181-
handler = slogotel.OtelHandler{
182-
Next: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
199+
handler = telemetry.OtelHandler{
200+
Next: slog.NewTextHandler(ioWriter, &slog.HandlerOptions{
183201
Level: getLogLevel(cfg.Log.Level),
184202
}),
185203
}
186204
default:
187-
handler = slogotel.OtelHandler{
188-
Next: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
205+
handler = telemetry.OtelHandler{
206+
Next: slog.NewTextHandler(ioWriter, &slog.HandlerOptions{
189207
Level: getLogLevel(cfg.Log.Level),
190208
}),
191209
}
@@ -259,7 +277,7 @@ func serve() func(cmd *cobra.Command, args []string) error {
259277
slog.Error(err.Error())
260278
}
261279

262-
shutdown := telemetry.NewTracer(exporter)
280+
shutdown := telemetry.NewTracer(cfg.AccountID, exporter)
263281

264282
defer func() {
265283
if err = shutdown(context.Background()); err != nil {
@@ -311,7 +329,7 @@ func serve() func(cmd *cobra.Command, args []string) error {
311329
slog.Error(err.Error())
312330
}
313331

314-
meter, err = telemetry.NewMeter(exporter)
332+
meter, err = telemetry.NewMeter(cfg.AccountID, exporter)
315333
if err != nil {
316334
slog.Error(err.Error())
317335
}

pkg/telemetry/meter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import (
66
"time"
77

88
"go.opentelemetry.io/otel/metric/noop"
9+
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
910

1011
"go.opentelemetry.io/contrib/instrumentation/host"
1112
orn "go.opentelemetry.io/contrib/instrumentation/runtime"
1213
"go.opentelemetry.io/otel/attribute"
1314
omt "go.opentelemetry.io/otel/metric"
1415
"go.opentelemetry.io/otel/sdk/metric"
1516
"go.opentelemetry.io/otel/sdk/resource"
16-
"go.opentelemetry.io/otel/semconv/v1.10.0"
1717

1818
"github.com/Permify/permify/internal"
1919
)
2020

2121
// NewMeter - Creates new meter
22-
func NewMeter(exporter metric.Exporter) (omt.Meter, error) {
22+
func NewMeter(accountId string, exporter metric.Exporter) (omt.Meter, error) {
2323
hostName, err := os.Hostname()
2424
if err != nil {
2525
return nil, err

pkg/telemetry/slogotel.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package telemetry
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"time"
8+
9+
"go.opentelemetry.io/otel/attribute"
10+
"go.opentelemetry.io/otel/baggage"
11+
"go.opentelemetry.io/otel/codes"
12+
"go.opentelemetry.io/otel/trace"
13+
)
14+
15+
const (
16+
// TraceIDKey is the key used by the Otel handler
17+
// to inject the trace ID in the log record.
18+
TraceIDKey = "TraceId"
19+
// SpanIDKey is the key used by the Otel handler
20+
// to inject the span ID in the log record.
21+
SpanIDKey = "SpanId"
22+
// SpanEventKey is the key used by the Otel handler
23+
// to inject the log record in the recording span, as a span event.
24+
SpanEventKey = "LogRecord"
25+
)
26+
27+
// OtelHandler is an implementation of slog's Handler interface.
28+
// Its role is to ensure correlation between logs and OTel spans
29+
// by:
30+
//
31+
// 1. Adding otel span and trace IDs to the log record.
32+
// 2. Adding otel context baggage members to the log record.
33+
// 3. Setting slog record as otel span event.
34+
// 4. Adding slog record attributes to the otel span event.
35+
// 5. Setting span status based on slog record level (only if >= slog.LevelError).
36+
type OtelHandler struct {
37+
// Next represents the next handler in the chain.
38+
Next slog.Handler
39+
// NoBaggage determines whether to add context baggage members to the log record.
40+
NoBaggage bool
41+
// NoTraceEvents determines whether to record an event for every log on the active trace.
42+
NoTraceEvents bool
43+
}
44+
45+
type OtelHandlerOpt func(handler *OtelHandler)
46+
47+
// HandlerFn defines the handler used by slog.Handler as return value.
48+
type HandlerFn func(slog.Handler) slog.Handler
49+
50+
// WithNoBaggage returns an OtelHandlerOpt, which sets the NoBaggage flag
51+
func WithNoBaggage(noBaggage bool) OtelHandlerOpt {
52+
return func(handler *OtelHandler) {
53+
handler.NoBaggage = noBaggage
54+
}
55+
}
56+
57+
// WithNoTraceEvents returns an OtelHandlerOpt, which sets the NoTraceEvents flag
58+
func WithNoTraceEvents(noTraceEvents bool) OtelHandlerOpt {
59+
return func(handler *OtelHandler) {
60+
handler.NoTraceEvents = noTraceEvents
61+
}
62+
}
63+
64+
// New creates a new OtelHandler to use with log/slog
65+
func New(next slog.Handler, opts ...OtelHandlerOpt) *OtelHandler {
66+
ret := &OtelHandler{
67+
Next: next,
68+
}
69+
for _, opt := range opts {
70+
opt(ret)
71+
}
72+
return ret
73+
}
74+
75+
// NewOtelHandler creates and returns a new HandlerFn, which wraps a handler with OtelHandler to use with log/slog.
76+
func NewOtelHandler(opts ...OtelHandlerOpt) HandlerFn {
77+
return func(next slog.Handler) slog.Handler {
78+
return New(next, opts...)
79+
}
80+
}
81+
82+
// Handle handles the provided log record and adds correlation between a slog record and an Open-Telemetry span.
83+
func (h OtelHandler) Handle(ctx context.Context, record slog.Record) error {
84+
if ctx == nil {
85+
return h.Next.Handle(ctx, record)
86+
}
87+
88+
if !h.NoBaggage {
89+
// Adding context baggage members to log record.
90+
b := baggage.FromContext(ctx)
91+
for _, m := range b.Members() {
92+
record.AddAttrs(slog.String(m.Key(), m.Value()))
93+
}
94+
}
95+
96+
span := trace.SpanFromContext(ctx)
97+
if span == nil || !span.IsRecording() {
98+
return h.Next.Handle(ctx, record)
99+
}
100+
101+
if !h.NoTraceEvents {
102+
// Adding log info to span event.
103+
eventAttrs := make([]attribute.KeyValue, 0, record.NumAttrs())
104+
eventAttrs = append(eventAttrs, attribute.String(slog.MessageKey, record.Message))
105+
eventAttrs = append(eventAttrs, attribute.String(slog.LevelKey, record.Level.String()))
106+
eventAttrs = append(eventAttrs, attribute.String(slog.TimeKey, record.Time.Format(time.RFC3339Nano)))
107+
record.Attrs(func(attr slog.Attr) bool {
108+
otelAttr := h.slogAttrToOtelAttr(attr)
109+
if otelAttr.Valid() {
110+
eventAttrs = append(eventAttrs, otelAttr)
111+
}
112+
113+
return true
114+
})
115+
116+
span.AddEvent(SpanEventKey, trace.WithAttributes(eventAttrs...))
117+
}
118+
119+
// Adding span info to log record.
120+
spanContext := span.SpanContext()
121+
if spanContext.HasTraceID() {
122+
traceID := spanContext.TraceID().String()
123+
record.AddAttrs(slog.String(TraceIDKey, traceID))
124+
}
125+
126+
if spanContext.HasSpanID() {
127+
spanID := spanContext.SpanID().String()
128+
record.AddAttrs(slog.String(SpanIDKey, spanID))
129+
}
130+
131+
// Setting span status if the log is an error.
132+
// Purposely leaving as codes.Unset (default) otherwise.
133+
if record.Level >= slog.LevelError {
134+
span.SetStatus(codes.Error, record.Message)
135+
}
136+
137+
return h.Next.Handle(ctx, record)
138+
}
139+
140+
// WithAttrs returns a new Otel whose attributes consists of handler's attributes followed by attrs.
141+
func (h OtelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
142+
return OtelHandler{
143+
Next: h.Next.WithAttrs(attrs),
144+
NoBaggage: h.NoBaggage,
145+
NoTraceEvents: h.NoTraceEvents,
146+
}
147+
}
148+
149+
// WithGroup returns a new Otel with a group, provided the group's name.
150+
func (h OtelHandler) WithGroup(name string) slog.Handler {
151+
return OtelHandler{
152+
Next: h.Next.WithGroup(name),
153+
NoBaggage: h.NoBaggage,
154+
NoTraceEvents: h.NoTraceEvents,
155+
}
156+
}
157+
158+
// Enabled reports whether the logger emits log records at the given context and level.
159+
// Note: We handover the decision down to the next handler.
160+
func (h OtelHandler) Enabled(ctx context.Context, level slog.Level) bool {
161+
return h.Next.Enabled(ctx, level)
162+
}
163+
164+
// slogAttrToOtelAttr converts a slog attribute to an OTel one.
165+
// Note: returns an empty attribute if the provided slog attribute is empty.
166+
func (h OtelHandler) slogAttrToOtelAttr(attr slog.Attr, groupKeys ...string) attribute.KeyValue {
167+
attr.Value = attr.Value.Resolve()
168+
if attr.Equal(slog.Attr{}) {
169+
return attribute.KeyValue{}
170+
}
171+
172+
key := func(k string, prefixes ...string) string {
173+
for _, prefix := range prefixes {
174+
k = fmt.Sprintf("%s.%s", prefix, k)
175+
}
176+
177+
return k
178+
}(attr.Key, groupKeys...)
179+
180+
value := attr.Value.Resolve()
181+
182+
switch attr.Value.Kind() {
183+
case slog.KindBool:
184+
return attribute.Bool(key, value.Bool())
185+
case slog.KindFloat64:
186+
return attribute.Float64(key, value.Float64())
187+
case slog.KindInt64:
188+
return attribute.Int64(key, value.Int64())
189+
case slog.KindString:
190+
return attribute.String(key, value.String())
191+
case slog.KindTime:
192+
return attribute.String(key, value.Time().Format(time.RFC3339Nano))
193+
case slog.KindGroup:
194+
groupAttrs := value.Group()
195+
if len(groupAttrs) == 0 {
196+
return attribute.KeyValue{}
197+
}
198+
199+
for _, groupAttr := range groupAttrs {
200+
return h.slogAttrToOtelAttr(groupAttr, append(groupKeys, key)...)
201+
}
202+
case slog.KindAny:
203+
switch v := attr.Value.Any().(type) {
204+
case []string:
205+
return attribute.StringSlice(key, v)
206+
case []int:
207+
return attribute.IntSlice(key, v)
208+
case []int64:
209+
return attribute.Int64Slice(key, v)
210+
case []float64:
211+
return attribute.Float64Slice(key, v)
212+
case []bool:
213+
return attribute.BoolSlice(key, v)
214+
default:
215+
return attribute.KeyValue{}
216+
}
217+
default:
218+
return attribute.KeyValue{}
219+
}
220+
221+
return attribute.KeyValue{}
222+
}

pkg/telemetry/tracer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import (
99
"go.opentelemetry.io/otel/attribute"
1010
"go.opentelemetry.io/otel/sdk/resource"
1111
"go.opentelemetry.io/otel/sdk/trace"
12-
"go.opentelemetry.io/otel/semconv/v1.10.0"
12+
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
1313

1414
"github.com/Permify/permify/internal"
1515
)
1616

1717
// NewTracer - Creates new tracer
18-
func NewTracer(exporter trace.SpanExporter) func(context.Context) error {
18+
func NewTracer(accountId string, exporter trace.SpanExporter) func(context.Context) error {
1919
hostName, err := os.Hostname()
2020
if err != nil {
2121
return func(context.Context) error { return nil }

0 commit comments

Comments
 (0)