slogcpadapter

package module
v1.0.7 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

README

slogcp-grpc-adapter

An adapter that lets the slogcp structured logging handler plug directly into the go-grpc-middleware logging interceptors.

slogcp-grpc-adapter implements the logging.Logger interface from github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging using a *slog.Logger backed by slogcp. This lets you:

  • Keep using go-grpc-middleware's logging interceptors for unary and streaming RPCs (on both client and server).
  • Emit JSON logs shaped for Google Cloud Logging / Error Reporting / Cloud Trace via slogcp.
  • Preserve request-scoped loggers, attributes, and trace context coming from context.Context.

It lives in its own module so that github.com/grpc-ecosystem/go-grpc-middleware is not a dependency of slogcp itself. Projects that want this integration can opt in to the adapter without affecting the core slogcp module graph.

Installation

go get github.com/pjscruggs/slogcp-grpc-adapter

You will usually also want:

  • github.com/pjscruggs/slogcp – the Google Cloud friendly slog.Handler.
  • github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging – the gRPC logging interceptors this adapter plugs into.

Why Would I Use This?

Who is this for?

This adapter is for you if:

  1. You are already using go-grpc-middleware for gRPC logging.
    Your services rely on the interceptors/logging package to log method names, status codes, latencies, payload sizes, and peer info for unary and streaming RPCs.

  2. You want those logs to be Google Cloud native.
    You want Cloud Logging to see severity instead of level, you want trace IDs in logging.googleapis.com/trace / spanId / trace_sampled, and you want Error Reporting to group RPC failures based on stack traces and serviceContext.

  3. You prefer stdout JSON over direct Cloud Logging clients.
    Like slogcp, this adapter keeps logging as JSON to stdout or to whatever writer you configure, letting Google Cloud's logging ingester do the heavy lifting. You keep the ergonomic slog APIs plus go-grpc-middleware's interceptor wiring.

How does this fit with slogcp and go-grpc-middleware?
  • slogcp gives you a slog.Handler that understands Google Cloud's logging, trace, and error-reporting conventions, plus HTTP and gRPC integrations of its own (slogcpgrpc).
  • go-grpc-middleware gives you generic, pluggable gRPC interceptors for logging, metrics, retries, auth, validation, and more.
  • slogcp-grpc-adapter is a thin bridge between the two: it implements go-grpc-middleware's logging.Logger using a *slog.Logger built on slogcp, so your existing interceptor chains can emit Cloud Logging–friendly JSON without rewriting your middleware configuration.

Use it when you want to keep the go-grpc-middleware logging story (including its context field injection and per-RPC metadata) while standardizing on slogcp as your logging backend for Google Cloud.

Features

Adapts go-grpc-middleware logging to slogcp

The core type in this module is *slogcpadapter.Logger, which:

  • Implements logging.Logger from github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging.
  • Forwards calls to a *slog.Logger backed by a *slogcp.Handler.
  • Preserves the context.Context passed in by go-grpc-middleware so slogcp can attach trace correlation fields (logging.googleapis.com/trace, etc.) and Error Reporting metadata.

The adapter converts the variadic key/value pairs used by go-grpc-middleware into structured slog.Attr values:

  • Keys are coerced to strings using fmt.Sprint.
  • Values are wrapped as slog.Any so they serialize naturally into JSON.
Severity mapping and GCP levels

go-grpc-middleware defines its own Level enum using the same numeric scheme as log/slog (for example, LevelDebug = -4, LevelInfo = 0, LevelWarn = 4, LevelError = 8). The adapter's default mapping simply converts logging.Level into slog.Level:

  • With the default go-grpc-middleware WithLevels configuration, LevelDebug/LevelInfo/LevelWarn/LevelError map 1:1 onto slog and therefore to the expected Cloud Logging severities via slogcp.
  • If you customize logging.WithLevels to return intermediate integer levels (for example values matching slogcp.LevelNotice, slogcp.LevelCritical, slogcp.LevelAlert, slogcp.LevelEmergency, or slogcp.LevelDefault), those numeric levels are preserved and flow through to slogcp, unlocking the full range of GCP severities for gRPC logs.

If you need completely different semantics, you can still supply your own mapping function via WithLevelMapper (see Customization below).

Works with your existing slog defaults

NewLogger supports a few construction paths so you can fit it into your existing logging setup:

  • Provide a *slogcp.Handler.
    The adapter builds a fresh *slog.Logger around the handler, giving you a dedicated logger for gRPC interceptors.

  • Provide a *slog.Logger.
    Use WithLogger to reuse an existing logger (for example, your app's default logger) so gRPC logs share the same configuration, attributes, and output destination.

  • Let it fall back to slog.Default().
    If you don’t pass a handler or logger, the adapter uses the process-wide default *slog.Logger, which is often already configured with a slogcp handler in slogcp-based services.

Drop-in interceptor helpers

To make wiring easy, the package exposes helpers that mirror go-grpc-middleware's logging interceptors but pre-wired with a slogcp handler:

  • UnaryServerInterceptor(handler *slogcp.Handler, opts ...logging.Option) grpc.UnaryServerInterceptor.
  • StreamServerInterceptor(handler *slogcp.Handler, opts ...logging.Option) grpc.StreamServerInterceptor.
  • UnaryClientInterceptor(handler *slogcp.Handler, opts ...logging.Option) grpc.UnaryClientInterceptor.
  • StreamClientInterceptor(handler *slogcp.Handler, opts ...logging.Option) grpc.StreamClientInterceptor.

These simply construct a *slogcpadapter.Logger for you and pass it into the corresponding go-grpc-middleware interceptors. You can still use all of go-grpc-middleware's logging options (for example, WithFieldsFromContext) to control what gets logged per RPC.

Quick Start

The examples below show how to wire slogcp, this adapter, and go-grpc-middleware's interceptors together. They intentionally focus on the logging pieces; for full observability (tracing, metrics) you will typically also add OpenTelemetry otelgrpc and other interceptors from the grpc-ecosystem project.

Server: basic logging with slogcp
package main

import (
	"log"
	"log/slog"
	"net"
	"os"

	grpc_logging "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
	"github.com/pjscruggs/slogcp"
	"github.com/pjscruggs/slogcp-grpc-adapter"
	"google.golang.org/grpc"
)

func main() {
	handler, err := slogcp.NewHandler(os.Stdout)
	if err != nil {
		log.Fatalf("configure slogcp: %v", err)
	}
	logger := slog.New(handler)
	slog.SetDefault(logger)

	adapted := slogcpadapter.NewLogger(handler)

	grpcServer := grpc.NewServer(
		grpc.ChainUnaryInterceptor(
			grpc_logging.UnaryServerInterceptor(adapted),
		),
		grpc.ChainStreamInterceptor(
			grpc_logging.StreamServerInterceptor(adapted),
		),
	)

	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("listen: %v", err)
	}
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("serve: %v", err)
	}
}

If you prefer, you can skip NewLogger and use the convenience helpers:

grpcServer := grpc.NewServer(
	grpc.ChainUnaryInterceptor(
		slogcpadapter.UnaryServerInterceptor(handler),
	),
	grpc.ChainStreamInterceptor(
		slogcpadapter.StreamServerInterceptor(handler),
	),
)

Either way, go-grpc-middleware will call into the adapter for each RPC, and the resulting logs will be emitted through slogcp's handler with Google Cloud–friendly JSON and trace correlation.

Client: logging outbound RPCs

On the client side, you can reuse the same handler (or logger) to observe outbound RPCs:

conn, err := grpc.NewClient(
	addr,
	grpc.WithTransportCredentials(insecure.NewCredentials()), // use real TLS creds in production
	grpc.WithChainUnaryInterceptor(slogcpadapter.UnaryClientInterceptor(handler)),
	grpc.WithChainStreamInterceptor(slogcpadapter.StreamClientInterceptor(handler)),
)
if err != nil {
	log.Fatalf("dial: %v", err)
}
defer conn.Close()

client := myservicepb.NewMyServiceClient(conn)
_ = client

Because the adapter always receives a context.Context, logs for outbound RPCs can still participate in OpenTelemetry tracing and Cloud Logging trace correlation, as long as there is an active span on the context and slogcp is configured normally.

Customization

Reusing an existing slog logger

If you already have a configured *slog.Logger (for example, with global attributes, source locations, or async wrappers), you can tell the adapter to use it directly:

base := slog.New(handler)
adapted := slogcpadapter.NewLogger(nil, slogcpadapter.WithLogger(base))

grpcServer := grpc.NewServer(
	grpc.ChainUnaryInterceptor(
		grpc_logging.UnaryServerInterceptor(adapted),
	),
)

In this mode, the handler argument to NewLogger is optional; the adapter will prefer the provided logger and only fall back to building a logger from the handler when no logger is supplied.

Custom level mapping

By default, the adapter passes logging.Level through to slog.Level, which works well with slogcp's extended severity levels when you customize logging.WithLevels. If you need finer control over how go-grpc-middleware's logging levels map onto slog (and thus Cloud Logging severities), provide a custom mapper:

mapper := func(level grpc_logging.Level) slog.Level {
	switch level {
	case grpc_logging.LevelDebug:
		return slog.LevelDebug
	case grpc_logging.LevelInfo:
		return slog.LevelInfo
	case grpc_logging.LevelWarn, grpc_logging.LevelError:
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}

adapted := slogcpadapter.NewLogger(handler, slogcpadapter.WithLevelMapper(mapper))
_ = adapted

You can also combine this with a custom logging.WithLevels configuration that returns values matching slogcp.LevelNotice, slogcp.LevelCritical, slogcp.LevelAlert, slogcp.LevelEmergency, or slogcp.LevelDefault to take full advantage of GCP's severity range for gRPC logs.

How This Plays With slogcp's Native gRPC Integration

The main slogcp repository already offers its own gRPC helpers in the slogcpgrpc package, which provide interceptors wired with OpenTelemetry stats handlers and trace propagation.

This adapter does not replace slogcpgrpc; instead it gives you another integration point:

  • Use slogcpgrpc when you want a single, opinionated package that handles slogcp logging, trace correlation, and OpenTelemetry wiring for gRPC.
  • Use slogcp-grpc-adapter when you already rely on the broader go-grpc-middleware ecosystem (auth, retry, selector, prometheus providers, etc.) and you simply want its logging interceptors to emit slogcp/Cloud Logging–compatible JSON.

In more complex setups you can mix both: for example, use slogcpgrpc for servers that you fully control, but use slogcp-grpc-adapter in services where go-grpc-middleware logging interceptors are already deeply embedded or where you benefit from its logging.WithFieldsFromContext ecosystem.

License

Apache 2.0

Contributing

Contributions are welcome! Feel free to submit issues for bugs or feature requests. For code contributions, please fork the repository, create a feature branch, and submit a pull request with your changes.

Documentation

Overview

Package slogcpadapter wires github.com/pjscruggs/slogcp into github.com/grpc-ecosystem/go-grpc-middleware/v2 logging interceptors. It implements logging.Logger by forwarding records to a slog.Logger backed by a slogcp handler, stringifying key/value pairs from the middleware into slog.Attr values and preserving the gRPC context for trace propagation. When no handler or logger is supplied, the adapter falls back to slog.Default so existing slogcp defaults still apply.

The helpers UnaryServerInterceptor, StreamServerInterceptor, UnaryClientInterceptor and StreamClientInterceptor wrap the middleware logging interceptors, keeping the same options surface (for example grpc_logging.WithFieldsFromContext or grpc_logging.WithLevels) while avoiding boilerplate.

Quick start:

handler, _ := slogcp.NewHandler(os.Stdout)

server := grpc.NewServer(
	grpc.ChainUnaryInterceptor(slogcpadapter.UnaryServerInterceptor(handler)),
	grpc.ChainStreamInterceptor(slogcpadapter.StreamServerInterceptor(handler)),
)

conn, _ := grpc.NewClient(
	addr,
	grpc.WithTransportCredentials(insecure.NewCredentials()),
	grpc.WithChainUnaryInterceptor(slogcpadapter.UnaryClientInterceptor(handler)),
	grpc.WithChainStreamInterceptor(slogcpadapter.StreamClientInterceptor(handler)),
)

Customization hooks WithLogger and WithLevelMapper let you reuse an existing slog.Logger (for example one shared across components) and adjust how grpc_logging.Level values map to slog.Level so slogcp severity tuning carries through to gRPC logs.

Index

Constants

View Source
const Version = "v1.0.7"

Version is the current version of the slogcp gRPC adapter. It follows semantic versioning (https://semver.org/).

Variables

This section is empty.

Functions

func StreamClientInterceptor

func StreamClientInterceptor(handler *slogcp.Handler, opts ...grpc_logging.Option) grpc.StreamClientInterceptor

StreamClientInterceptor returns a stream client interceptor that logs through slogcp.

Example:

handler, _ := slogcp.NewHandler(os.Stdout)
conn, _ := grpc.NewClient(
	addr,
	grpc.WithTransportCredentials(insecure.NewCredentials()),
	grpc.WithChainStreamInterceptor(slogcpadapter.StreamClientInterceptor(handler)),
)

func StreamServerInterceptor

func StreamServerInterceptor(handler *slogcp.Handler, opts ...grpc_logging.Option) grpc.StreamServerInterceptor

StreamServerInterceptor returns a stream server interceptor that logs through slogcp.

Example:

handler, _ := slogcp.NewHandler(os.Stdout)
server := grpc.NewServer(
	grpc.ChainStreamInterceptor(slogcpadapter.StreamServerInterceptor(handler)),
)

func UnaryClientInterceptor

func UnaryClientInterceptor(handler *slogcp.Handler, opts ...grpc_logging.Option) grpc.UnaryClientInterceptor

UnaryClientInterceptor returns a unary client interceptor that logs through slogcp.

Example:

handler, _ := slogcp.NewHandler(os.Stdout)
conn, _ := grpc.NewClient(
	addr,
	grpc.WithTransportCredentials(insecure.NewCredentials()),
	grpc.WithChainUnaryInterceptor(slogcpadapter.UnaryClientInterceptor(handler)),
)

func UnaryServerInterceptor

func UnaryServerInterceptor(handler *slogcp.Handler, opts ...grpc_logging.Option) grpc.UnaryServerInterceptor

UnaryServerInterceptor returns a unary server interceptor that logs through slogcp.

Example:

handler, _ := slogcp.NewHandler(os.Stdout)
server := grpc.NewServer(
	grpc.ChainUnaryInterceptor(slogcpadapter.UnaryServerInterceptor(handler)),
)

Types

type Logger

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

Logger adapts go-grpc-middleware logging calls to a slog.Logger. The underlying logger is usually backed by a slogcp.Handler.

func NewLogger

func NewLogger(handler *slogcp.Handler, opts ...LoggerOption) *Logger

NewLogger creates a go-grpc-middleware logging adapter. It uses opts first, then the provided slogcp.Handler, and finally slog.Default.

Example:

handler, _ := slogcp.NewHandler(os.Stdout)
adapter := slogcpadapter.NewLogger(handler)
server := grpc.NewServer(
	grpc.ChainUnaryInterceptor(grpc_logging.UnaryServerInterceptor(adapter)),
)

func (*Logger) Log

func (l *Logger) Log(ctx context.Context, level grpc_logging.Level, msg string, fields ...any)

Log forwards one go-grpc-middleware log event to the underlying slog.Logger. It preserves ctx so slogcp can attach trace correlation fields.

type LoggerOption

type LoggerOption func(*loggerConfig)

LoggerOption configures a Logger created by NewLogger.

func WithLevelMapper

func WithLevelMapper(mapper func(grpc_logging.Level) slog.Level) LoggerOption

WithLevelMapper makes NewLogger use mapper to convert go-grpc-middleware levels to slog levels. A nil mapper is ignored.

func WithLogger

func WithLogger(logger *slog.Logger) LoggerOption

WithLogger makes NewLogger use logger instead of constructing one from a handler. A nil logger is ignored.

Example:

base := slog.New(slog.NewTextHandler(os.Stdout, nil))
adapter := slogcpadapter.NewLogger(nil, slogcpadapter.WithLogger(base))
_ = grpc_logging.UnaryServerInterceptor(adapter) // reuse base logger in interceptors

Jump to

Keyboard shortcuts

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