Files
2026-07-04 17:19:09 -04:00

90 lines
4.3 KiB
Markdown

# 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).