|
| 1 | +# Examples: Storage + Observability |
| 2 | + |
| 3 | +The sample app in `examples/main.go` demonstrates: |
| 4 | + |
| 5 | +- storage CRUD using the `storage` package |
| 6 | +- observability bootstrap via `observability.Init(...)` (or `observability.New`, same behavior) |
| 7 | +- HTTP middleware instrumentation via `middlewares.Observability(obs)` |
| 8 | +- custom metric registration (`orders_created_total`) |
| 9 | +- logger bootstrap via `logger.Init(...)` and `slog` request logging |
| 10 | + |
| 11 | +Note: to get `trace_id` / `span_id` on request logs, keep request logging middleware |
| 12 | +after `middlewares.Observability(obs)` in the chi middleware chain. |
| 13 | + |
| 14 | +It exposes: |
| 15 | + |
| 16 | +- `POST /orders` (creates an item in storage) |
| 17 | +- `GET /orders/{id}` (reads item from storage) |
| 18 | +- `GET /health/liveness` (always 204) |
| 19 | +- `GET /health/readiness` (checks storage ping, 204/503) |
| 20 | +- `GET /api-docs` (minimal OpenAPI JSON document) |
| 21 | +- `/metrics` (Prometheus mode only) |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Prerequisites |
| 26 | + |
| 27 | +- Go installed |
| 28 | +- `grep` for quick metric filtering |
| 29 | +- `jq` (optional, for parsing JSON in the curl examples below) |
| 30 | +- Docker (optional, for local OTLP collector) |
| 31 | + |
| 32 | +### Optional: trace outbound HTTP from your own code |
| 33 | + |
| 34 | +Server-side spans come from `middlewares.Observability`. For **client** calls, wrap the transport (add module `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` to your app): |
| 35 | + |
| 36 | +```go |
| 37 | +import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" |
| 38 | + |
| 39 | +client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} |
| 40 | +``` |
| 41 | + |
| 42 | +This is separate from the example’s chi server instrumentation. |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## Run with Prometheus Metrics (quickest path) |
| 47 | + |
| 48 | +From repo root: |
| 49 | + |
| 50 | +```bash |
| 51 | +METRICS_MODE=prometheus ENABLE_TRACING=false LOGGER_LEVEL=info LOGGER_JSON=false go run ./examples |
| 52 | +``` |
| 53 | + |
| 54 | +In another terminal, generate traffic: |
| 55 | + |
| 56 | +```bash |
| 57 | +curl -i http://localhost:8080/health/liveness |
| 58 | +curl -i http://localhost:8080/health/readiness |
| 59 | +curl -s http://localhost:8080/api-docs |
| 60 | +ORDER_ID=$(curl -sf -X POST http://localhost:8080/orders | jq -r '.id') |
| 61 | +curl -i "http://localhost:8080/orders/${ORDER_ID}" |
| 62 | +``` |
| 63 | + |
| 64 | +Without `jq`, create an order with `curl -i -X POST http://localhost:8080/orders` and copy the `id` field from the JSON body for the GET URL. |
| 65 | + |
| 66 | +Verify metrics from the app directly: |
| 67 | + |
| 68 | +```bash |
| 69 | +curl -s http://localhost:8080/metrics | grep -E "http_requests_total|magic_storage_operations_total|orders_created_total" |
| 70 | +``` |
| 71 | + |
| 72 | +You should see series for: |
| 73 | + |
| 74 | +- `http_requests_total` (HTTP middleware) |
| 75 | +- `magic_storage_operations_total` (storage instrumentation) |
| 76 | +- `orders_created_total` (custom metric from app code) |
| 77 | + |
| 78 | +### Optional: view with Prometheus UI |
| 79 | + |
| 80 | +The repo includes `examples/prometheus.yml`: |
| 81 | + |
| 82 | +```bash |
| 83 | +docker run --rm --name prom-magic-example \ |
| 84 | + -p "9090:9090" \ |
| 85 | + -v "./examples/prometheus.yml:/etc/prometheus/prometheus.yml:ro" \ |
| 86 | + prom/prometheus |
| 87 | +``` |
| 88 | + |
| 89 | +Then open <http://localhost:9090> and query: |
| 90 | + |
| 91 | +- `http_requests_total` |
| 92 | +- `magic_storage_operations_total` |
| 93 | +- `orders_created_total` |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## Run with OTLP Metrics + Traces (with Jaeger UI) |
| 98 | + |
| 99 | +Start Jaeger (OTLP gRPC receiver + UI): |
| 100 | + |
| 101 | +```bash |
| 102 | +docker run --rm --name jaeger-magic-example \ |
| 103 | + -p "16686:16686" \ |
| 104 | + -p "4318:4317" \ |
| 105 | + -v "$(pwd)/examples/jaeger.json:/etc/jaeger/ui-config.json:ro" \ |
| 106 | + jaegertracing/all-in-one:latest \ |
| 107 | + --query.ui-config /etc/jaeger/ui-config.json |
| 108 | +``` |
| 109 | + |
| 110 | +Open the trace UI at <http://localhost:16686>. |
| 111 | + |
| 112 | +Start the local collector (config in `examples/otel-collector.yml`): |
| 113 | + |
| 114 | +```bash |
| 115 | +docker run --rm --name otelcol-magic-example \ |
| 116 | + -p "4317:4317" \ |
| 117 | + -p "9464:9464" \ |
| 118 | + -v "./examples/otel-collector.yml:/etc/otelcol-contrib/config.yaml:ro" \ |
| 119 | + otel/opentelemetry-collector-contrib:latest |
| 120 | +``` |
| 121 | + |
| 122 | +This collector setup: |
| 123 | + |
| 124 | +- receives app OTLP metrics + traces on `:4317` |
| 125 | +- re-exposes metrics on `:9464` (Prometheus format) |
| 126 | +- forwards traces to Jaeger OTLP endpoint on `host.docker.internal:4318` |
| 127 | + |
| 128 | +Run the app in OTLP mode: |
| 129 | + |
| 130 | +```bash |
| 131 | +METRICS_MODE=otlp ENABLE_TRACING=true OTLP_ENDPOINT=host.docker.internal:4317 LOGGER_LEVEL=debug LOGGER_JSON=true go run ./examples |
| 132 | +``` |
| 133 | + |
| 134 | +Generate traffic: |
| 135 | + |
| 136 | +```bash |
| 137 | +curl -i http://localhost:8080/health/liveness |
| 138 | +curl -i http://localhost:8080/health/readiness |
| 139 | +curl -s http://localhost:8080/api-docs |
| 140 | +ORDER_ID=$(curl -sf -X POST http://localhost:8080/orders | jq -r '.id') |
| 141 | +curl -i "http://localhost:8080/orders/${ORDER_ID}" |
| 142 | +``` |
| 143 | + |
| 144 | +Without `jq`, create an order with `curl -i -X POST http://localhost:8080/orders` and copy the `id` field from the JSON body for the GET URL. |
| 145 | + |
| 146 | +### Verify metrics in OTLP mode |
| 147 | + |
| 148 | +In OTLP mode, the app's `/metrics` endpoint intentionally returns 404. |
| 149 | +Metrics are exported to the collector, which re-exposes them at `:9464`: |
| 150 | + |
| 151 | +```bash |
| 152 | +curl -s http://localhost:9464/metrics | grep -E "http_requests_total|magic_storage_operations_total|orders_created_total" |
| 153 | +``` |
| 154 | + |
| 155 | +### Verify traces are working |
| 156 | + |
| 157 | +Collector still exports traces to `debug` too, so you can inspect raw output: |
| 158 | + |
| 159 | +```bash |
| 160 | +docker logs otelcol-magic-example |
| 161 | +``` |
| 162 | + |
| 163 | +You should see trace export entries after hitting `/orders` routes. |
| 164 | + |
| 165 | +To visualize traces in Jaeger: |
| 166 | + |
| 167 | +1. Open <http://localhost:16686> |
| 168 | +2. Select service `magic-storage-observability-example` |
| 169 | +3. Click **Find Traces** |
| 170 | + |
| 171 | +Expected span shape for a successful create request: |
| 172 | + |
| 173 | +- `HTTP POST /orders` (server/root span from middleware) |
| 174 | +- `orders.create` (example business span) |
| 175 | +- `storage.create` (storage instrumentation span) |
| 176 | + |
| 177 | +Expected span shape for a successful get request: |
| 178 | + |
| 179 | +- `HTTP GET /orders/{id}` |
| 180 | +- `orders.get` |
| 181 | +- `storage.get` |
| 182 | + |
| 183 | +### Verify log correlation (`trace_id` / `span_id`) |
| 184 | + |
| 185 | +When tracing is enabled and logs use `slog.*Context`, the logger wrapper injects `trace_id` and `span_id`. |
| 186 | + |
| 187 | +In this example, the `/orders` handlers log via `slog.InfoContext` / `slog.ErrorContext`. |
| 188 | + |
| 189 | +Run with JSON logs: |
| 190 | + |
| 191 | +```bash |
| 192 | +METRICS_MODE=otlp ENABLE_TRACING=true OTLP_ENDPOINT=localhost:4317 LOGGER_LEVEL=debug LOGGER_JSON=true go run ./examples |
| 193 | +``` |
| 194 | + |
| 195 | +Then hit: |
| 196 | + |
| 197 | +```bash |
| 198 | +ORDER_ID=$(curl -sf -X POST http://localhost:8080/orders | jq -r '.id') |
| 199 | +curl -i "http://localhost:8080/orders/${ORDER_ID}" |
| 200 | +``` |
| 201 | + |
| 202 | +Without `jq`, create an order with `curl -i -X POST http://localhost:8080/orders` and copy the `id` field from the JSON body for the GET URL. |
| 203 | + |
| 204 | +You should see log lines containing `trace_id` and `span_id`. |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## Notes |
| 209 | + |
| 210 | +- `METRICS_MODE` supports: |
| 211 | + - `prometheus` (default) |
| 212 | + - `otlp` |
| 213 | +- `ENABLE_TRACING=true` enables spans from HTTP + storage + pubsub instrumentation paths. |
| 214 | +- Logger config (environment variables): |
| 215 | + - `LOGGER_LEVEL` -> `debug|info|warn|error` (default: `info`) |
| 216 | + - `LOGGER_JSON` -> `true|false` (default: `false`) |
| 217 | +- For storage adapters outside this example, update the adapter config block in `examples/main.go`. |
0 commit comments