diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 7b8f646..84b8533 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -36,28 +36,36 @@ def init_git(): git.wait() def init_proto(): - # Order matters: download → generate (needs buf plugins) → mock (needs generated - # interfaces) → tidy (needs all imports including generated mocks to resolve) + # Order matters: download → generate (needs buf plugins) → tidy -e (reconcile + # indirect deps; -e ignores missing mock packages) → mock (needs generated + # interfaces) → tidy (final, needs all imports including mocks to resolve) print("Starting proto initialization...") - print("Step 1/4: Fetching Go modules (this might take a few minutes)...") + print("Step 1/5: Fetching Go modules (this might take a few minutes)...") code = Popen(["go", "mod", "download", "all"], cwd=PROJECT_DIRECTORY).wait() if code != 0: print("Error: Failed to fetch Go modules.") sys.exit(code) - print("Step 2/4: Running 'make generate'...") + print("Step 2/5: Running 'make generate'...") code = Popen(["make", "generate"], cwd=PROJECT_DIRECTORY).wait() if code != 0: print("Error: 'make generate' failed.") sys.exit(code) - print("Step 3/4: Running 'make mock'...") + print("Step 3/5: Tidying Go modules (pre-mock)...") + # -e flag ignores errors from missing mock packages (generated next step). + # Non-zero exit is expected (mock imports don't resolve yet) but logged for debugging. + code = Popen(["go", "mod", "tidy", "-e"], cwd=PROJECT_DIRECTORY).wait() + if code != 0: + print("Warning: pre-mock 'go mod tidy -e' exited with code %d (expected if mock packages don't exist yet)." % code) + + print("Step 4/5: Running 'make mock'...") code = Popen(["make", "mock"], cwd=PROJECT_DIRECTORY).wait() if code != 0: print("Error: 'make mock' failed.") sys.exit(code) - print("Step 4/4: Tidying Go modules...") + print("Step 5/5: Tidying Go modules (final)...") code = Popen(["go", "mod", "tidy"], cwd=PROJECT_DIRECTORY).wait() if code != 0: print("Error: 'go mod tidy' failed.") diff --git a/{{cookiecutter.app_name}}/AGENTS.md b/{{cookiecutter.app_name}}/AGENTS.md index bc9ab2d..3c88a6b 100644 --- a/{{cookiecutter.app_name}}/AGENTS.md +++ b/{{cookiecutter.app_name}}/AGENTS.md @@ -58,7 +58,7 @@ make run-docker # Run in Docker container ## Key Patterns - **gRPC-first**: All endpoints are defined in `proto/{{cookiecutter.app_name|lower}}.proto`. HTTP/JSON routes are auto-generated via grpc-gateway annotations. Never create HTTP handlers manually. -- **Context propagation**: `context.Context` is the first parameter everywhere. Interceptors propagate trace IDs, log fields, and options through it. +- **Context propagation**: `context.Context` is the first parameter everywhere. Interceptors propagate trace IDs, log fields, and options through it. Service code uses `slog.LogAttrs(ctx, ...)` for logging; ColdBrew's Handler automatically injects context fields. Use `github.com/go-coldbrew/log.AddAttrsToContext` (imported as `cblog` in service.go) to add typed context fields. - **Configuration**: All config via environment variables using `envconfig`. Add fields to `config/config.go` with struct tags. See [ColdBrew config docs](https://pkg.go.dev/github.com/go-coldbrew/core/config#Config) for framework options. - **Authentication**: JWT and API key auth are built in via `service/auth/`. Config-controlled — set `JWT_SECRET` or `API_KEYS` env vars to enable. Health/ready/reflection RPCs bypass auth automatically. See [Authentication docs](https://docs.coldbrew.cloud/howto/auth/). - **Health checks**: Kubernetes liveness (`/healthcheck`) and readiness (`/readycheck`) are built-in. Service starts as NOT_SERVING until `SetReady()` is called. diff --git a/{{cookiecutter.app_name}}/config/config.go b/{{cookiecutter.app_name}}/config/config.go index 5ccd3d7..d5e360b 100644 --- a/{{cookiecutter.app_name}}/config/config.go +++ b/{{cookiecutter.app_name}}/config/config.go @@ -2,9 +2,9 @@ package config import ( "context" + "log/slog" cbConfig "github.com/go-coldbrew/core/config" - "github.com/go-coldbrew/log" "github.com/kelseyhightower/envconfig" "{{cookiecutter.source_path}}/{{cookiecutter.app_name}}/service/auth" ) @@ -29,7 +29,7 @@ func init() { if defaultConfig.PanicOnConfigError { panic(err) } else { - log.Error(context.Background(), "msg", "error while loading config", "err", err) + slog.LogAttrs(context.Background(), slog.LevelError, "error while loading config", slog.Any("err", err)) } } } diff --git a/{{cookiecutter.app_name}}/go.mod b/{{cookiecutter.app_name}}/go.mod index 0af9763..0e554e3 100644 --- a/{{cookiecutter.app_name}}/go.mod +++ b/{{cookiecutter.app_name}}/go.mod @@ -12,10 +12,10 @@ tool ( require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 - github.com/go-coldbrew/core v0.1.45 - github.com/go-coldbrew/errors v0.2.13 - github.com/go-coldbrew/interceptors v0.1.20 - github.com/go-coldbrew/log v0.3.1 + github.com/go-coldbrew/core v0.1.51 + github.com/go-coldbrew/errors v0.2.15 + github.com/go-coldbrew/interceptors v0.1.25 + github.com/go-coldbrew/log v0.4.1 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 @@ -134,7 +134,7 @@ require ( github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-coldbrew/hystrixprometheus v0.1.2 // indirect github.com/go-coldbrew/options v0.3.0 // indirect - github.com/go-coldbrew/tracing v0.2.1 // indirect + github.com/go-coldbrew/tracing v0.2.2 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/{{cookiecutter.app_name}}/main.go b/{{cookiecutter.app_name}}/main.go index 8e15db2..80a918c 100644 --- a/{{cookiecutter.app_name}}/main.go +++ b/{{cookiecutter.app_name}}/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "log/slog" "net/http" "strings" @@ -11,7 +12,6 @@ import ( "{{cookiecutter.source_path}}/{{cookiecutter.app_name}}/service/auth" "{{cookiecutter.source_path}}/{{cookiecutter.app_name}}/version" "github.com/go-coldbrew/core" - "github.com/go-coldbrew/log" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/swaggest/swgui" "github.com/swaggest/swgui/v5emb" @@ -142,5 +142,7 @@ func main() { // Start the service and wait for it to exit // This is a blocking call and will not return until the service exits completely - log.Error(context.Background(), cb.Run()) + if err := cb.Run(); err != nil { + slog.LogAttrs(context.Background(), slog.LevelError, "service exited", slog.Any("err", err)) + } } diff --git a/{{cookiecutter.app_name}}/service/auth/auth.go b/{{cookiecutter.app_name}}/service/auth/auth.go index 3ce2c93..f3f4e31 100644 --- a/{{cookiecutter.app_name}}/service/auth/auth.go +++ b/{{cookiecutter.app_name}}/service/auth/auth.go @@ -15,11 +15,11 @@ package auth import ( "context" "fmt" + "log/slog" "strings" "time" "github.com/go-coldbrew/interceptors" - "github.com/go-coldbrew/log" "github.com/golang-jwt/jwt/v5" grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "google.golang.org/grpc" @@ -100,7 +100,7 @@ func withAuthLogging(fn grpcauth.AuthFunc) grpcauth.AuthFunc { authCtx, err := fn(ctx) if err != nil { method, _ := grpc.Method(ctx) - log.Warn(ctx, "msg", "auth failed", "method", method, "err", err) + slog.LogAttrs(ctx, slog.LevelWarn, "auth failed", slog.String("method", method), slog.Any("err", err)) } return authCtx, err } @@ -120,7 +120,7 @@ func eitherAuthFunc(authFuncs ...grpcauth.AuthFunc) grpcauth.AuthFunc { lastErr = err } method, _ := grpc.Method(ctx) - log.Warn(ctx, "msg", "auth failed: all methods exhausted", "method", method, "err", lastErr) + slog.LogAttrs(ctx, slog.LevelWarn, "auth failed: all methods exhausted", slog.String("method", method), slog.Any("err", lastErr)) return nil, lastErr } } diff --git a/{{cookiecutter.app_name}}/service/service.go b/{{cookiecutter.app_name}}/service/service.go index 51e6c46..73079fb 100644 --- a/{{cookiecutter.app_name}}/service/service.go +++ b/{{cookiecutter.app_name}}/service/service.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "log/slog" "time" "{{cookiecutter.source_path}}/{{cookiecutter.app_name}}/config" @@ -11,7 +12,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/genproto/googleapis/api/httpbody" "github.com/go-coldbrew/errors" - "github.com/go-coldbrew/log" + cblog "github.com/go-coldbrew/log" "google.golang.org/grpc/health" ) @@ -42,8 +43,8 @@ func (s *svc) HealthCheck(ctx context.Context, _ *emptypb.Empty) (*httpbody.Http } // Echo returns the message with the prefix added -// TODO: remove this, since this is just to demonstrate how to use endpoints and config -func (s *svc) Echo(_ context.Context, req *proto.EchoRequest) (resp *proto.EchoResponse, err error) { +// TODO: remove this, since this is just to demonstrate how to use endpoints, config, and logging +func (s *svc) Echo(ctx context.Context, req *proto.EchoRequest) (resp *proto.EchoResponse, err error) { start := time.Now() outcome := metrics.OutcomeSuccess defer func() { @@ -53,6 +54,13 @@ func (s *svc) Echo(_ context.Context, req *proto.EchoRequest) (resp *proto.EchoR s.monitoring.IncEchoTotal(outcome) s.monitoring.ObserveEchoDuration(outcome, time.Since(start)) }() + + // Add typed context fields — these appear in all logs for this request. + // ColdBrew interceptors already add trace_id and grpcMethod automatically. + ctx = cblog.AddAttrsToContext(ctx, slog.Int("echo_msg_len", len(req.GetMsg()))) + + slog.LogAttrs(ctx, slog.LevelInfo, "echo requested") + return &proto.EchoResponse{ Msg: fmt.Sprintf("%s: %s", s.prefix, req.GetMsg()), }, nil @@ -62,7 +70,7 @@ func (s *svc) Echo(_ context.Context, req *proto.EchoRequest) (resp *proto.EchoR // TODO: remove this, since this is just to demonstrate how to use endpoints and config func (s *svc) Error(ctx context.Context, req *proto.EchoRequest) (*proto.EchoResponse, error) { err := errors.New("This is an Error") - log.Info(ctx, "error requested") + slog.LogAttrs(ctx, slog.LevelInfo, "error requested") return nil, errors.Wrap(err, "endpoint error") }