From 9b6c438cc7e09cc513c3268e72b05c27097ff8381a2ccae3a9b1fc3aee9e3124 Mon Sep 17 00:00:00 2001 From: nfel Date: Sat, 29 Aug 2026 12:59:48 +0330 Subject: [PATCH] feat(gl): add structured grpc observability --- cmd/dashboard/main.go | 2 ++ cmd/gl/main.go | 2 ++ dashboard.cfg.toml | 2 +- go.mod | 10 ------ go.sum | 23 -------------- infrastructure/observability/logger.go | 30 ++++++++++++++++++ interface/grpc/server.go | 42 +++++++++++++++++++++++++- 7 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 infrastructure/observability/logger.go diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 0825463..b1dd2ca 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -10,11 +10,13 @@ import ( "gl/application/explorer" "gl/infrastructure/config" + "gl/infrastructure/observability" "gl/infrastructure/postgres" webadapter "gl/interface/web" ) func main() { + observability.Configure("gl-dashboard", slog.LevelInfo) configPath := flag.String("conf", "./dashboard.cfg.toml", "path to the dashboard TOML configuration file") flag.Parse() diff --git a/cmd/gl/main.go b/cmd/gl/main.go index 8f5eb0a..45c723a 100644 --- a/cmd/gl/main.go +++ b/cmd/gl/main.go @@ -11,11 +11,13 @@ import ( "gl/application/health" applicationledger "gl/application/ledger" "gl/infrastructure/config" + "gl/infrastructure/observability" "gl/infrastructure/postgres" grpcadapter "gl/interface/grpc" ) func main() { + observability.Configure("gl", slog.LevelInfo) configPath := flag.String("conf", "./gl.cfg.toml", "path to the TOML configuration file") flag.Parse() diff --git a/dashboard.cfg.toml b/dashboard.cfg.toml index dda9ac5..4c9f4fd 100644 --- a/dashboard.cfg.toml +++ b/dashboard.cfg.toml @@ -2,7 +2,7 @@ environment = "local" [http] host = "0.0.0.0" -port = 8080 +port = 8601 read-header-timeout = "5s" shutdown-timeout = "10s" diff --git a/go.mod b/go.mod index a6d5cab..357f00d 100644 --- a/go.mod +++ b/go.mod @@ -13,30 +13,20 @@ require ( ) require ( - github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cli/browser v1.3.0 // indirect - github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/natefinch/atomic v1.0.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.41.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect ) diff --git a/go.sum b/go.sum index fac2ebf..12fe333 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,8 @@ -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= -github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= @@ -35,17 +25,10 @@ github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc= github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -57,20 +40,14 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= diff --git a/infrastructure/observability/logger.go b/infrastructure/observability/logger.go new file mode 100644 index 0000000..1850a4e --- /dev/null +++ b/infrastructure/observability/logger.go @@ -0,0 +1,30 @@ +package observability + +import ( + "log/slog" + "os" +) + +// Configure installs the process-wide JSON logger used by every GL adapter. +func Configure(service string, level slog.Leveler) *slog.Logger { + handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: level, + ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { + switch attr.Key { + case slog.TimeKey: + attr.Key = "timestamp" + case slog.LevelKey: + attr.Key = "severity" + case slog.MessageKey: + attr.Key = "message" + case slog.SourceKey: + attr.Key = "source" + } + return attr + }, + }) + logger := slog.New(handler).With("service", service) + slog.SetDefault(logger) + return logger +} diff --git a/interface/grpc/server.go b/interface/grpc/server.go index bf45ca6..985cf64 100644 --- a/interface/grpc/server.go +++ b/interface/grpc/server.go @@ -4,13 +4,17 @@ import ( "context" "errors" "fmt" + "log/slog" "net" + "runtime/debug" "time" ledgerv1 "gl/gen/ledger/v1" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/reflection" + "google.golang.org/grpc/status" ) type ServerConfig struct { @@ -30,7 +34,7 @@ func Run(ctx context.Context, cfg ServerConfig, handler ledgerv1.GeneralLedgerSe func runWithListener(ctx context.Context, shutdownTimeout time.Duration, listener net.Listener, handler ledgerv1.GeneralLedgerServiceServer) error { defer listener.Close() - server := grpc.NewServer() + server := grpc.NewServer(grpc.ChainUnaryInterceptor(structuredUnaryLogger(), panicRecovery())) ledgerv1.RegisterGeneralLedgerServiceServer(server, handler) reflection.Register(server) @@ -67,3 +71,39 @@ func runWithListener(ctx context.Context, shutdownTimeout time.Duration, listene } return nil } + +func structuredUnaryLogger() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + started := time.Now() + response, err := handler(ctx, req) + code := status.Code(err) + level := slog.LevelInfo + if code != codes.OK { + level = slog.LevelError + } + slog.Log(ctx, level, "grpc call finished", + "component", "grpc_server", + "grpc_method", info.FullMethod, + "grpc_code", code.String(), + "duration_ms", float64(time.Since(started).Microseconds())/1000, + ) + return response, err + } +} + +func panicRecovery() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (response any, err error) { + defer func() { + if recovered := recover(); recovered != nil { + slog.ErrorContext(ctx, "grpc panic recovered", + "component", "grpc_server", + "grpc_method", info.FullMethod, + "panic", recovered, + "stack", string(debug.Stack()), + ) + err = status.Error(codes.Internal, "internal server error") + } + }() + return handler(ctx, req) + } +}