Skip to content

Commit

Permalink
add handler
Browse files Browse the repository at this point in the history
  • Loading branch information
ktong committed Feb 25, 2024
1 parent 3fa0009 commit 731a053
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 124 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Add

- Add handler for correlation between log records and Open Telemetry spans (#13).
- Add gcp.WithCallers to retrieve stack trace from error (#14).

## [0.1.1] - 2024-02-23
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Sloth provides opinionated slog handlers for major Cloud providers. It providers
- [`sampling`](sampling) provides a slog handler for sampling records at request level.
- [`gcp`](gcp) provides a slog handler for emitting JSON logs to GCP Cloud Logging.
- [`rate`](rate) provides a slog handler for limiting records within the given rate.
- [`otel`](otel) provides a slog handler for correlation between log records and Open Telemetry spans.
22 changes: 20 additions & 2 deletions gcp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,27 @@ import (
"strings"
)

// Keys for [W3C Trace Context] attributes.
//
// [W3C Trace Context]: https://www.w3.org/TR/trace-context/#traceparent-header-field-values
const (
TraceKey = "trace-id"
SpanKey = "span-id"
// TraceKey is the key used by the [ID of the whole trace] forest and is used to uniquely
// identify a distributed trace through a system. It is represented as a 16-byte array,
// for example, 4bf92f3577b34da6a3ce929d0e0e4736.
// All bytes as zero (00000000000000000000000000000000) is considered an invalid value.
//
// [ID of the whole trace]: https://www.w3.org/TR/trace-context/#trace-id
TraceKey = "trace-id"
// SpanKey is the key used by the [ID of this request] as known by the caller.
// It is represented as an 8-byte array, for example, 00f067aa0ba902b7.
// All bytes as zero (0000000000000000) is considered an invalid value.
//
// [ID of this request]: https://www.w3.org/TR/trace-context/#parent-id
SpanKey = "span-id"
// TraceFlagsKey is the key used by an 8-bit field that controls [tracing flags]
// such as sampling, trace level, etc.
//
// [tracing flags]: https://www.w3.org/TR/trace-context/#trace-flags
TraceFlagsKey = "trace-flags"
)

Expand Down
2 changes: 1 addition & 1 deletion gcp/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func WithTrace(project string) Option {
//
// If it is nil, the handler finds trace information from record's attributes.
//
// [W3C Trace Context]: https://www.w3.org/TR/trace-context/#trace-id
// [W3C Trace Context]: https://www.w3.org/TR/trace-context/#traceparent-header-field-values
func WithTraceContext(provider func(context.Context) (traceID [16]byte, spanID [8]byte, traceFlags byte)) Option {
return func(options *options) {
options.contextProvider = provider
Expand Down
7 changes: 4 additions & 3 deletions otel/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module github.com/nil-go/sloth/otel

go 1.21

require go.opentelemetry.io/otel/trace v1.24.0

require go.opentelemetry.io/otel v1.24.0 // indirect
require (
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/trace v1.24.0
)
232 changes: 232 additions & 0 deletions otel/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,235 @@
// Use of this source code is governed by a MIT license found in the LICENSE file.

package otel

import (
"context"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"slices"
"strconv"
"time"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)

// Keys for [W3C Trace Context] attributes.
//
// [W3C Trace Context]: https://www.w3.org/TR/trace-context/#traceparent-header-field-values
const (
// TraceKey is the key used by the [ID of the whole trace] forest and is used to uniquely
// identify a distributed trace through a system. It is represented as a 16-byte array,
// for example, 4bf92f3577b34da6a3ce929d0e0e4736.
// All bytes as zero (00000000000000000000000000000000) is considered an invalid value.
//
// [ID of the whole trace]: https://www.w3.org/TR/trace-context/#trace-id
TraceKey = "trace-id"
// SpanKey is the key used by the [ID of this request] as known by the caller.
// It is represented as an 8-byte array, for example, 00f067aa0ba902b7.
// All bytes as zero (0000000000000000) is considered an invalid value.
//
// [ID of this request]: https://www.w3.org/TR/trace-context/#parent-id
SpanKey = "span-id"
// TraceFlagsKey is the key used by an 8-bit field that controls [tracing flags]
// such as sampling, trace level, etc.
//
// [tracing flags]: https://www.w3.org/TR/trace-context/#trace-flags
TraceFlagsKey = "trace-flags"
)

type Handler struct {
handler slog.Handler

recordEvent bool
passThrough bool

groups []group
eventHandler eventHandler
}

type group struct {
name string
attrs []slog.Attr
}

func New(handler slog.Handler, opts ...Option) Handler {
if handler == nil {
panic("cannot create Handler with nil handler")

Check warning on line 62 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L60-L62

Added lines #L60 - L62 were not covered by tests
}

option := &options{handler: handler, eventHandler: eventHandler{}}
for _, opt := range opts {
opt(option)

Check warning on line 67 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L65-L67

Added lines #L65 - L67 were not covered by tests
}

return Handler(*option)

Check warning on line 70 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L70

Added line #L70 was not covered by tests
}

func (h Handler) Enabled(ctx context.Context, level slog.Level) bool {
return h.handler.Enabled(ctx, level)

Check warning on line 74 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L73-L74

Added lines #L73 - L74 were not covered by tests
}

func (h Handler) Handle(ctx context.Context, record slog.Record) error {
var attrs []slog.Attr

Check warning on line 78 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L77-L78

Added lines #L77 - L78 were not covered by tests

if spanContext := trace.SpanContextFromContext(ctx); spanContext.IsValid() {
tid := spanContext.TraceID()
sid := spanContext.SpanID()
flags := spanContext.TraceFlags()
attrs = append(attrs,
slog.String(TraceKey, hex.EncodeToString(tid[:])),
slog.String(SpanKey, hex.EncodeToString(sid[:])),
slog.String(TraceFlagsKey, hex.EncodeToString([]byte{byte(flags)})),
)

Check warning on line 88 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L80-L88

Added lines #L80 - L88 were not covered by tests
}

if h.recordEvent && h.eventHandler.Enabled(ctx) {
h.eventHandler.Handle(ctx, record)
if h.passThrough {
return nil

Check warning on line 94 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L91-L94

Added lines #L91 - L94 were not covered by tests
}
}

// Have to add the attributes to the handler before adding the group.
// Otherwise, the attributes are added to the group.
handler := h.handler.WithAttrs(attrs)
for _, group := range h.groups {
handler = handler.WithGroup(group.name).WithAttrs(group.attrs)

Check warning on line 102 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L100-L102

Added lines #L100 - L102 were not covered by tests
}

return handler.Handle(ctx, record)

Check warning on line 105 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L105

Added line #L105 was not covered by tests
}

func (h Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
h.eventHandler = h.eventHandler.WithAttrs(attrs)

Check warning on line 109 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L108-L109

Added lines #L108 - L109 were not covered by tests

if len(h.groups) == 0 {
h.handler = h.handler.WithAttrs(attrs)

Check warning on line 112 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L111-L112

Added lines #L111 - L112 were not covered by tests

return h

Check warning on line 114 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L114

Added line #L114 was not covered by tests
}
h.groups = slices.Clone(h.groups)
h.groups[len(h.groups)-1].attrs = slices.Clone(h.groups[len(h.groups)-1].attrs)
h.groups[len(h.groups)-1].attrs = append(h.groups[len(h.groups)-1].attrs, attrs...)

Check warning on line 118 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L116-L118

Added lines #L116 - L118 were not covered by tests

return h

Check warning on line 120 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L120

Added line #L120 was not covered by tests
}

func (h Handler) WithGroup(name string) slog.Handler {
h.eventHandler = h.eventHandler.WithGroup(name)

Check warning on line 124 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L123-L124

Added lines #L123 - L124 were not covered by tests

h.groups = slices.Clone(h.groups)
h.groups = append(h.groups, group{name: name})

Check warning on line 127 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L126-L127

Added lines #L126 - L127 were not covered by tests

return h

Check warning on line 129 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L129

Added line #L129 was not covered by tests
}

type eventHandler struct {
prefix string
attrs []attribute.KeyValue
}

func (e eventHandler) Enabled(ctx context.Context) bool {
span := trace.SpanFromContext(ctx)

Check warning on line 138 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L137-L138

Added lines #L137 - L138 were not covered by tests

return span.IsRecording() && span.SpanContext().IsSampled()

Check warning on line 140 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L140

Added line #L140 was not covered by tests
}

func (e eventHandler) Handle(ctx context.Context, record slog.Record) {
span := trace.SpanFromContext(ctx)
switch {
case record.Level >= slog.LevelError:
span.SetStatus(codes.Error, record.Message)
var (
keys []string
err error
)
record.Attrs(
func(attr slog.Attr) bool {
if e, ok := attr.Value.Resolve().Any().(error); ok {
keys = append(keys, attr.Key)
err = errors.Join(err, e)

Check warning on line 156 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L143-L156

Added lines #L143 - L156 were not covered by tests
}

return true

Check warning on line 159 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L159

Added line #L159 was not covered by tests
},
)
if err != nil {
err = errors.New(record.Message) //nolint:goerr113
} else {
err = fmt.Errorf("%s: %w", record.Message, err)

Check warning on line 165 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L162-L165

Added lines #L162 - L165 were not covered by tests
}
span.RecordError(err, trace.WithTimestamp(record.Time), trace.WithAttributes(e.attrs...), trace.WithStackTrace(true))
default:
span.AddEvent(record.Message, trace.WithTimestamp(record.Time), trace.WithAttributes(e.attrs...))

Check warning on line 169 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L167-L169

Added lines #L167 - L169 were not covered by tests
}
}

func (e eventHandler) WithAttrs(attrs []slog.Attr) eventHandler {
e.attrs = slices.Clone(e.attrs)
for _, attr := range attrs {
e.attrs = append(e.attrs, convertAttr(attr, e.prefix)...)

Check warning on line 176 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L173-L176

Added lines #L173 - L176 were not covered by tests
}

return e

Check warning on line 179 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L179

Added line #L179 was not covered by tests
}

func (e eventHandler) WithGroup(name string) eventHandler {
e.prefix = e.prefix + name + "."

Check warning on line 183 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L182-L183

Added lines #L182 - L183 were not covered by tests

return e

Check warning on line 185 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L185

Added line #L185 was not covered by tests
}

func convertAttr(attr slog.Attr, prefix string) []attribute.KeyValue { //nolint:cyclop,funlen
key := prefix + attr.Key
value := attr.Value

Check warning on line 190 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L188-L190

Added lines #L188 - L190 were not covered by tests

attrs := make([]attribute.KeyValue, 0, 1)
switch value.Kind() {
case slog.KindAny:
switch val := value.Any().(type) {
case []string:
attrs[0] = attribute.StringSlice(key, val)
case []int:
attrs[0] = attribute.IntSlice(key, val)
case []int64:
attrs[0] = attribute.Int64Slice(key, val)
case []float64:
attrs[0] = attribute.Float64Slice(key, val)
case []bool:
attrs[0] = attribute.BoolSlice(key, val)
case fmt.Stringer:
attrs[0] = attribute.Stringer(key, val)
default:
attrs[0] = attribute.String(key, fmt.Sprintf("%v", val))

Check warning on line 209 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L192-L209

Added lines #L192 - L209 were not covered by tests
}
case slog.KindBool:
attrs[0] = attribute.Bool(key, value.Bool())
case slog.KindDuration:
attrs[0] = attribute.String(key, value.Duration().String())
case slog.KindFloat64:
attrs[0] = attribute.Float64(key, value.Float64())
case slog.KindInt64:
attrs[0] = attribute.Int64(key, value.Int64())
case slog.KindString:
attrs[0] = attribute.String(key, value.String())
case slog.KindTime:
attrs[0] = attribute.String(key, value.Time().Format(time.RFC3339Nano))
case slog.KindUint64:
attrs[0] = attribute.String(key, strconv.FormatUint(value.Uint64(), 10))
case slog.KindGroup:
attrs = slices.Grow(attrs, len(value.Group()))
for _, groupAttr := range value.Group() {
attrs = append(attrs, convertAttr(groupAttr, key+".")...)

Check warning on line 228 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L211-L228

Added lines #L211 - L228 were not covered by tests
}
case slog.KindLogValuer:
attr.Value = attr.Value.Resolve()
attrs = append(attrs, convertAttr(attr, prefix)...)

Check warning on line 232 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L230-L232

Added lines #L230 - L232 were not covered by tests
}

return attrs

Check warning on line 235 in otel/handler.go

View check run for this annotation

Codecov / codecov/patch

otel/handler.go#L235

Added line #L235 was not covered by tests
}
22 changes: 22 additions & 0 deletions otel/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2024 The sloth authors
// Use of this source code is governed by a MIT license found in the LICENSE file.

package otel

// WithRecordEvent enables recording log records as trace span's events.
// If passThrough is true, the log record will pass through to the next handler.
//
// If the level is less than slog.LevelError, the log record will be recorded as an event.
// Otherwise. the log record will be recorded as an exception event and set the status of span to Error.
func WithRecordEvent(passThrough bool) Option {
return func(options *options) {
options.recordEvent = true
options.passThrough = passThrough

Check warning on line 14 in otel/option.go

View check run for this annotation

Codecov / codecov/patch

otel/option.go#L11-L14

Added lines #L11 - L14 were not covered by tests
}
}

type (
// Option configures the Handler with specific options.
Option func(*options)
options Handler
)
39 changes: 0 additions & 39 deletions otel/trace.go

This file was deleted.

Loading

0 comments on commit 731a053

Please sign in to comment.