strc

package
v0.0.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2025 License: MIT Imports: 16 Imported by: 0

README

strc

A simple tracing library. When OpenTelemetry is a bit too much. Features:

  • Simple code instrumentation.
  • Serialization into log/slog (structured logging library).
  • Handler with multiple sub-handlers for further processing.
  • Simple exporting slog handler for callback-based exporters.
  • Adding "trace_id" root field to all logs for easy correlation between logs and traces.

Instrumentation

Add the following code to function you want to trace:

span, ctx := strc.Start(ctx, "span name")
defer span.End()

Optionally, additional "save points" can be inserted:

span.Event("an event")

Result

All results are stored in log/slog records. Each span creates one record with group named span with the following data:

  • span.name: span name
  • span.id: span ID
  • span.parent: parent ID or 0000000 when there is no parent
  • span.trace: trace ID
  • span.event: event name (only on event)
  • span.at: duration within a span (only on event)
  • span.duration: trace duration (only when span ends)
  • span.time: log time (can be enabled in exporter)

Spans end up in log sink too, for better readability, the following fields are added to the root namespace:

  • msg: message in the form span XXX started or event XXX
  • trace_id - trace ID (disable by setting strc.TraceIDFieldKey to empty string)
  • build_id - build Git sha (disable by setting strc.BuildIDFieldKey to empty string)

Overriding time

Span start, event and end time is automatically taken via time.Now() call but there are some use cases when this needs to be overridden to a specific time. Use special attributes to do that:

span := strc.Start(ctx, "span name", "started", time.Now())
span.Event("an event", "at", time.Now())
span.End("finished", time.Now())

Propagation

A simple HTTP header-based propagation API is available. Note this is not meant to be used directly, there is HTTP middleware and client wrapper available:

// create new trace id
id := strc.NewTraceID()

// create a new context with trace id value
ctx := strc.WithContext(context.Background(), id)

// fetch the id from the context
id := strc.TraceIDFromContext(ctx)

// add the id from context to a HTTP request
strc.AddTraceIDHeader(ctx, request)

// fetch the id from a request
id := TraceIDFromRequest(request)

Middleware

The library provides native Echo middleware functions:

e.Use(strc.EchoRequestLogger(logger, strc.MiddlewareConfig{}))

Available Echo middleware in the preferred order of call:

  • EchoTraceExtractor extracts X-Strc-Trace-Id (and span) headers and stores them in the context. When no trace id is available, a random one is created.
  • EchoContextSetLogger: overrides the default Echo logger with per-request instance which captures context from the request. This means all logs created via Echo library will be forwarded into slog with values from context.
  • EchoHeadersExtractor extracts custom HTTP headers and stores them in the context. Can be appended to all logs via handler callback, useful for external correlation fields like request_id or edge_id.
  • EchoRequestLogger: creates a log record for every single HTTP request with configurable log level.

See strc.MiddlewareConfig for more info about configuration.

HTTP client

A TracingDoer type can be used to decorate HTTP clients adding necessary propagation automatically as long as tracing information is in the request context:

r, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://home.zapletalovi.com/", nil)
doer := strc.NewTracingDoer(http.DefaultClient)
doer.Do(r)

The TracingDoer can be optionally configured to log detailed debug information like request or reply HTTP headers or even full body. This is turned off by default, see strc.TracingDoerConfig for more info.

Example headers generated or parsed by HTTP client & middleware code:

X-Strc-Trace-ID: LOlIxiHprrrvHqD
X-Strc-Span-ID: VIPEcES.yuufaHI

Full example

package main

import (
	"context"
	"log/slog"

	"github.com/osbuild/logging/pkg/strc"
)

func subProcess(ctx context.Context) {
	span, ctx := strc.Start(ctx, "subProcess")
	defer span.End()

	span.Event("an event")
}

func process(ctx context.Context) {
	span, ctx := strc.Start(ctx, "process")
	defer span.End()

	subProcess(ctx)
}

func main() {
	span, ctx := strc.Start(context.Background(), "main")
	defer span.End()

	process(ctx)
}

Note the above example will print nothing as tracing is disabled by default, two things must be done. First, a slog destination logger must be set:

strc.SetLogger(slog.Default())

Second, the destination logger must have debug level handing enabled. The default logging level can be increased if needed:

strc.Level = slog.LevelInfo

Run the example with the following command:

go run github.com/osbuild/logging/internal/example_print/

Which prints something like (removed time and log level for readability):

span main started span.name=main span.id=pEnFDti span.parent=0000000 span.trace=SVyjVloJYogpNPq span.source=main.go:28
span process started span.name=process span.id=fRfWksO span.parent=pEnFDti span.trace=SVyjVloJYogpNPq span.source=main.go:18
span subProcess started span.name=subProcess span.id=gSouhiv span.parent=fRfWksO span.trace=SVyjVloJYogpNPq span.source=main.go:11
span subProcess event an event span.name=subProcess span.id=gSouhiv span.parent=fRfWksO span.trace=SVyjVloJYogpNPq span.event="an event" span.at=21.644µs span.source=main.go:14
span subProcess finished in 47.355µs span.name=subProcess span.id=gSouhiv span.parent=fRfWksO span.trace=SVyjVloJYogpNPq span.dur=47.355µs span.source=main.go:15
span process finished in 94.405µs span.name=process span.id=fRfWksO span.parent=pEnFDti span.trace=SVyjVloJYogpNPq span.dur=94.405µs span.source=main.go:22
span main finished in 285.246µs span.name=main span.id=pEnFDti span.parent=0000000 span.trace=SVyjVloJYogpNPq span.dur=285.246µs span.source=main.go:32

Exporting data

While the main goal of this library is just instrumenting and sending data into slog, a simple function callback exporter handler is provided by the package for quick collecting or exporting capabilities as well as multi-handler for chaining handlers which is useful to keep sending standard logging data to logging systems. It uses slog.Attr type as the data carrier:

package main

import (
	"context"
	"log/slog"
	"os"

	"github.com/osbuild/logging/pkg/strc"
)

func exportFunc(ctx context.Context, attrs []slog.Attr) {
	for _, attr := range attrs {
		println("exporting trace data", attr.Key, attr.Value.String())
	}
}

func main() {
	textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
	exportHandler := strc.NewExportHandler(exportFunc)
	multiHandler := strc.NewMultiHandler(textHandler, exportHandler)

	logger := slog.New(multiHandler)
	slog.SetDefault(logger)
	strc.SetLogger(logger)

	span, _ := strc.Start(context.Background(), "main")
	defer span.End()
}

There is additional NewMultiHandlerCustom which allows adding custom attributes from context via a callback function. This is useful when additional correlation id (e.g. background job UUID) needs to be added to every single regular log record. The multi-handler creates the following new keys in the root element:

  • trace_id - trace ID (disable by setting strc.TraceIDFieldKey to empty string)
  • build_id - build Git sha (disable by setting strc.BuildIDFieldKey to empty string)

Run the example with the following command:

go run github.com/osbuild/logging/internal/example_export/

For the best performance, we a dedicated exporting handler should be written customized to the output format. For more info, see writing an slog handler.

Documentation

Overview

A simple tracing library via log/slog.

Index

Constants

View Source
const (
	TraceHTTPHeaderName = "X-Strc-Trace-ID"
	SpanHTTPHeaderName  = "X-Strc-Span-ID"

	EmptyTraceID TraceID = TraceID("000000000000000")
	EmptySpanID  SpanID  = SpanID("0000000.0000000")
)

Variables

View Source
var (
	TraceIDKey = "trace_id"
	SpanIDKey  = "span_id"

	RequestBodyMaxSize  = 64 * 1024 // 64KB
	ResponseBodyMaxSize = 64 * 1024 // 64KB

	HiddenRequestHeaders = map[string]struct{}{
		"authorization": {},
		"cookie":        {},
		"set-cookie":    {},
		"x-auth-token":  {},
		"x-csrf-token":  {},
		"x-xsrf-token":  {},
	}
	HiddenResponseHeaders = map[string]struct{}{
		"set-cookie": {},
	}
)
View Source
var (
	// TraceIDFieldKey is the key used to store the trace ID in the log record by MultiHandler.
	// Set to empty string to disable this feature.
	TraceIDFieldKey = "trace_id"

	// BuildIDFieldKey is the key used to store the git commit in the log record by MultiHandler.
	// Set to empty string to disable this feature.
	BuildIDFieldKey = "build_id"
)

Level is the log level used for trace logging.

View Source
var ParentIDName string = "parent"

ParentIDName is the key name used for trace ID.

View Source
var SkipSource bool

SkipSource is a flag that disables source logging.

View Source
var SpanGroupName string = "span"

SpanGroupName is the group name used for span attributes.

View Source
var SpanIDName string = "id"

SpanIDName is the key name used for trace ID.

View Source
var TraceIDName string = "trace"

TraceIDName is the key name used for trace ID.

Functions

func AddSpanIDHeader

func AddSpanIDHeader(ctx context.Context, req *http.Request)

AddSpanIDHeader adds span ID from context to a request header. If span ID is not found in the context or if the request already has a span ID header, it does nothing.

func AddTraceIDHeader

func AddTraceIDHeader(ctx context.Context, req *http.Request)

AddTraceIDHeader adds trace ID from context to a request header. If trace ID is not found in the context or if the request already has a trace ID header, it does nothing.

func EchoContextSetLogger added in v0.0.8

func EchoContextSetLogger(logger *slog.Logger) echo.MiddlewareFunc

This sets the logger for each request to the specified logger. Anything processing the cecho.Context can just call echo.Context.Logger() to get the appropriate logger.

Meant to be chained after middlewares that add fields to the request context.

func EchoHeadersExtractor added in v0.0.8

func EchoHeadersExtractor(pairs []HeaderField) echo.MiddlewareFunc

EchoHeadersExtractor is a middleware that extracts values from headers and stores them in the context.

func EchoRequestLogger added in v0.0.8

func EchoRequestLogger(logger *slog.Logger, config MiddlewareConfig) echo.MiddlewareFunc

This generates exactly one log statement per request processed.

Meant to be chained after middlewares that add fields to the request context.

func EchoTraceExtractor added in v0.0.8

func EchoTraceExtractor() echo.MiddlewareFunc

EchoTraceExtractor extracts trace IDs and span IDs from HTTP headers and sets them in the request context.

Meant to be chained before any logging middleware.

func NewExportHandler

func NewExportHandler(export func(context.Context, []slog.Attr), opts ...ExporterOption) slog.Handler

NewExportHandler creates a new Exporter with the given callback function and options.

func RecoverPanicMiddleware

func RecoverPanicMiddleware(logger *slog.Logger) func(http.Handler) http.Handler

RecoverPanicMiddleware is a middleware that recovers from panics and logs them using slog as errors with status code 500. No body is returned.

func SetLogger

func SetLogger(logger *slog.Logger)

SetLogger sets the logger for the package.

func SetNoopLogger

func SetNoopLogger()

SetNoopLogger sets a no-op logger for the package.

func UniqAttrs

func UniqAttrs(attrs []slog.Attr) []slog.Attr

func WithSpan added in v0.0.4

func WithSpan(ctx context.Context, span *Span) context.Context

Start returns a new context with span.

func WithSpanID

func WithSpanID(ctx context.Context, spanID SpanID) context.Context

Start returns a new context with span ID.

func WithTraceID

func WithTraceID(ctx context.Context, traceID TraceID) context.Context

Start returns a new context with trace ID.

Types

type DoerErr

type DoerErr struct {
	Err error
}

DoerErr is a simple wrapped error without any message. Additional message would stack for each request as multiple doers are called leading to:

"error in doer1: error in doer2: error in doer3: something happened"

func NewDoerErr

func NewDoerErr(err error) *DoerErr

func (*DoerErr) Error

func (e *DoerErr) Error() string

func (*DoerErr) Unwrap

func (e *DoerErr) Unwrap() error

type ExportHandler

type ExportHandler struct {
	// contains filtered or unexported fields
}

ExportHandler is an slog.Handler which provides a callback function that allows for exporting attributes (slog.Attr) to a different system. This handler is not optimized for performance, it is recommended to write a dedicated slog.Handler. For more information on the topic, see https://github.com/golang/example/blob/master/slog-handler-guide/README.md

func (*ExportHandler) Enabled

func (h *ExportHandler) Enabled(ctx context.Context, level slog.Level) bool

func (*ExportHandler) Handle

func (h *ExportHandler) Handle(ctx context.Context, r slog.Record) error

func (*ExportHandler) WithAttrs

func (h *ExportHandler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*ExportHandler) WithGroup

func (h *ExportHandler) WithGroup(name string) slog.Handler

type ExporterOption

type ExporterOption func(*ExportHandler)

func IncludeTime

func IncludeTime() ExporterOption

IncludeTime is an ExporterOption that includes the time in the exported attributes.

type HeaderField added in v0.0.8

type HeaderField struct {
	HeaderName string
	FieldName  string
}

HeaderField is a pair of header name and field name.

type HttpRequestDoer

type HttpRequestDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

type MiddlewareConfig

type MiddlewareConfig struct {
	// DefaultLevel is the default log level for requests. Defaults to Info.
	DefaultLevel slog.Level

	// ClientErrorLevel is the log level for requests with client errors (4xx). Defaults to Warn.
	ClientErrorLevel slog.Level

	// ServerErrorLevel is the log level for requests with server errors (5xx). Defaults to Error.
	ServerErrorLevel slog.Level
}

type MultiCallback

type MultiCallback func(context.Context, []slog.Attr) ([]slog.Attr, error)

func HeadersCallback added in v0.0.8

func HeadersCallback(pairs []HeaderField) MultiCallback

HeadersCallback is a slog callback that extracts values from the context and adds them to the attributes.

type MultiHandler

type MultiHandler struct {
	// contains filtered or unexported fields
}

MultiHandler distributes records to multiple slog.Handler

func NewMultiHandler

func NewMultiHandler(handlers ...slog.Handler) *MultiHandler

NewMultiHandler distributes records to multiple slog.Handler

func NewMultiHandlerCustom

func NewMultiHandlerCustom(attrs []slog.Attr, callback MultiCallback, handlers ...slog.Handler) *MultiHandler

NewMultiHandlerCustom distributes records to multiple slog.Handler with custom attributes and callback. Pass static slice of attributes added to the every record, and a callback that can add dynamic attributes from the context. No custom fields are added to the "span" group.

func (*MultiHandler) Enabled

func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool

func (*MultiHandler) Handle

func (h *MultiHandler) Handle(ctx context.Context, recOrig slog.Record) error

func (*MultiHandler) WithAttrs

func (h *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*MultiHandler) WithGroup

func (h *MultiHandler) WithGroup(name string) slog.Handler

type NoopHandler

type NoopHandler struct {
}

NoopHandler does nothing.

func (*NoopHandler) Enabled

func (h *NoopHandler) Enabled(ctx context.Context, level slog.Level) bool

func (*NoopHandler) Handle

func (h *NoopHandler) Handle(ctx context.Context, r slog.Record) error

func (*NoopHandler) WithAttrs

func (h *NoopHandler) WithAttrs(attrs []slog.Attr) slog.Handler

func (*NoopHandler) WithGroup

func (h *NoopHandler) WithGroup(name string) slog.Handler

type Span

type Span struct {
	// contains filtered or unexported fields
}

Span represents a span of a trace. It is used to log events and end the span. It is a lightweight object and can be passed around in contexts.

func SpanFromContext added in v0.0.4

func SpanFromContext(ctx context.Context) *Span

Start returns span from a context or nil if it was not found.

func Start

func Start(ctx context.Context, name string, args ...any) (*Span, context.Context)

Start starts a new span with the given name and optional arguments. All arguments are present in subsequent Event and End calls. Must call End to finish the span.

span, ctx := strc.Start(ctx, "calculating something big")
defer span.End()

Avoid adding complex arguments to spans as they are added to the context.

It immediately logs a message with the span name, span information in SpanGroupName and optional arguments.

Special argument named "started" of type time.Time can be used to set the start time of the span.

func (*Span) End

func (s *Span) End(args ...any)

End finishes the span and logs the duration of the span. Optional arguments can be provided to log additional information.

It immediately logs a message with the span name, span information in SpanGroupName and optional arguments.

Special argument named "finished" of type time.Time can be used to set the finish time of the span.

func (*Span) Event

func (s *Span) Event(name string, args ...any)

Event logs a new event in the span with the given name and optional arguments.

It immediately logs a message with the span name, span information in SpanGroupName and optional arguments.

Special argument named "at" of type time.Time can be used to set the event time.

func (*Span) TraceID added in v0.0.8

func (s *Span) TraceID() TraceID

TraceID returns the trace ID of the span.

type SpanID

type SpanID string

SpanID is a unique identifier for a trace.

func NewSpanID

func NewSpanID(ctx context.Context) SpanID

NewSpanID generates a new span ID. Uses context to fetch its parent span ID.

func SpanIDFromContext

func SpanIDFromContext(ctx context.Context) SpanID

Start returns span ID from a context. It returns EmptySpanID if span ID is not found. Use NewSpanID to generate a new span ID.

func SpanIDFromRequest

func SpanIDFromRequest(req *http.Request) SpanID

SpanIDFromRequest returns span ID from a request. If span ID is not found, it returns EmptySpanID.

func (SpanID) ID

func (s SpanID) ID() string

func (SpanID) ParentID

func (s SpanID) ParentID() string

func (SpanID) String

func (s SpanID) String() string

type TraceID

type TraceID string

TraceID is a unique identifier for a trace.

func NewTraceID

func NewTraceID() TraceID

NewTraceID generates a new random trace ID.

func TraceIDFromContext

func TraceIDFromContext(ctx context.Context) TraceID

Start returns trace ID from a context. It returns EmptyTraceID if trace ID is not found. Use NewTraceID to generate a new trace ID.

func TraceIDFromRequest

func TraceIDFromRequest(req *http.Request) TraceID

TraceIDFromRequest returns trace ID from a request. If trace ID is not found, it returns EmptyTraceID.

func (TraceID) String

func (t TraceID) String() string

type Tracer added in v0.0.4

type Tracer struct {
	// contains filtered or unexported fields
}

Tracer is a wrapper for slog.Logger which logs into the initialized slog. Use strc.Start and End package functions to use slog.Default() logger.

func NewTracer added in v0.0.4

func NewTracer(logger *slog.Logger) *Tracer

NewTracer creates a new Tracer with the given logger. Use strc.Start and End package functions to use slog.Default() logger.

func (*Tracer) Start added in v0.0.4

func (t *Tracer) Start(ctx context.Context, name string, args ...any) (*Span, context.Context)

Start starts a new span, see strc.Start for more information.

type TracingDoer

type TracingDoer struct {
	// contains filtered or unexported fields
}

TracingDoer is a http client doer that adds tracing to the request and response.

func NewTracingDoer

func NewTracingDoer(doer HttpRequestDoer) *TracingDoer

NewTracingDoer returns a new TracingDoer.

func NewTracingDoerWithConfig

func NewTracingDoerWithConfig(doer HttpRequestDoer, config TracingDoerConfig) *TracingDoer

func (*TracingDoer) Do

func (td *TracingDoer) Do(req *http.Request) (*http.Response, error)

type TracingDoerConfig

type TracingDoerConfig struct {
	WithRequestBody     bool
	WithResponseBody    bool
	WithRequestHeaders  bool
	WithResponseHeaders bool
}

Directories

Path Synopsis
Simple tracing graph processor
Simple tracing graph processor

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL