add AGENTS.md
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user