module/apmslog/handler.go (129 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package apmslog // import "go.elastic.co/apm/module/apmslog/v2" import ( "context" "errors" "fmt" "log/slog" "slices" "strings" "go.elastic.co/apm/v2" ) const ( // FieldKeyTraceID is the field key for the trace ID. FieldKeyTraceID = "trace.id" // FieldKeyTransactionID is the field key for the transaction ID. FieldKeyTransactionID = "transaction.id" // FieldKeySpanID is the field key for the span ID. FieldKeySpanID = "span.id" // SlogErrorKey* are the key name values that are reported as APM Errors SlogErrorKeyErr = "err" SlogErrorKeyError = "error" ) type ApmHandler struct { tracer *apm.Tracer reportLevels []slog.Level errorRecordAttrs []string handler slog.Handler } // Enabled reports whether the handler handles records at the given level. func (h *ApmHandler) Enabled(ctx context.Context, level slog.Level) bool { return h.handler.Enabled(ctx, level) } // WithAttrs returns a new ApmHandler with passed attributes attached. func (h *ApmHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &ApmHandler{h.tracer, h.reportLevels, h.errorRecordAttrs, h.handler.WithAttrs(attrs)} } // WithGroup returns a new ApmHandler with passed group attached. func (h *ApmHandler) WithGroup(name string) slog.Handler { return &ApmHandler{h.tracer, h.reportLevels, h.errorRecordAttrs, h.handler.WithGroup(name)} } func (h *ApmHandler) Handle(ctx context.Context, r slog.Record) error { // attempt to extract any available trace info from context var traceId apm.TraceID var transactionId apm.SpanID var parentId apm.SpanID if tx := apm.TransactionFromContext(ctx); tx != nil { traceId = tx.TraceContext().Trace transactionId = tx.TraceContext().Span parentId = tx.TraceContext().Span // add trace/transaction ids to slog record to be logged r.Add(FieldKeyTraceID, traceId) r.Add(FieldKeyTransactionID, transactionId) } if span := apm.SpanFromContext(ctx); span != nil { parentId = span.TraceContext().Span // add span id to slog record to be logged r.Add(FieldKeySpanID, parentId) } // report record as APM error if h.tracer != nil && h.tracer.Recording() && slices.Contains(h.reportLevels, r.Level) { // attempt to find error attributes // slog doesnt have a standard way of attaching an // error to a record, so attempting to grab any attribute // that has error/err keys OR keys user has defined as reportable // and extracting the values seems like a likely way to do it. errorsToAttach := []error{} r.Attrs(func(a slog.Attr) bool { if slices.Contains(h.errorRecordAttrs, a.Key) { var err error // first check if value is of error type to retain as much info as possible if v, ok := a.Value.Any().(error); ok { errorsToAttach = append(errorsToAttach, v) // else just convert reportable error value as string } else { errorsToAttach = append(errorsToAttach, errors.Join(err, fmt.Errorf("%s", a.Value.String()))) } } return true }) // If there are multiple reportable error attributes, create a new // apm.ErrorLogRecord for each. Otherwise just create one apm.ErrorLogRecord // with no Error. errLogRecords := []apm.ErrorLogRecord{} if len(errorsToAttach) == 0 { errRecord := apm.ErrorLogRecord{ Message: r.Message, Level: strings.ToLower(r.Level.String()), } errLogRecords = append(errLogRecords, errRecord) } else { for _, err := range errorsToAttach { errRecord := apm.ErrorLogRecord{ Message: r.Message, Level: strings.ToLower(r.Level.String()), Error: err, } errLogRecords = append(errLogRecords, errRecord) } } // for each errRecord, send to apm for _, errRecord := range errLogRecords { errlog := h.tracer.NewErrorLog(errRecord) errlog.Handled = true errlog.Timestamp = r.Time.UTC() errlog.SetStacktrace(2) // add available trace info if not zero type if traceId != (apm.TraceID{}) { errlog.TraceID = traceId } if transactionId != (apm.SpanID{}) { errlog.TransactionID = transactionId } if parentId != (apm.SpanID{}) { errlog.ParentID = parentId } // send error to APM errlog.Send() } } return h.handler.Handle(ctx, r) } type apmHandlerOption func(h *ApmHandler) // Create a new ApmHandler. func NewApmHandler(opts ...apmHandlerOption) *ApmHandler { h := &ApmHandler{ apm.DefaultTracer(), []slog.Level{slog.LevelError}, []string{SlogErrorKeyErr, SlogErrorKeyError}, slog.Default().Handler(), } for _, opt := range opts { opt(h) } return h } // Set slog handler for ApmHandler // default: slog.Default().Handler() func WithHandler(handler slog.Handler) apmHandlerOption { return func(h *ApmHandler) { h.handler = handler } } // Set which slog log level will be reported // default: slog.LevelError func WithReportLevel(lvls []slog.Level) apmHandlerOption { return func(h *ApmHandler) { h.reportLevels = lvls } } // Set with slog attribute keys will be used as errors. // default: 'error','err' func WithErrorRecordAttrs(keys []string) apmHandlerOption { return func(h *ApmHandler) { h.errorRecordAttrs = keys } } // Set custom tracer for ApmHandler. // default: apm.DefaultTracer() func WithTracer(tracer *apm.Tracer) apmHandlerOption { return func(h *ApmHandler) { h.tracer = tracer } }