Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions interceptors/logging/examples/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/grpc-ecosystem/go-grpc-middleware/interceptors/logging/examples

go 1.23.0
go 1.24.0

require (
github.com/go-kit/log v0.2.1
Expand All @@ -19,11 +19,11 @@ require (
github.com/mattn/go-isatty v0.0.14 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

replace github.com/grpc-ecosystem/go-grpc-middleware/v2 => ../../../
5 changes: 5 additions & 0 deletions interceptors/logging/examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,24 @@ go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
4 changes: 2 additions & 2 deletions interceptors/logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors"
_ "log/slog"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only purpose of adding this was so that the documentation comment for the Logger interface renders the hyperlink to the slog.Level implementation.
It is non-essential and I can remove it if not required.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, TIL

)

var (
Expand Down Expand Up @@ -206,8 +207,7 @@ func AddFields(ctx context.Context, f Fields) {
// Logger requires Log method, similar to experimental slog, allowing logging interceptor to be interoperable. Official
// adapters for popular loggers are in `provider/` directory (separate modules). It's totally ok to copy simple function
// implementation over.
// TODO(bwplotka): Once slog is official, we could use slog method directly. Currently level is copied over, so we don't
// depend on experimental module.
// [Level] is an alias for [slog.Level], so you can use it in your own logger implementation.
// interface used for all our interceptors.
type Logger interface {
Log(ctx context.Context, level Level, msg string, fields ...any)
Expand Down
39 changes: 8 additions & 31 deletions interceptors/logging/slog.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,15 @@

package logging

// A Level is the importance or severity of a log event.
// The higher the level, the more important or severe the event.
type Level int
import "log/slog"

// Level numbers are inherently arbitrary,
// but we picked them to satisfy three constraints.
// Any system can map them to another numbering scheme if it wishes.
//
// First, we wanted the default level to be Info, Since Levels are ints, Info is
// the default value for int, zero.
//
// Level is an alias for slog.Level.
type Level slog.Level

// Second, we wanted to make it easy to use levels to specify logger verbosity.
// Since a larger level means a more severe event, a logger that accepts events
// with smaller (or more negative) level means a more verbose logger. Logger
// verbosity is thus the negation of event severity, and the default verbosity
// of 0 accepts all events at least as severe as INFO.
//
// Third, we wanted some room between levels to accommodate schemes with named
// levels between ours. For example, Google Cloud Logging defines a Notice level
// between Info and Warn. Since there are only a few of these intermediate
// levels, the gap between the numbers need not be large. Our gap of 4 matches
// OpenTelemetry's mapping. Subtracting 9 from an OpenTelemetry level in the
// DEBUG, INFO, WARN and ERROR ranges converts it to the corresponding slog
// Level range. OpenTelemetry also has the names TRACE and FATAL, which slog
// does not. But those OpenTelemetry levels can still be represented as slog
// Levels by using the appropriate integers.
//
// Names for common levels.
// Common level values.
const (
LevelDebug Level = -4
LevelInfo Level = 0
LevelWarn Level = 4
LevelError Level = 8
LevelDebug Level = Level(slog.LevelDebug)
LevelInfo Level = Level(slog.LevelInfo)
LevelWarn Level = Level(slog.LevelWarn)
LevelError Level = Level(slog.LevelError)
)
212 changes: 212 additions & 0 deletions interceptors/logging/slog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright (c) The go-grpc-middleware Authors.
// Licensed under the Apache License 2.0.

package logging

import (
"context"
"log/slog"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
)

// TestLevelValues verifies that Level constants retain their expected numeric
// values after the type was changed from a bare int to an alias of slog.Level.
// Any change here would be a breaking change for users who rely on the numeric
// values (e.g. serialization, comparison, or custom code-to-level mappers).
func TestLevelValues(t *testing.T) {
tests := []struct {
name string
level Level
wantInt int
slogLvl slog.Level
}{
{name: "LevelDebug", level: LevelDebug, wantInt: -4, slogLvl: slog.LevelDebug},
{name: "LevelInfo", level: LevelInfo, wantInt: 0, slogLvl: slog.LevelInfo},
{name: "LevelWarn", level: LevelWarn, wantInt: 4, slogLvl: slog.LevelWarn},
{name: "LevelError", level: LevelError, wantInt: 8, slogLvl: slog.LevelError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Numeric value must remain unchanged (backward compatibility).
assert.Equal(t, tt.wantInt, int(tt.level), "numeric value of %s must be %d", tt.name, tt.wantInt)
// Must equal the corresponding slog.Level constant.
assert.Equal(t, tt.slogLvl, slog.Level(tt.level), "%s must equal slog.%s", tt.name, tt.slogLvl)
})
}
}

// TestLevelSlogRoundTrip ensures Level to slog.Level conversions are lossless.
func TestLevelSlogRoundTrip(t *testing.T) {
tests := []struct {
name string
level Level
}{
{name: "LevelDebug", level: LevelDebug},
{name: "LevelInfo", level: LevelInfo},
{name: "LevelWarn", level: LevelWarn},
{name: "LevelError", level: LevelError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
slogLevel := slog.Level(tt.level)
backToLevel := Level(slogLevel)
assert.Equal(t, tt.level, backToLevel, "round-trip conversion must be lossless for %v", tt.level)
})
}
}

// TestLevelComparison verifies the ordering contract: Debug < Info < Warn < Error.
func TestLevelComparison(t *testing.T) {
tests := []struct {
name string
lower Level
higher Level
}{
{name: "Debug < Info", lower: LevelDebug, higher: LevelInfo},
{name: "Info < Warn", lower: LevelInfo, higher: LevelWarn},
{name: "Warn < Error", lower: LevelWarn, higher: LevelError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Less(t, tt.lower, tt.higher)
})
}
}

// TestLoggerFuncReceivesCorrectLevel asserts that when the logging interceptor
// invokes Logger.Log, the level argument is still the expected Level type and
// value. This catches accidental type mismatches that could arise after the
// Level alias change.
func TestLoggerFuncReceivesCorrectLevel(t *testing.T) {
tests := []struct {
name string
level Level
}{
{name: "LevelDebug", level: LevelDebug},
{name: "LevelInfo", level: LevelInfo},
{name: "LevelWarn", level: LevelWarn},
{name: "LevelError", level: LevelError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var captured Level
fn := LoggerFunc(func(_ context.Context, lvl Level, _ string, _ ...any) {
captured = lvl
})
fn.Log(context.Background(), tt.level, "test message")
assert.Equal(t, tt.level, captured)
})
}
}

// TestLoggerInterfaceAcceptsSlogLevel ensures that a slog.Level value can be
// passed through the Logger interface after a simple cast, which is the primary
// use-case unlocked by the alias change.
func TestLoggerInterfaceAcceptsSlogLevel(t *testing.T) {
tests := []struct {
name string
slogLevel slog.Level
wantLevel Level
}{
{name: "slog.LevelDebug", slogLevel: slog.LevelDebug, wantLevel: LevelDebug},
{name: "slog.LevelInfo", slogLevel: slog.LevelInfo, wantLevel: LevelInfo},
{name: "slog.LevelWarn", slogLevel: slog.LevelWarn, wantLevel: LevelWarn},
{name: "slog.LevelError", slogLevel: slog.LevelError, wantLevel: LevelError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var captured Level
fn := LoggerFunc(func(_ context.Context, lvl Level, _ string, _ ...any) {
captured = lvl
})
fn.Log(context.Background(), Level(tt.slogLevel), "msg")
assert.Equal(t, tt.wantLevel, captured,
"Level(%v) passed to Logger should equal %v", tt.slogLevel, tt.wantLevel)
})
}
}

// TestDefaultServerCodeToLevel_LevelTypes confirms that the server code-to-level
// mapper returns values that are valid slog.Level equivalents.
func TestDefaultServerCodeToLevel_LevelTypes(t *testing.T) {
tests := []struct {
code codes.Code
wantLvl Level
}{
{codes.OK, LevelInfo},
{codes.NotFound, LevelInfo},
{codes.InvalidArgument, LevelInfo},
{codes.Internal, LevelError},
{codes.Unknown, LevelError},
{codes.DataLoss, LevelError},
{codes.Unauthenticated, LevelInfo},
}
for _, tt := range tests {
t.Run(tt.code.String(), func(t *testing.T) {
got := DefaultServerCodeToLevel(tt.code)
assert.Equal(t, tt.wantLvl, got)
// Verify the returned Level is a valid slog.Level.
assert.Equal(t, slog.Level(tt.wantLvl), slog.Level(got))
})
}
}

// TestDefaultClientCodeToLevel_LevelTypes confirms that the client code-to-level
// mapper returns values that are valid slog.Level equivalents.
func TestDefaultClientCodeToLevel_LevelTypes(t *testing.T) {
tests := []struct {
code codes.Code
wantLvl Level
}{
{codes.OK, LevelDebug},
{codes.Canceled, LevelDebug},
{codes.NotFound, LevelDebug},
{codes.Unknown, LevelInfo},
{codes.DeadlineExceeded, LevelInfo},
{codes.Unauthenticated, LevelInfo},
{codes.Internal, LevelWarn},
{codes.Unavailable, LevelWarn},
}
for _, tt := range tests {
t.Run(tt.code.String(), func(t *testing.T) {
got := DefaultClientCodeToLevel(tt.code)
assert.Equal(t, tt.wantLvl, got)
// Verify the returned Level is a valid slog.Level.
assert.Equal(t, slog.Level(tt.wantLvl), slog.Level(got))
})
}
}

// TestCustomCodeToLevelWithSlogLevel simulates a user-defined CodeToLevel
// function that returns slog.Level values cast to Level. This is the main
// integration pattern enabled by the alias change.
func TestCustomCodeToLevelWithSlogLevel(t *testing.T) {
// A user-defined mapper using slog.Level values directly.
customMapper := func(code codes.Code) Level {
switch code {
case codes.OK:
return Level(slog.LevelDebug)
case codes.Internal:
return Level(slog.LevelError)
default:
return Level(slog.LevelInfo)
}
}

require.Equal(t, LevelDebug, customMapper(codes.OK))
require.Equal(t, LevelError, customMapper(codes.Internal))
require.Equal(t, LevelInfo, customMapper(codes.NotFound))
}

// TestLevelZeroValue ensures the zero value of Level equals LevelInfo (which is
// slog.LevelInfo = 0). This is important because Go zero-initializes variables
// and this was always the implicit contract.
func TestLevelZeroValue(t *testing.T) {
var zeroLevel Level
assert.Equal(t, LevelInfo, zeroLevel, "zero value of Level must be LevelInfo")
assert.Equal(t, slog.LevelInfo, slog.Level(zeroLevel), "zero value must map to slog.LevelInfo")
}
Loading