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 trace context, and opt in to selecting request-scoped loggers and their attributes 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:
-
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.
-
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.
-
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.
Selecting a request-scoped logger
Selection is fixed by default, including in the four convenience interceptor
helpers. Enable contextual selection with an adapter option:
common := base.With("service", "billing")
adapted := slogcpadapter.NewLogger(nil,
slogcpadapter.WithLogger(common),
slogcpadapter.WithLoggerPolicy(slogcpadapter.PreferContext),
)
ctx = slogcp.ContextWithLogger(ctx, common.With("request_id", "alpha"))
unaryServer := grpc_logging.UnaryServerInterceptor(adapted)
streamServer := grpc_logging.StreamServerInterceptor(adapted)
unaryClient := grpc_logging.UnaryClientInterceptor(adapted)
streamClient := grpc_logging.StreamClientInterceptor(adapted)
PreferContext selects the entire logger stored by slogcp.ContextWithLogger,
even when WithLogger supplies a different logger. Its destination, level filter,
redaction, bound attributes and groups apply. A filtered or discarded event is
not retried through the fallback. Attributes bound only to the fallback are not
merged; derive request loggers from a common base to share attributes. An open
WithGroup also groups RPC event fields. Duplicate fields follow the selected
handler's existing rules. Cloud Logging formatting and trace extraction require
a selected handler that supports them.
Absent or nil contexts use the construction-time fallback: WithLogger, the
supplied handler, or the default captured by NewLogger, in that order. Later
slog.SetDefault calls do not change that fallback. An explicitly stored default
logger counts as present. ContextWithLogger(ctx, nil) leaves inherited loggers
intact; a nil context remains nil. Cancellation does not suppress adapter logging.
Unknown policy values behave as Fixed.
Install the contextual logger before the logging interceptor runs. On unary
servers, put enrichment in an outer interceptor; on streaming servers, pass a
stream wrapper whose Context() returns the enriched context. On clients, attach
the logger to the call context or in an outer interceptor. The first interceptor
in a gRPC chain is outermost. The middleware retains its event context: a child
context created inside a handler or inner interceptor does not retroactively
change completion logs. Middleware field injection and trace propagation are
separate from logger selection.
The convenience helpers continue to accept upstream middleware options only.
Use NewLogger with the upstream constructors above for contextual selection or
other adapter options.
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.