A minimal, production-ready Go HTTP service with OpenTelemetry traces, metrics, and logs wired up out of the box. Clone it, rename the module, add your business logic.
caching-fire-go/
├── cmd/server/main.go # Entry point — wires everything together
├── internal/
│ ├── config/config.go # Env-based config with defaults
│ ├── observability/
│ │ ├── setup.go # Single Setup() call returns one shutdown func
│ │ ├── tracer.go # OTLP gRPC trace exporter + propagator
│ │ ├── meter.go # OTLP gRPC metric exporter (periodic reader)
│ │ └── logger.go # slog → stdout (JSON) AND OTel collector
│ └── handler/
│ └── example.go # Example GET /example handler — replace with your logic
├── deploy/
│ ├── docker-compose.yml # app + otel-collector
│ └── otel-collector.yaml # collector config: OTLP in, debug + prometheus out
├── Dockerfile
└── .env.example
| Signal | How | Where it lands |
|---|---|---|
| Traces | otelgin middleware + manual spans |
OTel collector → debug log |
| Metrics | otel.Meter counters + histograms |
OTel collector → Prometheus |
| Logs | slog with multiHandler |
stdout (JSON) + OTel collector |
Logs go to both stdout and the OTel collector simultaneously. Pod logs and the collector both have the full log stream.
| Endpoint | Description |
|---|---|
GET /example |
Example handler with trace, metric, and log |
GET /healthz |
Returns {"status": "ok"} |
# Replace with your own module path
find . -type f -name "*.go" | xargs sed -i 's|github.com/your-org/caching-fire-go|github.com/you/your-service|g'
sed -i 's|github.com/your-org/caching-fire-go|github.com/you/your-service|g' go.modcd deploy
docker compose up# Requires an OTLP collector on localhost:4317
cp .env.example .env
go mod tidy
go run ./cmd/servercurl localhost:8080/example
curl localhost:8080/healthzThis repo ships a full local Kubernetes test: a Valkey cluster managed by the
Valkey operator, the Go app pointed at it via /hit and /fetch, and a
k6 Job driving the /hit → wait 5s → /fetch pattern from
inside the cluster. Everything is wrapped in make targets and runs on
kind.
Cache endpoints under test:
| Endpoint | Description |
|---|---|
POST /hit |
Generates a 2 KB payload, stores it in Valkey under a fresh key (TTL 10s), returns the key |
GET /fetch?key= |
200 if the key is still cached, 404 once its TTL has elapsed |
-
docker,kind,kubectl,helm -
A Helm repo named
valkeyadded locally — this is where the operator and cluster charts are pulled from. The Makefile runshelm install … valkey/valkey-operatorandvalkey/valkey-resourcesverbatim:helm repo add valkey <your-valkey-charts-repo-url> helm repo update
From the repo root:
make kind-allThis creates the kind cluster and then runs, in order:
valkey-operator—helm install valkey-operator valkey/valkey-operator -n valkey-operator-system --create-namespacevalkey-cluster—helm install my-cluster valkey/valkey-resources -n valkey --create-namespace, then waits for the ValkeyCluster pods to be Readykind-image— builds the app image and side-loads it into the cluster (kind load)deploy-app—helm upgrade --installthe app chart withvalues-kind.yamlintons/caching-firek6-job— runs the k6 load Job in-cluster and tails its results
Each step is also a standalone target — run make help to list them, or step
through them one at a time.
The operator names the Valkey Service after the release, not my-cluster. For
the my-cluster release in ns/valkey it creates a headless Service
(CLUSTER-IP: None) named valkey-my-cluster-valkey-resources, so the app is
configured with:
VALKEY_ADDR=valkey-my-cluster-valkey-resources.valkey.svc.cluster.local:6379
(set in deploy/helm/caching-fire-go/values-kind.yaml). If your release or
namespace differs, confirm the name with make valkey-svc and update that one
line — the app fail-fasts on a bad address, so pods CrashLoopBackOff rather
than start.
make k6-job # re-run k6 against the running app
make app-forward # forward svc/caching-fire-go -> localhost:8080 to curl by hand
make kind-down # delete the whole kind clusterFull walkthrough, architecture diagram, and troubleshooting live in
deploy/kind/README.md.
| Variable | Default | Description |
|---|---|---|
HTTP_ADDR |
:8080 |
Address the HTTP server listens on |
OTEL_EXPORTER_OTLP_ENDPOINT |
localhost:4317 |
gRPC endpoint of the OTel collector |
OTEL_SERVICE_NAME |
caching-fire-go |
Service name in all OTel signals |
LOG_LEVEL |
info |
Minimum log level for stdout |
VALKEY_ADDR |
localhost:6379 |
Address of the Valkey cache |
CACHE_TTL_SECONDS |
10 |
TTL for /hit payloads |
- Create
internal/handler/yourhandler.gofollowing the pattern inexample.go:- Pull
ctxfromc.Request.Context()and pass it to every downstream call - Start child spans with
otel.Tracer(...).Start(ctx, "span.name") - Record metrics via
otel.Meter(...) - Log with
slog.InfoContext(ctx, ...)— trace IDs are correlated automatically
- Pull
- Register the route in
cmd/server/main.go
The service handles SIGINT/SIGTERM gracefully:
- HTTP server drains in-flight requests (10 s timeout)
- OTel providers flush buffered traces, metrics, and logs before exit