# AGENTS.md — go-app A small framework for bootstrapping logging, OTEL, and HTTP/gRPC servers from config + environment. Apps embed `*config.AppConfig` in a custom config struct and register services; the framework runs the servers and lifecycle. ## The app init pattern (current, v0.17+) ```go ctx, cncl := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) defer cncl() // Custom config that embeds *config.AppConfig: ctx, cfg := app.MustLoadConfigInto(ctx, &config.ServiceConfig{}) cfg.LoadEnv() // SEE GOTCHA 1 — custom-field env is the caller's job a := &app.App{AppContext: ctx} a.InitOTEL() for _, svc := range appServices { sdFunc, _ := svc.Init(ctx, cfg); ... } a.GRPC, a.HTTP = service.MergeServices(ctx, appServices...) // app-side helper a.MustRun() <-a.Done() // then run collected ShutdownFuncs within a grace period ``` - `MustLoadConfigInto` loads the `-config` YAML + env into a struct that embeds `*config.AppConfig`; `MustSetupConfigAndLogging` is the simple (AppConfig-only) variant. Config loading owns `flag.Parse`, so register app flags in `init()`. - `MustRun` starts gRPC then HTTP then a monitor goroutine; panics if neither is enabled. `Done()` unblocks on ctx cancel or a server exit. ## Registering servers Fill option structs and assign to `App.GRPC` / `App.HTTP` (or use a `service.AppService` + `MergeServices` convenience layer per app): - **gRPC** (`pkg/srv/grpc/opts.AppGRPC`): append `*GRPCService{Name, Type (*grpc.ServiceDesc), Service (impl), GwRegistrationFuncs}`. At least one service must add a transport-credentials **dial option** (e.g. `insecure.NewCredentials()`) so the grpc-gateway can dial the server. Instrumentation, protovalidate, reflection, and the gateway are auto-wired from `GRPCConfig`. - **HTTP** (`pkg/srv/http/opts.AppHTTP`): `Funcs` (path + handler), `Handlers` (`HTTPHandler{Prefix|Pattern, StripPrefix, Handler}` for muxes), and `HealthChecks`. `/health` and the Prometheus endpoint are mounted for you. - **MCP** is not a framework feature: mount an MCP streamable-HTTP handler as an `HTTPHandler{Prefix: "/api/mcp", Handler: ...}`. ## OTEL / Prometheus `InitOTEL()` sets up tracer + meter providers (no-op if disabled). The meter provider **always** includes a Prometheus reader; the HTTP server mounts `promhttp` at `OTEL.PrometheusPath` (default `/metrics`). OTLP export auto-enables when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. Helpers: `otel.GetTracer(ctx, name)`, `otel.GetMeter(ctx, name)`. ## Config & schema `AppConfig` covers `APP_*` env (name/env/version, http, grpc, otel, logging). Custom fields are validated for schema via `app.CustomSchema(&cfg)`. Precedence for AppConfig: defaults → env → YAML file. ## Known gotchas (fix candidates) 1. **Custom-field env is NOT applied.** `MustLoadConfigInto` only parses env for the embedded `AppConfig`; its own doc says custom env is "up to the caller." So `env:"..."` tags on custom fields do nothing unless the app runs `env.Parse` itself. `MustSetupConfigAndLoggingInto[T]` looks like the intended helper but is currently an **empty stub** (returns its args unchanged). → Good candidate: make `MustLoadConfigInto`/that stub run `env.Parse` over the custom struct (skipping the embedded `AppConfig` to preserve precedence). 2. **grpc-gateway path strip defaults to false.** `GRPCGatewayPath` defaults to `/grpc-api` but `GRPCGatewayPathStrip` defaults `false`, so the gateway sees the full prefixed path and 404s every route. Apps must set `grpcGatewayPathStrip: true` (or accept requests must include the prefix in the proto `google.api.http` path). Consider defaulting strip to true. 3. **Prometheus unit suffixing.** The OTEL→Prometheus exporter appends the OTEL unit as a name suffix, so `metric.WithUnit("°F")` on `econet_setpoint_fahrenheit` yields `..._fahrenheit_F`. Either don't set WithUnit (bake units into names) or enable `prometheus.WithoutUnits()` in the meter setup. 4. README's example `main.go` is stale (references removed `app.AppHTTP` / `srv.HTTPFunc`); trust the template app instead. ## Layout `pkg/app` (bootstrap/lifecycle/schema) · `pkg/config` (AppConfig, env/yaml) · `pkg/logging` (zerolog) · `pkg/otel` (providers + helpers) · `pkg/srv/grpc` (+gateway) · `pkg/srv/http` (+health, +prometheus).