diff --git a/Cookiecutter.md b/Cookiecutter.md index dbf40e3..724686f 100644 --- a/Cookiecutter.md +++ b/Cookiecutter.md @@ -1,13 +1,16 @@ --- layout: default -title: "Cookiecutter Setup" -nav_order: 3 -description: "Detailed cookiecutter template setup for ColdBrew" -permalink: /getting-started +title: "Cookiecutter Reference" +nav_order: 10 +description: "Detailed reference for the ColdBrew cookiecutter project template" +permalink: /cookiecutter-reference --- -# Cookiecutter Setup +# Cookiecutter Reference {: .no_toc } +{: .note } +Looking to create your first ColdBrew service? See the **[Getting Started](/getting-started)** guide instead. This page is a detailed reference for the cookiecutter template. + ## Table of contents {: .no_toc .text-delta } diff --git a/FAQ.md b/FAQ.md index 64c879f..bdc0d1c 100644 --- a/FAQ.md +++ b/FAQ.md @@ -2,7 +2,7 @@ layout: default title: "FAQ" nav_order: 8 -description: "Frequently asked questions about ColdBrew" +description: "Frequently asked questions about ColdBrew: gRPC framework configuration, interceptors, tracing, and troubleshooting" permalink: /faq --- # Frequently Asked Questions @@ -64,6 +64,17 @@ func init() { } ``` +## How does trace ID propagation work? + +ColdBrew generates a unique trace ID for every request automatically. It can also read a trace ID from two sources: + +1. **HTTP header** — `x-trace-id` (configurable via `TRACE_HEADER_NAME`) is forwarded from the HTTP gateway to gRPC +2. **Proto field** — if your request message has a `trace_id` string field, ColdBrew reads it via the generated `GetTraceId()` method + +The trace ID is then propagated to structured logs (`"trace": "abc123"`) and Sentry/Rollbar error reports — so you can search for one ID and find the complete request flow across your logs and error tracking. + +See the [Tracing How-To](/howto/Tracing/#trace-id-propagation) for details. + ## How do I migrate from OpenTracing to OpenTelemetry? The `tracing` package supports both. To switch: @@ -72,6 +83,37 @@ The `tracing` package supports both. To switch: 2. The `tracing.NewInternalSpan()`, `tracing.NewDatastoreSpan()`, and `tracing.NewExternalSpan()` functions work with both backends 3. See the [Tracing How-To](/howto/Tracing/) and [Integrations](/integrations) guides for setup details +## What is vtprotobuf and why does ColdBrew use it? + +[vtprotobuf](https://github.com/planetscale/vtprotobuf) (by PlanetScale) generates optimized `MarshalVT()`/`UnmarshalVT()` methods for protobuf messages that are typically **2–3x faster** than standard `proto.Marshal()` with fewer allocations. + +ColdBrew registers a custom gRPC codec that uses vtprotobuf automatically. You don't need to change any application code — if your proto messages have VT methods generated (the default with the cookiecutter template), the fast path is used. Messages without VT methods fall back to standard protobuf transparently. + +**Key differences from standard protobuf:** + +| | Standard protobuf | vtprotobuf | +|---|---|---| +| Marshal/Unmarshal | Reflection-based | Generated code, no reflection | +| Performance | Baseline | ~2–3x faster, fewer allocations | +| Extra features | None | `CloneVT()`, `EqualVT()`, object pooling | +| Compatibility | Universal | Falls back to standard if VT methods missing | + +vtprotobuf only affects the **gRPC wire protocol**. The HTTP/JSON gateway uses grpc-gateway's own marshallers independently. + +To disable: `DISABLE_VT_PROTOBUF=true`. See the [vtprotobuf How-To](/howto/vtproto) for full details including code generation setup. + +## How does ColdBrew ensure API consistency? + +Through **compile-time enforcement**. Your `.proto` file is the single source of truth. Running `buf generate` produces: + +1. **Typed Go interfaces** — the compiler refuses to build until every RPC method is implemented +2. **HTTP gateway handlers** — REST endpoints that can't drift from the gRPC definition +3. **OpenAPI spec** — Swagger documentation generated from the same proto, always in sync + +This strongly prevents a documented endpoint that doesn't exist, an undocumented endpoint that does exist, or an HTTP route that doesn't match the gRPC method signature. The proto file is the contract — the compiler, the gateway, and the docs all enforce it. + +See [Self-Documenting APIs](/architecture#self-documenting-apis) for the full pipeline. + ## Is hystrixprometheus still maintained? **No.** The `hystrixprometheus` package depends on `afex/hystrix-go`, which is unmaintained. Do not invest in this package for new projects. @@ -112,6 +154,23 @@ func init() { See the [Metrics How-To](/howto/Metrics/) for more details. +## How do I use grpcurl or Postman with my ColdBrew service? + +ColdBrew enables [gRPC server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) by default, so tools like [grpcurl](https://github.com/fullstorydev/grpcurl), [grpcui](https://github.com/fullstorydev/grpcui), and Postman can discover your services and methods without needing proto files. + +```bash +# List all services +grpcurl -plaintext localhost:9090 list + +# Describe a specific service +grpcurl -plaintext localhost:9090 describe mypackage.MyService + +# Call a method +grpcurl -plaintext -d '{"msg": "hello"}' localhost:9090 mypackage.MyService/Echo +``` + +To disable reflection (e.g., in production for security), set `DISABLE_GRPC_REFLECTION=true`. See the [Configuration Reference](/config-reference) for details. + ## How do I configure graceful shutdown? ColdBrew handles SIGTERM and SIGINT automatically. When a signal is received: @@ -140,6 +199,18 @@ notifier.Notify(err, ctx) See the [Errors How-To](/howto/errors) and [Integrations](/integrations) for full setup instructions. +## Is ColdBrew designed for Kubernetes? + +Yes — ColdBrew is Kubernetes-native by design. Out of the box you get: + +- **Liveness probe** at `/healthcheck` and **readiness probe** at `/readycheck` +- **Graceful shutdown** on SIGTERM with configurable drain periods (`SHUTDOWN_DURATION_IN_SECONDS`, `GRPC_GRACEFUL_DURATION_IN_SECONDS`) +- **Prometheus metrics** at `/metrics` for scraping +- **Structured JSON logging** to stdout (ready for Fluentd, Loki, or any log aggregator) +- **Environment variable configuration** via [envconfig](https://github.com/kelseyhightower/envconfig) — works natively with ConfigMaps and Secrets + +ColdBrew also follows [12-factor app](https://12factor.net/) principles: no config files, stateless processes, port binding, and log streams. See the [Production Deployment guide](/howto/production) for K8s manifests, ServiceMonitor setup, and graceful shutdown tuning, and the [Architecture](/architecture) page for the full design principles table. + ## Where can I get help? - **[GitHub Discussions](https://github.com/go-coldbrew/core/discussions)** — Ask questions, share ideas diff --git a/Index.md b/Index.md index 7f40d25..77bd9ed 100644 --- a/Index.md +++ b/Index.md @@ -8,7 +8,7 @@ permalink: / # ColdBrew {: .fs-9 } -A Go microservice framework for building production-grade gRPC services with built-in observability, resilience, and HTTP gateway support. +A Kubernetes-native Go microservice framework for building production-grade gRPC services with built-in observability, resilience, and HTTP gateway support. Follows [12-factor](https://12factor.net/) principles out of the box. {: .fs-6 .fw-300 } **Production-proven:** Powers 100+ microservices, handling peaks of ~70k QPS per service at [Gojek](https://www.gojek.com/en-id/). @@ -25,12 +25,19 @@ A Go microservice framework for building production-grade gRPC services with bui | Feature | Description | |---------|-------------| -| **gRPC + REST Gateway** | Define your API once in protobuf, get both gRPC and REST endpoints automatically via [grpc-gateway] | +| **gRPC + REST Gateway** | Define your API once in protobuf — get gRPC, REST, and [Swagger docs](/architecture#self-documenting-apis) automatically via [grpc-gateway] | | **Structured Logging** | Pluggable backends (go-kit, zap, logrus) with per-request context fields and trace ID propagation | | **Distributed Tracing** | [OpenTelemetry], [Jaeger], and [New Relic] support with automatic span creation in interceptors | | **Prometheus Metrics** | Built-in request latency, error rate, and circuit breaker metrics at `/metrics` | | **Error Tracking** | Stack traces, gRPC status codes, and async notification to [Sentry], Rollbar, or Airbrake | | **Resilience** | Client-side circuit breaking and retries via interceptors | +| **Fast Serialization** | [vtprotobuf] codec enabled by default — faster gRPC marshalling with automatic fallback to standard protobuf | +| **Kubernetes-native** | Health/ready probes, graceful SIGTERM shutdown, structured JSON logs, Prometheus metrics — all wired automatically | +| **Swagger / OpenAPI** | Interactive API docs auto-served at `/swagger/` from your protobuf definitions | +| **Profiling** | Go [pprof] endpoints at `/debug/pprof/` for CPU, memory, goroutine, and trace profiling | +| **gRPC Reflection** | Server reflection enabled by default — works with [grpcurl], [grpcui], and Postman | +| **HTTP Compression** | Automatic gzip compression for all HTTP gateway responses | +| **Container-aware Runtime** | Auto-tunes GOMAXPROCS to match container CPU limits via [automaxprocs] | ## Quick Start @@ -60,44 +67,27 @@ Your service starts with all of these endpoints ready: | `localhost:9091/swagger/` | Swagger UI | | `localhost:9091/debug/pprof/` | Go pprof profiling | -## Minimal Service Example +## Define Once, Get Everything -A ColdBrew service implements the `CBService` interface: +Your API is defined once in protobuf — ColdBrew generates everything else: -```go -package main - -import ( - "context" - - "github.com/go-coldbrew/core" - "github.com/go-coldbrew/core/config" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "google.golang.org/grpc" - - pb "github.com/yourorg/myservice/proto" // your generated protobuf package -) - -type myService struct{} - -func (s *myService) InitGRPC(ctx context.Context, server *grpc.Server) error { - pb.RegisterMyServiceServer(server, s) - return nil -} - -func (s *myService) InitHTTP(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error { - return pb.RegisterMyServiceHandlerFromEndpoint(ctx, mux, endpoint, opts) -} - -func main() { - cfg := config.GetColdBrewConfig() - cb := core.New(cfg) - cb.SetService(&myService{}) - cb.Run() +```protobuf +rpc Echo(EchoRequest) returns (EchoResponse) { + option (google.api.http) = { + post: "/api/v1/echo" + body: "*" + }; } ``` -All logging, tracing, metrics, health checks, and graceful shutdown are wired automatically. +This single definition gives you: +- **gRPC endpoint** on `:9090` — with reflection for [grpcurl] and Postman +- **REST endpoint** at `POST /api/v1/echo` on `:9091` — via [grpc-gateway] +- **Swagger UI** at `/swagger/` — interactive API docs from your proto +- **Prometheus metrics** — per-method latency, error rate, and request count +- **Distributed tracing** — automatic span creation through the interceptor chain + +Run `buf generate` — it creates typed Go interfaces from your proto definitions. The compiler ensures every RPC method is implemented, so API changes are caught at build time, not runtime. Just fill in your business logic and `make run`. Logging, tracing, metrics, health checks, and graceful shutdown are wired automatically. See the [full pipeline](/architecture#self-documenting-apis) for details. ## How It Works @@ -125,10 +115,6 @@ All logging, tracing, metrics, health checks, and graceful shutdown are wired au ColdBrew is modular — use the full framework or pick individual packages: -``` -options → errors → log → tracing → grpcpool → interceptors → data-builder → core -``` - | Package | What It Does | |---------|-------------| | [**core**](https://github.com/go-coldbrew/core) | gRPC server + HTTP gateway, health checks, graceful shutdown | @@ -152,6 +138,7 @@ ColdBrew integrates with the tools you already use: - [new relic] — Application performance monitoring - [sentry] — Error tracking and alerting - [go-grpc-middleware] — Middleware utilities +- [vtprotobuf] — Fast protobuf serialization ## Next Steps @@ -170,3 +157,8 @@ ColdBrew integrates with the tools you already use: [new relic]: https://newrelic.com/ [sentry]: https://sentry.io/ [go-grpc-middleware]: https://pkg.go.dev/github.com/grpc-ecosystem/go-grpc-middleware +[vtprotobuf]: https://github.com/planetscale/vtprotobuf +[pprof]: https://pkg.go.dev/net/http/pprof +[grpcurl]: https://github.com/fullstorydev/grpcurl +[grpcui]: https://github.com/fullstorydev/grpcui +[automaxprocs]: https://github.com/uber-go/automaxprocs diff --git a/Packages.md b/Packages.md index ffaacf4..3551e5f 100644 --- a/Packages.md +++ b/Packages.md @@ -1,7 +1,7 @@ --- layout: default title: Packages -description: "ColdBrew packages documentation" +description: "ColdBrew Go packages: core, interceptors, errors, log, tracing, options, grpcpool, and data-builder API reference" permalink: /packages nav_order: 9 --- diff --git a/USING.md b/USING.md index 3396f11..2ef61c4 100644 --- a/USING.md +++ b/USING.md @@ -101,11 +101,13 @@ ColdBrew uses environment variables for configuration. Common settings: | `HTTP_PORT` | `9091` | HTTP gateway port | | `LOG_LEVEL` | `info` | Log level (debug, info, warn, error) | | `JSON_LOGS` | `true` | JSON formatted logs | -| `ENVIRONMENT` | `development` | Environment name | -| `TRACE_HEADER_NAME` | `X-Trace-Id` | Header name for trace propagation | -| `NEW_RELIC_APP_NAME` | | New Relic application name | -| `NEW_RELIC_LICENSE_KEY` | | New Relic license key | -| `SENTRY_DSN` | | Sentry DSN for error tracking | +| `ENVIRONMENT` | `""` | Environment name | +| `TRACE_HEADER_NAME` | `x-trace-id` | Header name for trace propagation | +| `NEW_RELIC_APPNAME` | `""` | New Relic application name | +| `NEW_RELIC_LICENSE_KEY` | `""` | New Relic license key | +| `SENTRY_DSN` | `""` | Sentry DSN for error tracking | + +See the **[Configuration Reference](/config-reference)** for the complete list of 40+ environment variables including gRPC keepalive, TLS, OpenTelemetry OTLP, Prometheus histogram buckets, and graceful shutdown tuning. ## Adding Interceptors diff --git a/architecture.md b/architecture.md index e7fb6be..e4f268a 100644 --- a/architecture.md +++ b/architecture.md @@ -16,6 +16,81 @@ permalink: /architecture --- +## Design Principles + +ColdBrew follows [12-factor app](https://12factor.net/) methodology and is designed to run on Kubernetes from day one: + +| 12-Factor Principle | How ColdBrew Implements It | +|--------------------|-----------------------------| +| **Config** | All configuration via environment variables ([envconfig](https://github.com/kelseyhightower/envconfig)) — no config files, no YAML. See [Configuration Reference](/config-reference) | +| **Port binding** | Self-contained HTTP (`:9091`) and gRPC (`:9090`) servers, no external app server needed | +| **Logs** | Structured JSON to stdout by default — ready for any log aggregator (Fluentd, Loki, CloudWatch) | +| **Disposability** | Graceful SIGTERM handling with configurable drain periods. See [Signals](/howto/signals) | +| **Dev/prod parity** | Same binary, same config mechanism, same observability in every environment | +| **Concurrency** | Stateless processes — scale horizontally by adding replicas | +| **Backing services** | External dependencies (databases, caches, queues) attached via environment variables | + +ColdBrew is **Kubernetes-native**: health/ready probe endpoints, Prometheus metrics scraping, graceful pod termination, and structured logging work without any additional setup. See the [Production Deployment guide](/howto/production) for K8s manifests and configuration. + +## Self-Documenting APIs + +ColdBrew follows a **define once, get everything** approach. Your `.proto` file is the single source of truth — one `buf generate` produces everything your service needs: + +``` + ┌─── Go protobuf types (*.pb.go) + ├─── gRPC service stubs (*_grpc.pb.go) + myservice.proto ──buf──►├─── HTTP/REST gateway handlers (*.gw.go) + ├─── OpenAPI/Swagger spec (*.swagger.json) + └─── vtprotobuf fast codec (*_vtproto.pb.go) +``` + +Each output maps to a self-documenting endpoint: + +| Output | Serves | How Clients Discover It | +|--------|--------|------------------------| +| gRPC stubs | `:9090` | gRPC reflection — `grpcurl -plaintext localhost:9090 list` | +| HTTP gateway | `:9091/api/...` | Swagger UI at `/swagger/` | +| OpenAPI spec | `:9091/swagger/*.swagger.json` | Import into Postman, code generators, or API gateways | +| Health/version | `:9091/healthcheck` | Returns git commit, version, build date, Go version as JSON | +| Metrics | `:9091/metrics` | Prometheus self-describing exposition format with HELP lines | +| Profiling | `:9091/debug/pprof/` | Standard Go pprof index page | + +**Every client gets documentation for free:** +- **gRPC clients** use server reflection to discover services and methods without proto files +- **REST clients** use the interactive Swagger UI or import the OpenAPI spec +- **Operations** use health checks (build metadata), Prometheus metrics, and pprof + +The HTTP annotations in your proto file define both the REST routes and their Swagger documentation simultaneously: + +```protobuf +rpc Echo(EchoRequest) returns (EchoResponse) { + option (google.api.http) = { + post: "/api/v1/example/echo" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Echo endpoint" + description: "Returns the input message unchanged." + tags: "example" + }; +} +``` + +This creates: a gRPC method, a `POST /api/v1/example/echo` REST endpoint, and a documented Swagger UI entry — all from one definition. + +### Type-Safe by Design + +`buf generate` produces typed Go interfaces from your proto service definitions. When you add a new RPC method to your `.proto` file and regenerate, the Go compiler will refuse to build until you implement it — there's no way to forget an endpoint or deploy a half-implemented API. + +``` +myservice.proto buf generate Go compiler +─────────────── ──────────────────────► ───────────────── +rpc Echo(...) EchoServer interface ✓ Implemented +rpc Greet(...) GreetServer interface ✗ Build error until implemented +``` + +This means your proto file is the **contract** — the compiler enforces it, grpc-gateway serves it as REST, and the OpenAPI spec documents it. They can never drift from each other. + ## Overview ColdBrew is a layered framework where each layer is an independent Go module. The `core` package orchestrates everything, but you can use any package standalone. @@ -125,7 +200,7 @@ Interceptors are gRPC middleware that run on every request. ColdBrew chains them | Order | Interceptor | Package | What It Does | |-------|------------|---------|--------------| | 1 | Response Time Logging | `interceptors` | Logs method name, duration, and status code | -| 2 | Trace ID | `interceptors` | Extracts or generates a trace ID and adds it to the context | +| 2 | Trace ID | `interceptors` | Generates a trace ID (or reads it from the `x-trace-id` HTTP header or a `trace_id` proto field) and propagates it to structured logs and Sentry/Rollbar error reports | | 3 | Context Tags | `grpc_ctxtags` | Extracts gRPC metadata into context tags for logging | | 4 | OpenTracing | `grpc_opentracing` | Creates a tracing span for the request | | 5 | Prometheus | `grpc_prometheus` | Records request count, latency histogram, and status codes | diff --git a/config-reference.md b/config-reference.md new file mode 100644 index 0000000..2acf411 --- /dev/null +++ b/config-reference.md @@ -0,0 +1,169 @@ +--- +layout: default +title: "Configuration Reference" +nav_order: 5 +description: "Complete environment variable reference for ColdBrew Go microservice framework configuration" +permalink: /config-reference +--- +# Configuration Reference +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +ColdBrew is configured entirely through environment variables using [envconfig](https://github.com/kelseyhightower/envconfig). All fields have sensible defaults — you can run a service with zero configuration. + +Access the config in code via: + +```go +cfg := config.GetColdBrewConfig() +``` + +## Server + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `LISTEN_HOST` | string | `0.0.0.0` | Host address to listen on | +| `GRPC_PORT` | int | `9090` | gRPC server port | +| `HTTP_PORT` | int | `9091` | HTTP gateway port | +| `APP_NAME` | string | `""` | Application name (used in logs, metrics, New Relic) | +| `ENVIRONMENT` | string | `""` | Environment name (e.g., production, staging, development) | +| `RELEASE_NAME` | string | `""` | Release/version name | + +## Logging + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `LOG_LEVEL` | string | `info` | Log level: debug, info, warn, error | +| `JSON_LOGS` | bool | `true` | Emit logs in JSON format | + +## gRPC Server + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `DISABLE_GRPC_REFLECTION` | bool | `false` | Disable gRPC server reflection (used by tools like grpcurl) | +| `DO_NOT_LOG_GRPC_REFLECTION` | bool | `true` | Suppress logging of gRPC reflection API calls | +| `GRPC_MAX_SEND_MSG_SIZE` | int | `2147483647` | Maximum send message size in bytes (default: ~2GB, unlimited) | +| `GRPC_MAX_RECV_MSG_SIZE` | int | `4194304` | Maximum receive message size in bytes (default: 4MB) | +| `DISABLE_VT_PROTOBUF` | bool | `false` | Disable [vtprotobuf](https://github.com/planetscale/vtprotobuf) marshaller for gRPC. See [vtprotobuf guide](/howto/vtproto) | + +## gRPC TLS + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `GRPC_TLS_KEY_FILE` | string | `""` | Path to TLS private key file. Both key and cert must be set to enable TLS | +| `GRPC_TLS_CERT_FILE` | string | `""` | Path to TLS certificate file. Both key and cert must be set to enable TLS | +| `GRPC_TLS_INSECURE_SKIP_VERIFY` | bool | `false` | Skip TLS certificate verification (development only) | + +## gRPC Keepalive + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `GRPC_SERVER_MAX_CONNECTION_IDLE_IN_SECONDS` | int | `0` | Close idle connections after this duration (0 = disabled) | +| `GRPC_SERVER_MAX_CONNECTION_AGE_IN_SECONDS` | int | `0` | Maximum connection lifetime with ±10% jitter (0 = disabled) | +| `GRPC_SERVER_MAX_CONNECTION_AGE_GRACE_IN_SECONDS` | int | `0` | Grace period after max connection age before force-closing | + +## HTTP Gateway + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `DISABLE_SWAGGER` | bool | `false` | Disable Swagger UI at the swagger URL | +| `SWAGGER_URL` | string | `/swagger/` | URL path for Swagger UI | +| `DISABLE_DEBUG` | bool | `false` | Disable pprof debug endpoints at `/debug/` | +| `USE_JSON_BUILTIN_MARSHALLER` | bool | `false` | Use `encoding/json` instead of the default protojson marshaller for `application/json` | +| `JSON_BUILTIN_MARSHALLER_MIME` | string | `application/json` | Content-Type for the JSON builtin marshaller | +| `HTTP_HEADER_PREFIXES` | []string | `""` | HTTP header prefixes to forward as gRPC metadata (comma-separated) | +| `TRACE_HEADER_NAME` | string | `x-trace-id` | HTTP header name for trace ID propagation to log/trace contexts | + +## Prometheus Metrics + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `DISABLE_PROMETHEUS` | bool | `false` | Disable Prometheus metrics endpoint at `/metrics` | +| `ENABLE_PROMETHEUS_GRPC_HISTOGRAM` | bool | `true` | Enable gRPC request latency histograms | +| `PROMETHEUS_GRPC_HISTOGRAM_BUCKETS` | []float64 | `""` | Custom histogram buckets (comma-separated seconds, e.g., `0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10`) | + +## New Relic + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `NEW_RELIC_LICENSE_KEY` | string | `""` | New Relic license key (required to enable New Relic) | +| `NEW_RELIC_APPNAME` | string | `""` | Application name in New Relic | +| `DISABLE_NEW_RELIC` | bool | `false` | Disable all New Relic reporting | +| `NEW_RELIC_DISTRIBUTED_TRACING` | bool | `true` | Enable New Relic distributed tracing | +| `NEW_RELIC_OPENTELEMETRY` | bool | `true` | Enable New Relic via OpenTelemetry | +| `NEW_RELIC_OPENTELEMETRY_SAMPLE` | float64 | `0.2` | Trace sampling ratio for New Relic OpenTelemetry (0.0–1.0) | + +## OpenTelemetry (OTLP) + +{: .note } +When `OTLP_ENDPOINT` is set, it takes precedence over New Relic OpenTelemetry configuration. + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `OTLP_ENDPOINT` | string | `""` | OTLP gRPC endpoint (e.g., `localhost:4317`, `api.honeycomb.io:443`) | +| `OTLP_HEADERS` | string | `""` | Custom headers as `key=value` pairs (comma-separated, e.g., `x-honeycomb-team=your-key`) | +| `OTLP_COMPRESSION` | string | `gzip` | Compression type: `gzip` or `none` | +| `OTLP_INSECURE` | bool | `false` | Disable TLS for OTLP connection (development only) | +| `OTLP_SAMPLING_RATIO` | float64 | `0.2` | Trace sampling ratio (0.0–1.0, where 1.0 = sample all) | +| `OTLP_USE_OPENTRACING_BRIDGE` | bool | `true` | Enable OpenTracing compatibility bridge for existing instrumentation | + +## Error Tracking + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `SENTRY_DSN` | string | `""` | Sentry DSN for error notification | + +## Graceful Shutdown + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `DISABLE_SIGNAL_HANDLER` | bool | `false` | Disable ColdBrew's SIGINT/SIGTERM handler | +| `SHUTDOWN_DURATION_IN_SECONDS` | int | `15` | Time to wait for in-flight requests to complete before forced shutdown | +| `GRPC_GRACEFUL_DURATION_IN_SECONDS` | int | `7` | Time to wait for healthcheck failure to propagate before initiating shutdown. Should be less than `SHUTDOWN_DURATION_IN_SECONDS` | + +## Runtime + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `DISABLE_AUTO_MAX_PROCS` | bool | `false` | Disable automatic GOMAXPROCS tuning (useful if your container runtime already sets it) | + +## Deprecated + +| Variable | Replacement | Notes | +|----------|------------|-------| +| `HTTP_HEADER_PREFIX` | `HTTP_HEADER_PREFIXES` | Single prefix replaced by comma-separated list | + +--- + +## Example: Minimal Production Configuration + +```bash +export APP_NAME=myservice +export ENVIRONMENT=production +export LOG_LEVEL=info +export NEW_RELIC_LICENSE_KEY=your-key +export NEW_RELIC_APPNAME=myservice +export SENTRY_DSN=https://your-dsn@sentry.io/123 +``` + +## Example: Local Development with Jaeger + +```bash +export APP_NAME=myservice +export ENVIRONMENT=development +export LOG_LEVEL=debug +export OTLP_ENDPOINT=localhost:4317 +export OTLP_INSECURE=true +export OTLP_SAMPLING_RATIO=1.0 +export DISABLE_NEW_RELIC=true +``` + +--- + +Source: [`core/config/config.go`](https://github.com/go-coldbrew/core/blob/main/config/config.go) diff --git a/howto/APIs.md b/howto/APIs.md index 4850e1c..850a485 100644 --- a/howto/APIs.md +++ b/howto/APIs.md @@ -2,6 +2,7 @@ layout: default title: "Building and Configuring APIs" parent: "How To" +description: "Build gRPC and REST APIs with ColdBrew using protobuf definitions and grpc-gateway HTTP annotations" --- ## Table of contents {: .no_toc .text-delta } @@ -543,5 +544,5 @@ For more advanced customization options, refer to the [grpc-gateway customizatio [grpc-gateway]: https://grpc-ecosystem.github.io/grpc-gateway/ [gRPC Gateway mapping]: https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/examples/ [grpc-gateway plugin]: https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/generating_stubs/ -[ColdBrew cookiecutter]: /getting-started#using-the-coldbrew-cookiecutter-template +[ColdBrew cookiecutter]: /cookiecutter-reference#using-the-coldbrew-cookiecutter-template [grpc-gateway customization guide]: https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/ diff --git a/howto/Debugging.md b/howto/Debugging.md index 16b8432..636e79c 100644 --- a/howto/Debugging.md +++ b/howto/Debugging.md @@ -58,6 +58,92 @@ ColdBrew provides a way to override the log level of a request based on the requ For information on this feature, please refer to the [Overriding log level at request time] page. +## Debugging with Delve + +[Delve](https://github.com/go-delve/delve) is the standard Go debugger. To debug a ColdBrew service: + +```bash +# Install delve +go install github.com/go-delve/delve/cmd/dlv@latest + +# Run your service under delve +dlv debug . -- [flags] + +# Or attach to a running process +dlv attach $(pgrep myservice) +``` + +### Useful breakpoint locations + +When debugging ColdBrew services, these are good places to set breakpoints: + +- **Your handler**: `break service/service.go:42` — your gRPC method implementation +- **Interceptor chain entry**: `break github.com/go-coldbrew/interceptors.UnaryServerInterceptor` — see what interceptors fire +- **Error notification**: `break github.com/go-coldbrew/errors/notifier.Notify` — catch when errors are sent to Sentry/Rollbar + +### VS Code / GoLand + +Both IDEs support Delve natively. Configure Delve to listen on its own port (for example, set `"host": "0.0.0.0", "port": 2345` in your launch.json) and keep this distinct from your service ports (gRPC on 9090, HTTP on 9091) to avoid conflicts. + +## gRPC debugging environment variables + +Go's gRPC library has built-in debug logging. These environment variables are useful when troubleshooting connectivity or protocol issues: + +```bash +# Enable gRPC internal logging (WARNING: very verbose) +export GRPC_GO_LOG_VERBOSITY_LEVEL=99 +export GRPC_GO_LOG_SEVERITY_LEVEL=info +``` + +This will print detailed gRPC transport and connection state information to stderr. Useful for diagnosing: +- Connection establishment failures +- TLS handshake issues +- Load balancer resolution problems +- Keepalive/ping timeouts + +{: .warning } +Do not enable verbose gRPC logging in production — it generates enormous log volume and may impact performance. + +## Inspecting the interceptor chain + +ColdBrew chains interceptors in a specific order. If you're not sure what's running, you can inspect the chain at startup by setting `LOG_LEVEL=debug`: + +```bash +LOG_LEVEL=debug make run +``` + +The server interceptor chain runs in this order: +1. Response time logging +2. Trace ID injection +3. Context tags +4. OpenTracing/OpenTelemetry +5. Prometheus metrics +6. Error notification +7. NewRelic +8. Panic recovery + +If a request is failing or behaving unexpectedly, check whether an interceptor is modifying the context or returning early. The response time logging interceptor logs every request with method name and duration — check these logs first. + +## Common error patterns + +### "transport is closing" +Usually means the client connection was closed before the response arrived. Check: +- `SHUTDOWN_DURATION_IN_SECONDS` is long enough for your slowest requests +- Client-side timeouts match server-side processing time +- Load balancer idle timeout isn't shorter than your keepalive settings + +### "context deadline exceeded" +The request's context expired. This propagates through the interceptor chain. Check: +- Client-side deadline/timeout settings +- Whether a downstream dependency (database, external API) is slow +- Circuit breaker state via Prometheus metrics + +### Metrics endpoint returns 404 +Prometheus is disabled. Check `DISABLE_PROMETHEUS` environment variable (should be `false` or unset). + +### Health check returns error +The service hasn't called `SetReady()` yet. This typically happens during startup while dependencies are initializing. Check your service's `InitGRPC` method. + --- [configuration option]: https://pkg.go.dev/github.com/go-coldbrew/core/config#Config [Overriding log level at request time]: /howto/Log/#overriding-log-level-at-request-time diff --git a/howto/Log.md b/howto/Log.md index 2e5f266..dd2f980 100644 --- a/howto/Log.md +++ b/howto/Log.md @@ -139,7 +139,7 @@ Will output the debug log messages even when the global log level is set to info --- [TraceId interceptor]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#TraceIdInterceptor [go-coldbrew/tracing]: https://pkg.go.dev/github.com/go-coldbrew/tracing -[ColdBrew cookiecutter]: /getting-started +[ColdBrew cookiecutter]: /cookiecutter-reference [interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors [UseColdBrewServcerInterceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#UseColdBrewServerInterceptors [OverrideLogLevel]: https://github.com/go-coldbrew/log#func-overrideloglevel diff --git a/howto/Tracing.md b/howto/Tracing.md index 2663397..00d6770 100644 --- a/howto/Tracing.md +++ b/howto/Tracing.md @@ -2,6 +2,7 @@ layout: default title: "Tracing" parent: "How To" +description: "Set up distributed tracing in ColdBrew with OpenTelemetry, Jaeger, and New Relic for gRPC services" --- ## Table of contents {: .no_toc .text-delta } @@ -147,11 +148,60 @@ ctx := tracing.MergeContextValues(parentCtx, mainCtx) {: .warning} The functions `CloneContextValues` and `MergeParentContext` are deprecated. Use [NewContextWithParentValues] and [MergeContextValues] instead. +## Trace ID Propagation + +ColdBrew automatically generates a unique trace ID for every request and propagates it across your observability stack. There are two ways a trace ID can enter the system: + +### 1. HTTP header (default: `x-trace-id`) + +When a request arrives via the HTTP gateway, ColdBrew reads the `x-trace-id` header (configurable via `TRACE_HEADER_NAME`) and forwards it as gRPC metadata. The `ServerErrorInterceptor` then injects it into the context. + +```bash +# Pass a trace ID from the client +curl -H "x-trace-id: req-abc-123" localhost:9091/api/v1/echo -d '{"msg":"hello"}' +``` + +If no header is provided, ColdBrew generates a random trace ID automatically. + +### 2. Proto field (`trace_id`) + +If your request proto message has a `trace_id` field, the [TraceId interceptor] reads it automatically: + +```protobuf +message EchoRequest { + string msg = 1; + string trace_id = 2; // ColdBrew reads this automatically +} +``` + +The generated `GetTraceId()` (or `GetTraceID()`) method is detected via interface assertion — no registration needed. If both the HTTP header and proto field are present, the proto field takes precedence since it runs later in the interceptor chain. + +### Where the trace ID appears + +Once extracted, the trace ID is propagated to: + +| Destination | How | Example | +|-------------|-----|---------| +| **Structured logs** | Added as `"trace"` field via log context | `{"level":"info","msg":"handled request","trace":"req-abc-123"}` | +| **Sentry / Rollbar / Airbrake** | Attached to error notifications as a tag | Visible in the error report for correlation | +| **Request context** | Stored in ColdBrew options | Accessible via `notifier.GetTraceId(ctx)` in your handler code | + +{: .note } +ColdBrew's trace ID is separate from OpenTelemetry's W3C trace context. OpenTelemetry spans have their own trace/span IDs managed by the tracing SDK. ColdBrew's trace ID is a lightweight request correlation ID for logs and error reports — it can also read from an existing OpenTracing span's `"trace"` baggage item if one exists. + +This means a single trace ID connects your logs and error reports — you can search for `req-abc-123` in your log aggregator and Sentry to find the complete request flow. + +### Customizing the header name + +```bash +export TRACE_HEADER_NAME=x-request-id # Use a different header +``` + --- [TraceId interceptor]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#TraceIdInterceptor [go-coldbrew/tracing]: https://pkg.go.dev/github.com/go-coldbrew/tracing -[ColdBrew cookiecutter]: /getting-started +[ColdBrew cookiecutter]: /cookiecutter-reference [interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors [UseColdBrewServcerInterceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#UseColdBrewServerInterceptors [Default Client Interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#DefaultClientInterceptors diff --git a/howto/data-builder.md b/howto/data-builder.md index d3774e7..9ecac5b 100644 --- a/howto/data-builder.md +++ b/howto/data-builder.md @@ -188,6 +188,147 @@ result, err := p.RunParallel( {: .important} Parallel execution is beneficial for I/O-bound builders (network calls, database queries). For CPU-bound operations, the overhead may outweigh the benefits. +### Visualizing the dependency graph + +After compiling a plan, you can generate a visual representation of the execution graph using `BuildGraph`: + +```go +err := p.BuildGraph(context.Background(), "svg", "dependency-graph.svg") +if err != nil { + log.Fatal(err) +} +``` + +Supported formats include `svg`, `png`, and `dot` (Graphviz). This is useful for: +- Verifying that dependencies are resolved as expected +- Identifying opportunities for parallelism (independent branches in the graph can run concurrently) +- Documentation and onboarding + +You can also use the standalone function if you have a `Plan` interface: + +```go +builder.BuildGraph(myPlan, "svg", "graph.svg") +``` + +{: .note } +Graph generation requires [Graphviz](https://graphviz.org/) to be installed on your system (`brew install graphviz` or `apt-get install graphviz`). + +## Error handling + +### Builder function errors + +When a builder function returns an error, execution stops for any functions that depend on its output. Other independent branches continue executing. + +```go +func BuildGrossPrice(_ context.Context, req AppRequest) (GrossPrice, error) { + if len(req.Cart) == 0 { + return GrossPrice{}, fmt.Errorf("cart is empty") + } + // ... +} +``` + +When running with `RunParallel`, if multiple builders fail, their errors are joined into a single error. You can unwrap individual errors using `errors.Is` or `errors.As`. + +### Compile-time validation + +The `Compile` method catches structural errors before runtime: +- **Missing dependencies**: A builder requires a type that no other builder produces and wasn't provided as input +- **Circular dependencies**: Builder A depends on B, and B depends on A (directly or transitively) +- **Duplicate outputs**: Two builders produce the same output type + +Always compile plans in `init()` so these errors surface at startup, not at request time: + +```go +func init() { + p, err = b.Compile(AppRequest{}) + if err != nil { + panic(err) // Fail fast — don't serve requests with a broken plan + } +} +``` + +## Testing data-builder plans + +### Unit testing individual builders + +Test each builder function in isolation — they're just regular Go functions: + +```go +func TestBuildGrossPrice(t *testing.T) { + req := AppRequest{ + Cart: []Item{ + {Name: "item1", PriceInCents: 1000}, + {Name: "item2", PriceInCents: 2000}, + }, + } + price, err := BuildGrossPrice(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if price.InCents != 3000 { + t.Errorf("expected 3000, got %d", price.InCents) + } +} +``` + +### Integration testing with Replace + +Use `Replace` to swap specific builders with mocks while keeping the rest of the plan intact: + +```go +func TestPlanWithMockDiscount(t *testing.T) { + // Compile a fresh plan for this test to avoid shared mutable state + b := builder.New() + err := b.AddBuilders(BuildGrossPrice, BuildPriceAdjustment, BuildAppResponse) + if err != nil { + t.Fatal(err) + } + testPlan, err := b.Compile(AppRequest{}) + if err != nil { + t.Fatal(err) + } + + // Replace one builder with a mock + err = testPlan.Replace(context.Background(), BuildPriceAdjustment, func(_ context.Context, gp GrossPrice) (PriceAdjustment, error) { + return PriceAdjustment{DiscountInCents: 500}, nil + }) + if err != nil { + t.Fatal(err) + } + + result, err := testPlan.Run(context.Background(), AppRequest{ + Cart: []Item{{Name: "item1", PriceInCents: 1000}}, + }) + if err != nil { + t.Fatal(err) + } + resp := result.Get(AppResponse{}).(AppResponse) + if resp.PriceInDollars != 5.0 { + t.Errorf("expected 5.0, got %f", resp.PriceInDollars) + } +} +``` + +## Common pitfalls + +### Circular dependencies +If builder A needs the output of B and B needs the output of A, `Compile` will return an error. Break the cycle by introducing an intermediate type or restructuring your builders. + +### Missing input types +If you forget to pass an initial input type to `Compile`, any builder that depends on it (directly or transitively) will cause a compile error. Make sure all "root" types are listed: + +```go +// Wrong: missing UserProfile that some builders depend on +p, err = b.Compile(AppRequest{}) + +// Right: provide all root inputs +p, err = b.Compile(AppRequest{}, UserProfile{}) +``` + +### Type identity matters +Two structs with identical fields but different names are different types. `data-builder` uses Go's type system for dependency resolution — `type Price struct{ V int }` and `type Cost struct{ V int }` are distinct. + --- [data-builder]: https://pkg.go.dev/github.com/go-coldbrew/data-builder [guide to go generate]: https://eli.thegreenplace.net/2021/a-comprehensive-guide-to-go-generate/ diff --git a/howto/gRPC.md b/howto/gRPC.md index 1a8afbe..988f94b 100644 --- a/howto/gRPC.md +++ b/howto/gRPC.md @@ -130,7 +130,7 @@ func main() { ``` --- -[ColdBrew cookiecutter]: /getting-started +[ColdBrew cookiecutter]: /cookiecutter-reference [Building and Configuring APIs]: /howto/APIs [grpcpool]: https://pkg.go.dev/github.com/go-coldbrew/grpcpool [grpcpool.Dial]: https://pkg.go.dev/github.com/go-coldbrew/grpcpool#Dial diff --git a/howto/index.md b/howto/index.md index ef701e4..2d675ea 100644 --- a/howto/index.md +++ b/howto/index.md @@ -2,7 +2,7 @@ layout: default title: How To nav_order: 6 -description: "A collection of How To guides for ColdBrew" +description: "Step-by-step guides for logging, tracing, metrics, error handling, APIs, and debugging in ColdBrew Go services" permalink: /howto has_children: true has_toc: true diff --git a/howto/interceptors.md b/howto/interceptors.md index 9f4b716..f955fea 100644 --- a/howto/interceptors.md +++ b/howto/interceptors.md @@ -131,7 +131,7 @@ Use the function [AddUnaryServerInterceptor] and [AddUnaryClientInterceptor] to [TraceId interceptor]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#TraceIdInterceptor [go-coldbrew/tracing]: https://pkg.go.dev/github.com/go-coldbrew/tracing -[ColdBrew cookiecutter]: /getting-started +[ColdBrew cookiecutter]: /cookiecutter-reference [interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors [UseColdBrewServcerInterceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#UseColdBrewServerInterceptors [Default Client Interceptors]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#DefaultClientInterceptors diff --git a/howto/production.md b/howto/production.md new file mode 100644 index 0000000..176ece8 --- /dev/null +++ b/howto/production.md @@ -0,0 +1,366 @@ +--- +layout: default +title: "Production Deployment" +parent: "How To" +description: "Deploy ColdBrew Go services to production with Kubernetes manifests, health probes, Prometheus, and graceful shutdown" +--- +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +## Overview + +This guide covers deploying ColdBrew services to production on Kubernetes. ColdBrew is designed for containerized environments — health checks, metrics, and graceful shutdown work out of the box. + +## Docker image + +The [ColdBrew cookiecutter] generates a multi-stage Dockerfile: + +```dockerfile +# Build stage +FROM golang:1.25 AS builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o /service . + +# Runtime stage +FROM alpine:latest +RUN apk --no-cache add ca-certificates +COPY --from=builder /service /service +EXPOSE 9090 9091 +ENTRYPOINT ["/service"] +``` + +Key points: +- `CGO_ENABLED=0` produces a static binary — no libc dependency +- `ca-certificates` is needed for TLS connections to external services (New Relic, Sentry, OTLP endpoints) +- Ports 9090 (gRPC) and 9091 (HTTP) are the defaults + +Build and push: + +```bash +docker build -t your-registry/myservice:v1.0.0 . +docker push your-registry/myservice:v1.0.0 +``` + +## Kubernetes Deployment + +### Basic Deployment + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myservice + labels: + app: myservice +spec: + replicas: 3 + selector: + matchLabels: + app: myservice + template: + metadata: + labels: + app: myservice + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9091" + prometheus.io/path: "/metrics" + spec: + terminationGracePeriodSeconds: 30 + containers: + - name: myservice + image: your-registry/myservice:v1.0.0 + ports: + - name: grpc + containerPort: 9090 + protocol: TCP + - name: http + containerPort: 9091 + protocol: TCP + env: + - name: APP_NAME + value: myservice + - name: ENVIRONMENT + value: production + - name: LOG_LEVEL + value: info + envFrom: + - secretRef: + name: myservice-secrets + livenessProbe: + httpGet: + path: /healthcheck + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + readinessProbe: + httpGet: + path: /readycheck + port: http + initialDelaySeconds: 3 + periodSeconds: 5 + timeoutSeconds: 3 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +``` + +### Secrets + +Store sensitive values like API keys in a Kubernetes Secret: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: myservice-secrets +type: Opaque +stringData: + NEW_RELIC_LICENSE_KEY: "your-license-key" + SENTRY_DSN: "https://your-dsn@sentry.io/123" +``` + +### Service + +Expose both gRPC and HTTP ports: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: myservice + labels: + app: myservice +spec: + selector: + app: myservice + ports: + - name: grpc + port: 9090 + targetPort: grpc + protocol: TCP + - name: http + port: 9091 + targetPort: http + protocol: TCP +``` + +## Health probes + +ColdBrew provides two health endpoints: + +| Endpoint | Purpose | Kubernetes probe | +|----------|---------|------------------| +| `/healthcheck` | Liveness — is the process alive? | `livenessProbe` | +| `/readycheck` | Readiness — can it accept traffic? | `readinessProbe` | + +Both return JSON with build/version info on success. During graceful shutdown, `/readycheck` fails first, which causes Kubernetes to stop routing traffic before the process exits. + +{: .important } +Set `terminationGracePeriodSeconds` to at least `SHUTDOWN_DURATION_IN_SECONDS` + `GRPC_GRACEFUL_DURATION_IN_SECONDS` to avoid SIGKILL during shutdown. With defaults (15 + 7 = 22), a value of 30 provides a safe buffer. + +## Graceful shutdown tuning + +ColdBrew's shutdown sequence: + +1. Receive SIGTERM from Kubernetes +2. Fail `/readycheck` immediately +3. Wait `GRPC_GRACEFUL_DURATION_IN_SECONDS` (default: 7s) for the load balancer to drain +4. Stop accepting new requests +5. Wait `SHUTDOWN_DURATION_IN_SECONDS` (default: 15s) for in-flight requests to complete +6. Call `Stop()` if your service implements `CBStopper` +7. Exit + +Tune these values based on your service: + +```yaml +env: + # If your longest request takes 30s, set shutdown duration accordingly + - name: SHUTDOWN_DURATION_IN_SECONDS + value: "35" + # Match your load balancer's health check interval + propagation time + - name: GRPC_GRACEFUL_DURATION_IN_SECONDS + value: "10" +``` + +For more details, see [Signal Handling and Graceful Shutdown](/howto/signals). + +## Prometheus monitoring + +### Prometheus ServiceMonitor + +If you're using the [Prometheus Operator](https://prometheus-operator.dev/): + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: myservice + labels: + app: myservice +spec: + selector: + matchLabels: + app: myservice + endpoints: + - port: http + path: /metrics + interval: 15s +``` + +### Key metrics to alert on + +ColdBrew exposes these metrics out of the box via gRPC interceptors: + +| Metric | Type | Description | +|--------|------|-------------| +| `grpc_server_handled_total` | Counter | Total RPCs completed, by method and status code | +| `grpc_server_handling_seconds` | Histogram | RPC latency distribution (if `ENABLE_PROMETHEUS_GRPC_HISTOGRAM=true`) | +| `grpc_server_started_total` | Counter | Total RPCs started | + +Recommended alerts: + +```yaml +# High error rate +- alert: HighGRPCErrorRate + expr: | + sum(rate(grpc_server_handled_total{grpc_code!="OK"}[5m])) by (grpc_service) + / + sum(rate(grpc_server_handled_total[5m])) by (grpc_service) + > 0.05 + for: 5m + +# High latency (p99 > 1s) +- alert: HighGRPCLatency + expr: | + histogram_quantile(0.99, + sum(rate(grpc_server_handling_seconds_bucket[5m])) by (le, grpc_service) + ) > 1 + for: 5m +``` + +### Custom histogram buckets + +If the default latency buckets don't match your SLOs, customize them: + +```yaml +env: + - name: PROMETHEUS_GRPC_HISTOGRAM_BUCKETS + value: "0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10" +``` + +## gRPC load balancing + +gRPC uses HTTP/2 with long-lived connections. A standard Kubernetes Service with `ClusterIP` won't distribute load across pods — all requests go over a single connection to one pod. + +Solutions: + +### Option 1: Headless Service + client-side balancing + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: myservice-headless +spec: + clusterIP: None # headless + selector: + app: myservice + ports: + - name: grpc + port: 9090 +``` + +Use with ColdBrew's [grpcpool](https://pkg.go.dev/github.com/go-coldbrew/grpcpool) for client-side round-robin: + +```go +conn, err := grpcpool.DialContext(ctx, "dns:///myservice-headless:9090", + grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`), +) +``` + +### Option 2: Service mesh / L7 proxy + +Use a gRPC-aware proxy (Istio, Linkerd, Envoy) that understands HTTP/2 multiplexing and balances per-request rather than per-connection. + +## TLS + +Enable TLS on the gRPC server: + +```yaml +env: + - name: GRPC_TLS_CERT_FILE + value: /certs/tls.crt + - name: GRPC_TLS_KEY_FILE + value: /certs/tls.key +volumeMounts: + - name: tls-certs + mountPath: /certs + readOnly: true +volumes: + - name: tls-certs + secret: + secretName: myservice-tls +``` + +{: .note } +If you're using a service mesh that handles mTLS (Istio, Linkerd), you typically don't need ColdBrew's built-in TLS — the mesh sidecar terminates TLS at the pod level. + +## Resource tuning + +### GOMAXPROCS + +ColdBrew automatically sets `GOMAXPROCS` to match the container's CPU limit using [automaxprocs](https://github.com/uber-go/automaxprocs). This prevents the Go runtime from spawning more OS threads than the container has CPU quota. + +If your container runtime already handles this (e.g., via `cgroup`-aware runtimes), disable it: + +```yaml +env: + - name: DISABLE_AUTO_MAX_PROCS + value: "true" +``` + +### Connection keepalive + +For services behind load balancers with idle connection timeouts, configure keepalive: + +```yaml +env: + # Close connections idle for more than 5 minutes + - name: GRPC_SERVER_MAX_CONNECTION_IDLE_IN_SECONDS + value: "300" + # Force connection refresh every 30 minutes (with ±10% jitter) + - name: GRPC_SERVER_MAX_CONNECTION_AGE_IN_SECONDS + value: "1800" + # Allow 30s grace period for in-flight RPCs on aged connections + - name: GRPC_SERVER_MAX_CONNECTION_AGE_GRACE_IN_SECONDS + value: "30" +``` + +## Production checklist + +- [ ] Set `APP_NAME` and `ENVIRONMENT` for log/metric identification +- [ ] Configure `livenessProbe` on `/healthcheck` and `readinessProbe` on `/readycheck` +- [ ] Set `terminationGracePeriodSeconds` ≥ shutdown + healthcheck wait duration +- [ ] Enable Prometheus scraping (annotation or ServiceMonitor) +- [ ] Set up error tracking (`SENTRY_DSN` or equivalent) +- [ ] Configure tracing (`OTLP_ENDPOINT` or `NEW_RELIC_LICENSE_KEY`) +- [ ] Use headless Service or L7 proxy for gRPC load balancing +- [ ] Set resource requests and limits +- [ ] Store secrets in Kubernetes Secrets, not environment variable literals +- [ ] Disable debug endpoints in production if not needed (`DISABLE_DEBUG=true`) +- [ ] Run `make lint` (includes `govulncheck`) before deploying + +--- +[ColdBrew cookiecutter]: /cookiecutter-reference diff --git a/howto/signals.md b/howto/signals.md index dc9a43a..351a518 100644 --- a/howto/signals.md +++ b/howto/signals.md @@ -60,7 +60,7 @@ If you want to avoid this, you can set the `SHUTDOWN_DURATION_IN_SECONDS` to a v Make sure you configure your load balancer to stop sending new requests to your application after readiness check fails. This will ensure that no new requests are sent to your application when it is shutting down. --- -[ColdBrew cookiecutter]: /getting-started#using-the-coldbrew-cookiecutter-template +[ColdBrew cookiecutter]: /cookiecutter-reference#using-the-coldbrew-cookiecutter-template [go-coldbrew/core]: https://pkg.go.dev/github.com/go-coldbrew/core [config]: https://pkg.go.dev/github.com/go-coldbrew/core/config#Config [CBStopper]: https://pkg.go.dev/github.com/go-coldbrew/core#CBStopper diff --git a/howto/swagger.md b/howto/swagger.md index aa535bd..bd70296 100644 --- a/howto/swagger.md +++ b/howto/swagger.md @@ -84,4 +84,4 @@ func main() { [Config]: https://pkg.go.dev/github.com/go-coldbrew/core/config#Config [SetOpenAPIHandler]: https://pkg.go.dev/github.com/go-coldbrew/core#CB [grpc-gateway's Open API specification]: https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_openapi_output/ -[ColdBrew cookiecutter]: /getting-started +[ColdBrew cookiecutter]: /cookiecutter-reference diff --git a/howto/vtproto.md b/howto/vtproto.md new file mode 100644 index 0000000..e697337 --- /dev/null +++ b/howto/vtproto.md @@ -0,0 +1,170 @@ +--- +layout: default +title: "VTProtobuf (Fast Serialization)" +parent: "How To" +description: "How ColdBrew uses vtprotobuf for faster gRPC serialization with automatic fallback to standard protobuf" +--- +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +## Overview + +ColdBrew uses [vtprotobuf](https://github.com/planetscale/vtprotobuf) (by PlanetScale) as the default gRPC serialization codec. vtprotobuf generates optimized `MarshalVT()` and `UnmarshalVT()` methods that are significantly faster than the standard `proto.Marshal()` — typically **2–3x faster** with fewer allocations. + +This is enabled by default. You don't need to do anything to benefit from it — if your proto messages have VT methods generated, ColdBrew uses them automatically. + +## How it works + +At startup, ColdBrew registers a custom gRPC codec that replaces the default protobuf serializer. The codec uses a **three-level fallback chain** so it's backward compatible with any proto message: + +``` +Marshal: + 1. vtprotoMessage → MarshalVT() (fastest, if VT methods exist) + 2. proto.Message → proto.Marshal() (standard protobuf v2) + 3. protov1.Message → protov1.Marshal() (legacy protobuf v1) + +Unmarshal: + 1. vtprotoMessage → UnmarshalVT() + 2. proto.Message → proto.Unmarshal() + 3. protov1.Message → protov1.Unmarshal() +``` + +This means: +- Messages with VT methods get the fast path automatically +- Messages without VT methods (e.g., from third-party libraries) still work via standard protobuf +- No code changes needed — the codec switch is transparent + +The codec also includes panic recovery with async error notification, so a serialization bug won't crash your service silently. + +{: .note } +The vtproto codec only affects **gRPC wire protocol** serialization. The HTTP/JSON gateway uses grpc-gateway's own marshallers (`ProtoMarshaller` or `JSONBuiltin`) independently — they are not affected by this setting. + +## Code generation setup + +The [ColdBrew cookiecutter] template includes vtprotobuf in `buf.gen.yaml` out of the box: + +```yaml +- remote: buf.build/community/planetscale-vtprotobuf:v0.6.0 + out: proto + opt: paths=source_relative,features=marshal+unmarshal+size+clone+pool+equal +``` + +This generates the following methods on every proto message: + +| Feature | Generated Method | Use Case | +|---------|-----------------|----------| +| `marshal` | `MarshalVT()` | Fast serialization (used by gRPC codec) | +| `unmarshal` | `UnmarshalVT()` | Fast deserialization (used by gRPC codec) | +| `size` | `SizeVT()` | Pre-calculate serialized size without allocating | +| `clone` | `CloneVT()` | Deep copy a message efficiently | +| `pool` | `ReturnToVTPool()` | Object pooling to reduce GC pressure | +| `equal` | `EqualVT()` | Fast message comparison | + +After running `make generate` (or `buf generate`), your proto files will have `*_vtproto.pb.go` files alongside the standard `*.pb.go` files. + +### Adding vtprotobuf to an existing project + +If you're not using the cookiecutter template, add the vtprotobuf plugin to your `buf.gen.yaml`: + +```yaml +plugins: + # ... your existing plugins ... + - remote: buf.build/community/planetscale-vtprotobuf:v0.6.0 + out: proto + opt: paths=source_relative,features=marshal+unmarshal+size+clone+pool+equal +``` + +Then add the dependency to your `go.mod`: + +```bash +go get github.com/planetscale/vtprotobuf +``` + +Regenerate your proto code: + +```bash +buf generate +``` + +That's it — ColdBrew's codec will automatically detect and use the VT methods on your messages. + +## Using VT features in your code + +Beyond the automatic marshal/unmarshal speedup, you can use the generated methods directly: + +### Cloning messages + +```go +// Deep copy without reflection +original := &pb.MyMessage{Name: "test", Items: []*pb.Item{{Id: 1}}} +cloned := original.CloneVT() +// Modify cloned without affecting original +cloned.Items[0].Id = 2 +``` + +### Comparing messages + +```go +// Fast equality check +if msg1.EqualVT(msg2) { + // messages are identical +} +``` + +### Object pooling + +For high-throughput services, object pooling reduces GC pressure: + +```go +msg := pb.MyMessageFromVTPool() +// ... use msg ... +msg.ReturnToVTPool() +``` + +{: .warning } +Only use pooling when you're sure the message won't be accessed after returning it to the pool. This is an advanced optimization — the marshal/unmarshal speedup alone is usually sufficient. + +### Pre-calculating size + +```go +// Useful for capacity planning or pre-allocating buffers +size := msg.SizeVT() +buf := make([]byte, 0, size) +``` + +## Disabling vtprotobuf + +To fall back to standard protobuf marshalling: + +```bash +export DISABLE_VT_PROTOBUF=true +``` + +You might want to disable it when: +- **Debugging serialization issues** — to isolate whether a bug is in vtprotobuf or your proto definitions +- **Compatibility testing** — to verify your service works with standard protobuf +- **Profiling** — to measure the actual performance difference in your workload + +{: .note } +Disabling vtprotobuf only affects the gRPC codec. The generated `*_vtproto.pb.go` files remain in your codebase and the VT methods are still available for direct use (e.g., `CloneVT()`). + +## How the codec is registered + +For those curious about the internals, ColdBrew registers the codec during server initialization: + +```go +// core/initializers.go +func InitializeVTProto() { + encoding.RegisterCodec(vtprotoCodec{}) +} +``` + +This is called from `processConfig()` in `core/core.go` when `DisableVTProtobuf` is `false` (the default). The codec registers itself with the name `"proto"`, replacing gRPC's default protobuf codec globally for the process. + +--- +[vtprotobuf]: https://github.com/planetscale/vtprotobuf +[ColdBrew cookiecutter]: /cookiecutter-reference +[Configuration Reference]: /config-reference diff --git a/integrations.md b/integrations.md index 0f63f6a..72f4148 100644 --- a/integrations.md +++ b/integrations.md @@ -255,6 +255,36 @@ func main() { } ``` +## vtprotobuf + +[vtprotobuf] is a Protocol Buffers compiler that generates optimized marshalling code, created by [PlanetScale](https://planetscale.com). ColdBrew uses it as the default gRPC serialization codec for faster message encoding/decoding. + +### How ColdBrew uses it + +ColdBrew registers a custom gRPC codec at startup that prioritizes vtprotobuf's `MarshalVT()`/`UnmarshalVT()` methods, falling back to standard protobuf for messages that don't have VT methods generated. This is transparent — you get the speed benefit without changing any application code. + +### Setup + +The [ColdBrew cookiecutter] template includes vtprotobuf code generation in `buf.gen.yaml`: + +```yaml +- remote: buf.build/community/planetscale-vtprotobuf:v0.6.0 + out: proto + opt: paths=source_relative,features=marshal+unmarshal+size+clone+pool+equal +``` + +After `make generate`, your proto messages will have additional `*_vtproto.pb.go` files with fast `MarshalVT()`, `UnmarshalVT()`, `CloneVT()`, `EqualVT()`, and pool methods. + +### Configuration + +vtprotobuf is enabled by default. To disable it (e.g., for debugging serialization issues): + +```bash +export DISABLE_VT_PROTOBUF=true +``` + +For more details, see the [vtprotobuf how-to guide](/howto/vtproto). + ## ColdBrew packages All ColdBrew packages are designed to be used as standalone packages. They can be used in any Go project. They are not tied to ColdBrew and can be used in any Go project. @@ -267,7 +297,7 @@ To see all the ColdBrew packages, check out the [ColdBrew packages] page. [GRPC Gateway]: https://grpc-ecosystem.github.io/grpc-gateway/ [gRPC Gateway example]: /howto/APIs/#adding-a-new-api-to-your-service [Buf]: https://buf.build/ -[ColdBrew cookiecutter]: /getting-started#using-the-coldbrew-cookiecutter-template +[ColdBrew cookiecutter]: /cookiecutter-reference#using-the-coldbrew-cookiecutter-template [Prometheus]: https://prometheus.io/ [metrics documentation]: /howto/Metrics/ [New Relic]: https://newrelic.com/ @@ -295,4 +325,5 @@ To see all the ColdBrew packages, check out the [ColdBrew packages] page. [failsafe-go]: https://github.com/failsafe-go/failsafe-go [SetupEnvironment]: https://pkg.go.dev/github.com/go-coldbrew/core#SetupEnvironment [SetupReleaseName]: https://pkg.go.dev/github.com/go-coldbrew/core#SetupReleaseName +[vtprotobuf]: https://github.com/planetscale/vtprotobuf [open an issue]: https://github.com/go-coldbrew/core/issues diff --git a/quickstart.md b/quickstart.md index 780d2fe..6f6053b 100644 --- a/quickstart.md +++ b/quickstart.md @@ -1,11 +1,11 @@ --- layout: default -title: "Quickstart" +title: "Getting Started" nav_order: 2 -description: "Create and run your first ColdBrew service in 5 minutes" -permalink: /quickstart +description: "Create and run your first ColdBrew service in 5 minutes with cookiecutter or manual setup" +permalink: /getting-started --- -# Quickstart: Your First ColdBrew Service +# Getting Started: Your First ColdBrew Service {: .no_toc } ## Table of contents @@ -230,6 +230,9 @@ make generate This runs `buf generate` and creates the Go code for your new message types and service interface. +{: .note } +After regenerating, the Go compiler will report an error until you implement the new `Greet` method — this is by design. Your proto file is the contract, and the compiler enforces it. You can't forget an endpoint or deploy a half-implemented API. + ### 3. Implement the handler Add to `service/service.go`: @@ -294,6 +297,134 @@ Everything below was set up automatically by ColdBrew: - **Race-detected tests** via `make test` - **Vulnerability scanning** via `make lint` (includes govulncheck) +## Alternative: Manual Setup (No Cookiecutter) + +If you prefer to set up a project manually without cookiecutter, here's the minimal path: + +### 1. Initialize your module + +```bash +mkdir myservice && cd myservice +go mod init github.com/yourname/myservice +go get github.com/go-coldbrew/core +``` + +### 2. Define your proto + +Create `proto/myservice.proto`: + +```protobuf +syntax = "proto3"; + +package myservice; + +option go_package = "github.com/yourname/myservice/proto"; + +import "google/api/annotations.proto"; + +service MyService { + rpc Echo(EchoRequest) returns (EchoResponse) { + option (google.api.http) = { + post: "/api/v1/echo" + body: "*" + }; + } +} + +message EchoRequest { + string msg = 1; +} + +message EchoResponse { + string msg = 1; +} +``` + +### 3. Generate Go code + +Create `buf.yaml`: + +```yaml +version: v2 +modules: + - path: proto +deps: + - buf.build/googleapis/googleapis +``` + +Create `buf.gen.yaml`: + +```yaml +version: v2 +plugins: + - remote: buf.build/protocolbuffers/go + out: proto + opt: paths=source_relative + - remote: buf.build/grpc/go + out: proto + opt: paths=source_relative + - remote: buf.build/grpc-ecosystem/gateway + out: proto + opt: paths=source_relative +``` + +Then generate: + +```bash +buf dep update +buf generate +``` + +### 4. Write main.go + +```go +package main + +import ( + "context" + + "github.com/go-coldbrew/core" + "github.com/go-coldbrew/core/config" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + + pb "github.com/yourname/myservice/proto" +) + +type myService struct { + pb.UnimplementedMyServiceServer +} + +func (s *myService) Echo(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) { + return &pb.EchoResponse{Msg: req.GetMsg()}, nil +} + +func (s *myService) InitGRPC(ctx context.Context, server *grpc.Server) error { + pb.RegisterMyServiceServer(server, s) + return nil +} + +func (s *myService) InitHTTP(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error { + return pb.RegisterMyServiceHandlerFromEndpoint(ctx, mux, endpoint, opts) +} + +func main() { + cfg := config.GetColdBrewConfig() + cb := core.New(cfg) + cb.SetService(&myService{}) + cb.Run() +} +``` + +### 5. Run it + +```bash +go mod tidy +go run . +``` + +Your service starts on `:9090` (gRPC) and `:9091` (HTTP) with metrics, health checks, and profiling endpoints — all wired automatically. + ## Next Steps - **[Using ColdBrew](/using)** — Configure ports, environment variables, and interceptors diff --git a/tests/navigation.spec.ts b/tests/navigation.spec.ts index a60e83d..a915926 100644 --- a/tests/navigation.spec.ts +++ b/tests/navigation.spec.ts @@ -2,14 +2,15 @@ import { test, expect } from "@playwright/test"; const topLevelPages = [ { path: "/", title: "ColdBrew" }, - { path: "/quickstart/", title: "Quickstart" }, - { path: "/getting-started/", title: "Cookiecutter Setup" }, + { path: "/getting-started/", title: "Getting Started" }, + { path: "/cookiecutter-reference/", title: "Cookiecutter Reference" }, { path: "/using/", title: "Using ColdBrew" }, { path: "/architecture/", title: "Architecture" }, { path: "/howto/", title: "How To" }, { path: "/integrations/", title: "Integrations" }, { path: "/faq/", title: "Frequently Asked Questions" }, { path: "/packages/", title: "Packages" }, + { path: "/config-reference/", title: "Configuration Reference" }, ]; const howtoPages = [ @@ -24,6 +25,8 @@ const howtoPages = [ "/howto/signals/", "/howto/swagger/", "/howto/data-builder/", + "/howto/vtproto/", + "/howto/production/", ]; test.describe("Page Loading", () => {