Observing
GuideOperationsSince version 0.2.0
Operations guide: configure, deploy and observe the gateway. What the capability does, product side: product guide; calling it: developer guide.
Goal: see traffic per operation, not per raw URL.
Command
curl -s localhost:9090/metrics | grep '^flexway_' # the admin port, never the traffic oneobservability:
otlp_endpoint: tempo.observ:4317 # optional, host:port or URL (http://, https://); otherwise OTEL_EXPORTER_OTLP_ENDPOINT, otherwise nothing is pushedTraces and metrics go to that endpoint. An endpoint that takes traces only (Tempo, Jaeger), or a
collector that already scrapes /metrics, wants OTEL_METRICS_EXPORTER=none: the metrics then stay
on /metrics, pushed nowhere.
Every replica names itself in the resource it pushes: service.instance.id is POD_NAME (set it from
the Downward API, metadata.name) or else the hostname, which is the pod name in Kubernetes. A backend
that turns OTLP into Prometheus series makes it the instance label; without it two replicas would
write the same series and the counters would jump between them. OTEL_RESOURCE_ATTRIBUTES is read
last and may add attributes or replace this one.
| Metric | Answers |
|---|---|
flexway_requests_total |
How many, by operation, method, status_class, upstream, tenant (bounded) |
flexway_request_duration_seconds |
How long, streams excluded - an SSE's length would drown the latency |
flexway_upstream_duration_seconds |
How much of that time is the upstream's |
flexway_requests_in_flight |
What is in flight, per upstream - the scaling signal |
flexway_retries_total |
How many retries, by upstream and reason (connection, status_502, status_503) |
flexway_inference_tokens_total |
How many tokens each tenant uses, by backend and direction (input, output) - as the backend reports them; by agent since 1.2.0: an agent an ai override declares, _other for any other, none without one - never a subject; by capability and metered_by since 1.5.0 - a consumption sums metered_by!="upstream" only (a series from before 1.5.0 has no such label) |
flexway_inference_calls_total |
How many inference calls were served - completions, embeddings, metered gRPC methods -, by tenant, agent, client, backend, capability and metered_by |
flexway_inference_cost_EUR_total, flexway_inference_fallback_total |
How many euros, by tenant, agent and the backend that served; how many calls moved to a fallback backend, by from, to, cause and, since 1.2.0, tenant |
flexway_quota_requests_total |
How many requests each quota policy counted for each tenant - what it consumed of its quota, per replica; flexway_quota_exceeded_total counts the refusals, without a tenant |
flexway_build_info |
How many replicas, and which version each serves (version, commit) - during a rolling update |
flexway_streams_open |
How many streams are open, by operation and protocol (sse, websocket, grpc, http) - never in the latency |
flexway_server_in_flight, flexway_server_in_flight_limit, flexway_server_shedding |
The replica against its in-flight ceiling (declared or derived from its memory), and whether it protects itself (1) - shedder, or adaptive limit below its ceiling |
flexway_config_last_reload_successful |
1 while the replica's last reload was applied (or none since its start), 0 when it was rejected: the console says it “Watch” while it serves a configuration behind |
flexway_server_draining |
1 as soon as the replica drains to stop, carried by its last push: the console says it “Retired”, not silent |
flexway_circuit_state, flexway_circuit_retry_at_seconds, flexway_circuit_transitions_total |
Each upstream's breaker state, read at collection (never stale without traffic), its next trial, its transitions (from, to) - a cycle shorter than a collection still counts |
flexway_upstream_connections, flexway_upstream_errors_total |
The connections open to each upstream; the answers the gateway wrote for it, by code (upstream_timeout, upstream_unavailable, upstream_error) |
Event journal - GET /events, on the admin port: the latest reloads (with a
rejection's cause), breaker transitions and starts or stops of load shedding on this replica,
newest first; 128 of each kind, in memory, lost at a restart. It is what the console's overview
reads - never the logs.
curl -s localhost:9090/events | jq '.events[:5]'curl -s localhost:8428/api/v1/query --data-urlencode 'query=sum(flexway_requests_total)'
# dashboard: http://localhost:3000/d/flexway-gateway; traces: Explore > Tempo, service flexwayCheck: a trace crosses edge -> gateway -> upstream without a break in context, even with no
collector on the gateway side. Metrics carry the operation, never the concrete path: a rising
_unmatched is traffic to undeclared routes. No log contains personal data in the clear.
Reading a shutdown. On SIGTERM, readyz turns red first - long enough for the
orchestrator to take the replica out -, then what is in flight finishes, streams included, within
server.shutdown.drain. The log says so in one line:
{"level":"INFO","msg":"shutting down: readiness withdrawn, draining in-flight requests","readiness_delay":5000000000,"drain":20000000000}Past drain, the process cuts what is left, says so (the drain ran out: the requests still in flight are cut) and exits cleanly, after its last OTLP push: a Kubernetes terminationGracePeriodSeconds - or
a Docker Compose stop_grace_period, 10 s by default - must therefore exceed drain by a few seconds.
The stop is announced (flexway_server_draining = 1) by an immediate push on SIGTERM: even a
process killed before its drain ends is said “Retired” by the console.
Metrics and propagation implemented since 0.2.0; the tenant is a dimension since 0.3.0.
Access log, sampling (1.0.0) - observability.access_log: true writes one access line per
request, with the path template (never the path received). observability.sampling: { max_per_second: 50 } caps the root traces per replica, never cutting a trace begun upstream.
Log level (1.1.x) - observability.log_level: debug shows what each request does
(operation, template, upstream, status, duration), without a restart: the level follows the
reload, and a log level changed line says so. Go back to info after the diagnosis. Every line
carries trace_id when the request has a trace; a start that cannot go on ends on a FATAL line.
Simulating (1.0.0) - "what would this configuration do?" without deploying it, on the admin
port; nothing is called, spent or counted:
curl -s localhost:9090/simulate -d '{"method":"GET","host":"api.example.com","path":"/orders?status=open",
"identity":{"issuer":"customers","subject":"alice","tenant":"acme","scopes":["orders:read"]}}'The answer gives the verdict (served, or refused and its code), the operation, the chain and the
request the upstream would receive.
Auditing decisions (1.0.0) - audit: { decisions: refusals } (or all) writes one
authz_decision line per decision on a declared operation, on stdout but marked log.type: audit
so that your pipeline files it apart. It names the operation and its path template (never the path
received), the status and the refusal's code, the mechanism, the issuer, the tenant, the agents, and
the subject hashed. Each line carries seq, prev and hash: a line removed or changed shows. With
audit: { key: "${AUDIT_KEY}" } these are HMACs - without the key, whoever can write the whole
stream can also rewrite it.
Opaque tokens (1.0.0) - a provider that issues references rather than JWTs is declared with
opaque_tokens (one issuer at most, client required): each token is introspected (RFC 7662) and
held to the same rules as a JWT; the answer is kept cache (30 s by default), never past its exp -
the delay before a revocation shows.