...

Source file src/cdr.dev/slog/sloggers/slogstackdriver/slogstackdriver.go

Documentation: cdr.dev/slog/sloggers/slogstackdriver

     1  // Package slogstackdriver contains the slogger for google cloud's stackdriver.
     2  package slogstackdriver // import "cdr.dev/slog/sloggers/slogstackdriver"
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  
    11  	"cloud.google.com/go/compute/metadata"
    12  	"go.opencensus.io/trace"
    13  	logpbtype "google.golang.org/genproto/googleapis/logging/type"
    14  	logpb "google.golang.org/genproto/googleapis/logging/v2"
    15  
    16  	"cdr.dev/slog"
    17  	"cdr.dev/slog/internal/syncwriter"
    18  )
    19  
    20  // Sink creates a slog.Sink configured to write JSON logs
    21  // to stdout for stackdriver.
    22  //
    23  // See https://cloud.google.com/logging/docs/agent
    24  func Sink(w io.Writer) slog.Sink {
    25  	projectID, _ := metadata.ProjectID()
    26  
    27  	return stackdriverSink{
    28  		projectID: projectID,
    29  		w:         syncwriter.New(w),
    30  	}
    31  }
    32  
    33  type stackdriverSink struct {
    34  	projectID string
    35  	w         *syncwriter.Writer
    36  }
    37  
    38  func (s stackdriverSink) LogEntry(ctx context.Context, ent slog.SinkEntry) {
    39  	// Note that these documents are inconsistent, so we only use the special
    40  	// keys described by both.
    41  	// https://cloud.google.com/logging/docs/agent/configuration#special-fields
    42  	// https://cloud.google.com/stackdriver/docs/solutions/agents/ops-agent/configuration#special-fields
    43  	e := slog.M(
    44  		slog.F("logging.googleapis.com/severity", sev(ent.Level)),
    45  		slog.F("message", ent.Message),
    46  		// Unfortunately, both of these fields are required.
    47  		slog.F("timestampSeconds", ent.Time.Unix()),
    48  		slog.F("timestampNanos", ent.Time.UnixNano()%1e9),
    49  		slog.F("logging.googleapis.com/sourceLocation", &logpb.LogEntrySourceLocation{
    50  			File:     ent.File,
    51  			Line:     int64(ent.Line),
    52  			Function: ent.Func,
    53  		}),
    54  	)
    55  
    56  	if len(ent.LoggerNames) > 0 {
    57  		e = append(e, slog.F("logging.googleapis.com/operation", &logpb.LogEntryOperation{
    58  			Producer: strings.Join(ent.LoggerNames, "."),
    59  		}))
    60  	}
    61  
    62  	if ent.SpanContext != (trace.SpanContext{}) {
    63  		e = append(e,
    64  			slog.F("logging.googleapis.com/trace", s.traceField(ent.SpanContext.TraceID)),
    65  			slog.F("logging.googleapis.com/spanId", ent.SpanContext.SpanID.String()),
    66  			slog.F("logging.googleapis.com/trace_sampled", ent.SpanContext.IsSampled()),
    67  		)
    68  	}
    69  
    70  	e = append(e, ent.Fields...)
    71  
    72  	buf, _ := json.Marshal(e)
    73  
    74  	buf = append(buf, '\n')
    75  	s.w.Write("slogstackdriver", buf)
    76  }
    77  
    78  func (s stackdriverSink) Sync() {
    79  	s.w.Sync("stackdriverSink")
    80  }
    81  
    82  func sev(level slog.Level) logpbtype.LogSeverity {
    83  	switch level {
    84  	case slog.LevelDebug:
    85  		return logpbtype.LogSeverity_DEBUG
    86  	case slog.LevelInfo:
    87  		return logpbtype.LogSeverity_INFO
    88  	case slog.LevelWarn:
    89  		return logpbtype.LogSeverity_WARNING
    90  	case slog.LevelError:
    91  		return logpbtype.LogSeverity_ERROR
    92  	default:
    93  		return logpbtype.LogSeverity_CRITICAL
    94  	}
    95  }
    96  
    97  func (s stackdriverSink) traceField(tID trace.TraceID) string {
    98  	return fmt.Sprintf("projects/%v/traces/%v", s.projectID, tID)
    99  }
   100  

View as plain text