Skip to content

maguro/gslog

Repository files navigation

gslog

Go Version Documentation Go Report Card codecov License

A Google Cloud Logging Handler implementation for slog.


This Google Cloud Logging (GCL) slog.Handler implementation directly fills the GCL entry, logging.Entry, with information obtained in the context.Context, slog.Record, and implied context within the Handler itself. The logging.Entry.Payload is filled with a Protobuf structpb.Struct instance, resulting in a jsonPayload with the log message having the key "message". Log records are sent asynchronously. Critical level, or higher, log records will be sent synchronously.

The GCL Handler's options include a number of ways to include information from "outside" frameworks:

  • Labels attached to the context, via gslog.WithLabels(ctx, ...labels), which are added to the GCL entry, logging.Entry, Labels field.
  • OpenTelemetry baggage attached to the context which are added as attributes, slog.Attr, to the logging record, slog.Record. The baggage keys are prefixed with "otel-baggage/" to mitigate collision with other log attributes.
  • OpenTelemetry tracing attached to the context which are added directly to the GCL entry, logging.Entry, tracing fields.
  • Labels from the Kubernetes Downward API podinfo labels file, which are added to the GCL entry, logging.Entry, Labels field. The labels are prefixed with "k8s-pod/" to adhere to the GCL conventions for Kubernetes Pod labels.

Install

go get m4o.io/gslog

Compatibility: go >= 1.21

Example Usage

First create a Google Cloud Logging logging.Client to use throughout your application:

ctx := context.Background()
client, err := logging.NewClient(ctx, "my-project")
if err != nil {
	// TODO: Handle error.
}

Usually, you'll want to add log entries to a buffer to be periodically flushed (automatically and asynchronously) to the Cloud Logging service. Use the logger when creating the new gslog.GcpHandler which is passed to slog.New() to obtain a slog-based logger.

loggger := client.Logger("my-log")

h := gslog.NewGcpHandler(loggger)
l := slog.New(h)

l.Info("How now brown cow?")

Writing critical, or higher, log level entries will be sent synchronously.

l.Log(context.Background(), gslog.LevelCritical, "Danger, Will Robinson!")

Close your client before your program exits, to flush any buffered log entries.

err = client.Close()
if err != nil {
   // TODO: Handle error.
}

Logger Configuration Options

Creating a Google Cloud Logging Handler using gslog.NewGcpHandler(logger, ...options) accepts the following options:

Configuration option Arguments Description
gslog.WithLogLeveler(leveler) slog.Leveler Specifies the slog.Leveler for logging. Explicitly setting the log level here takes precedence over the other options.
gslog.WithLogLevelFromEnvVar(envVar) string Specifies the log level for logging comes from tne environmental variable specified by the key.
gslog.WithDefaultLogLeveler() slog.Leveler Specifies the default slog.Leveler for logging.
gslog.WithSourceAdded() Causes the handler to compute the source code position of the log statement and add a slog.SourceKey attribute to the output.
gslog.WithLabels() Adds any labels found in the context to the logging.Entry's Labels field.
gslog.WithReplaceAttr(mapper) gslog.Mapper Specifies an attribute mapper used to rewrite each non-group attribute before it is logged.
otel.WithOtelBaggage() Directs that the slog.Handler to include OpenTelemetry baggage. The baggage.Baggage is obtained from the context, if available, and added as attributes.
otel.WithOtelTracing() Directs that the slog.Handler to include OpenTelemetry tracing. Tracing information is obtained from the trace.SpanContext stored in the context, if provided.
k8s.WithPodinfoLabels(root) string Directs that the slog.Handler to include labels from the Kubernetes Downward API podinfo labels file. The labels file is expected to be found in the directory specified by root and MUST be named "labels", per the Kubernetes Downward API for Pods.

Design Notes

There's a number of different ways to map the slog.Record to a GCL entry, logging.Entry.

  • a JSON string
  • a value that can be marshaled to a JSON object, like a map[string]interface{} or a struct
  • a Protobuf *anypb.Any

The pros and cons are

Payload type pros cons
JSON string fast and efficient to generate on the slog side logged as a flat unstructured textPayload in GCL
value that can be marshaled to a JSON object logged as a structured jsonPayload in GCL the marshalling effort is complicated and not amortized
Protobuf *anypb.Any not known at the moment not known at the moment

Even though a JSON string can be marshaled to a JSON object, the GCL client merely looks at its Go type and decides to treat it as a flat text message.

If not a string, values that can be marshaled to a JSON object are actually first marshalled into a JSON object, i.e. map[string]interface{}, and then that resulting JSON object is translated into an equivalent structpb.Struct Protobuf message. The GCL logger will redo this marshalling and translating for every message logged.

The reason why the logging.Entry Payload field is set with a Protobuf structpb.Struct when the end result is a jsonPayload GCL logging entry is because that's what Logger.Log(e) does anyway, behind the scenes. When either

About

An slog Handler for Google Cloud Logging

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages