Skip to content

Commit 320d46a

Browse files
committed
docs: add signal pipeline architecture diagram for HTTP metering enhancement
1 parent 92f41ae commit 320d46a

3 files changed

Lines changed: 92 additions & 37 deletions

File tree

docs/enhancements/http-traffic-metering.md renamed to docs/enhancements/http-metering/http-traffic-metering.md

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ Each metric will record the following dimensions:
133133

134134
### Surfacing Signals from the Edge
135135

136+
![HTTP Metering Signal Pipeline](./signal-pipeline.png)
137+
136138
All metering signals originate from the **edge cluster**, where the Envoy
137139
Gateway proxies (`datum-downstream-gateway`) actually serve customer traffic.
138140
There is no central collection point that observes individual requests — the
@@ -144,50 +146,47 @@ The raw access log already carries everything the meters need *except* one
144146
thing: the `route_name` field identifies the owning project only by its
145147
control-plane namespace UID (e.g. `ns-<project-uid>`), not by the
146148
human-readable project name. To populate the `project_name` dimension, three
147-
components must be updated, all operating at the edge:
148-
149-
1. **Network Services Operator (controller).** When the operator reconciles a
150-
customer `HTTPRoute` into its downstream representation, it injects the
151-
project name as a request header (`x-datum-project-name`) via a
152-
`RequestHeaderModifier` filter on each route rule. The project name is read
153-
from the upstream cluster identity (the Milo project name) that the
154-
operator already holds while mapping upstream → downstream resources. Routes
155-
that already define a `RequestHeaderModifier` are merged into rather than
156-
duplicated, since Gateway API permits at most one such filter per rule.
157-
158-
2. **Envoy access log format.** The `EnvoyProxy` access log JSON format is
159-
extended with a `project_name` field sourced from the injected header:
160-
`project_name: "%REQ(X-DATUM-PROJECT-NAME)%"`. Because the header is set on
161-
the route before the access log is written, every logged request for a
162-
customer route carries the resolved project name. (We use `%REQ()%` rather
163-
than `%METADATA(ROUTE:...)%` because Envoy Gateway's JSON access log
164-
formatter does not register the metadata formatter, so route metadata is not
165-
accessible from JSON access logs.)
149+
components collaborate at the edge:
150+
151+
1. **Extension Server (xDS mutation).** The NSO extension server implements
152+
`ApplyTPPRouteConfig` in `internal/extensionserver/mutate/tpp.go`. During
153+
each xDS route-config build, it iterates every VirtualHost owned by NSO and
154+
calls `injectProjectNameMetadata` on every route, which writes the resolved
155+
`project_name` string directly into
156+
`filter_metadata["datum-gateway"]["project_name"]` on the Envoy
157+
`RouteConfiguration` proto. This happens for every NSO-owned route
158+
regardless of whether a `TrafficProtectionPolicy` governs it — WAF config is
159+
an optional overlay on top of the metadata that is always stamped. The
160+
project name is sourced from `idx.ProjectNames[dsNS]`, the
161+
downstream-namespace → project-name mapping the operator maintains in its
162+
policy index.
163+
164+
2. **Envoy access log format.** The `EnvoyProxy` access log JSON format
165+
includes a `project_name` field read from the xDS route metadata:
166+
`project_name: "%METADATA(ROUTE:datum-gateway:project_name)%"`. Because the
167+
extension server stamps the metadata into the xDS route before any request is
168+
served, every logged request for a customer route carries the resolved
169+
project name. (`%METADATA(ROUTE:...)%` is used because the name lives in xDS
170+
route metadata — it is not a per-request value and does not need to travel as
171+
a header.)
166172

167173
3. **Vector billing collector.** The `billing-usage-collector-vector` VRL
168-
transform reads the `project_name` field from each access log line and adds
169-
it as a dimension on all four emitted CloudEvents (requests, ingress-bytes,
170-
egress-bytes, connection-seconds), and subject. An absent or empty value (rendered by
171-
Envoy as `"-"`) is normalized to an empty string so unmatched routes do not
174+
transform reads the `project_name` field from each parsed access log line
175+
and adds it as a dimension on all four emitted CloudEvents (requests,
176+
ingress-bytes, egress-bytes, connection-seconds). An absent or Envoy-default
177+
`"-"` value is normalized to an empty string so unmatched routes do not
172178
pollute the dimension.
173179

174-
This keeps the entire signal path — request handling, name resolution, log
175-
emission, parsing, and CloudEvent forwarding — co-located on the edge cluster.
180+
This keeps the entire signal path — xDS route enrichment, log emission,
181+
parsing, and CloudEvent forwarding — co-located on the edge cluster.
176182

177183
#### Transport: how access logs reach Vector
178184

179-
The access log line must travel from the Envoy proxy to the
180-
`billing-usage-collector-vector` agent. Two transports are viable; see
181-
[Access Log Transport](#access-log-transport-file-sink-vs-otlp-sink) under
182-
Alternatives for the trade-offs. In short:
183-
184-
- **File sink (stdout) + `kubernetes_logs`** — the current/baseline approach,
185-
where Envoy writes JSON to stdout and Vector tails the node's container logs.
186-
This requires Vector to run as a per-node DaemonSet co-located with the Envoy
187-
pod, which holds on edge clusters but not where Vector runs as an aggregator.
188-
- **OpenTelemetry (OTLP) sink** — Envoy pushes access logs directly to Vector's
189-
OTLP receiver over the network, independent of pod/node topology. This is
190-
implemented in a draft PR (see below).
185+
The access log line travels from the Envoy proxy to the
186+
`billing-usage-collector-vector` agent via the **File sink (stdout) +
187+
`kubernetes_logs`** approach: Envoy writes JSON to `/dev/stdout` (the `File`
188+
sink configured on `datum-downstream-gateway`) and the node-local Vector
189+
DaemonSet tails the container log file via its `kubernetes_logs` source.
191190

192191
---
193192

45.3 KB
Loading
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
@startuml signal-pipeline
2+
skinparam sequenceArrowThickness 1.5
3+
skinparam sequenceGroupBorderColor #888888
4+
skinparam sequenceGroupFontColor #444444
5+
skinparam sequenceGroupBackgroundColor #F8F8F8
6+
skinparam participantBackgroundColor #DDEEFF
7+
skinparam participantBorderColor #336699
8+
skinparam noteBorderColor #AAAAAA
9+
skinparam noteBackgroundColor #FFFDE7
10+
skinparam defaultFontName "Helvetica"
11+
skinparam defaultFontSize 12
12+
13+
title HTTP Metering Signal Pipeline
14+
15+
participant "Envoy Gateway\n(Control Plane)" as EG
16+
participant "NSO\nExtension Server" as EXT <<Go>>
17+
participant "Envoy Proxy\n(Data Plane)\n/dev/stdout" as ENV
18+
participant "Vector Agent\n(DaemonSet)\nkubernetes_logs" as VEC <<VRL>>
19+
participant "Billing Gateway\n(HTTP sink)" as BG
20+
21+
== xDS Build (once per HTTPRoute change) ==
22+
23+
EG -> EXT : PostRouteModifyHook\n(RouteConfiguration)
24+
note right of EXT
25+
ApplyTPPRouteConfig iterates\nevery NSO-owned VirtualHost/route
26+
and calls injectProjectNameMetadata,\nwriting project_name into
27+
filter_metadata["datum-gateway"]
28+
on each Envoy route proto.
29+
end note
30+
EXT -> EG : mutated RouteConfiguration\n(route.metadata.filter_metadata\n["datum-gateway"]["project_name"])
31+
EG -> ENV : xDS push — routes now carry\nproject_name in filter_metadata
32+
33+
== Per-Request (data plane) ==
34+
35+
?-> ENV : HTTP request
36+
activate ENV
37+
ENV -> ENV : Serve request,\nwrite JSON access log to stdout\n(File sink, path: /dev/stdout)\nincl. METADATA(ROUTE:datum-gateway:project_name)
38+
ENV -->?: HTTP response
39+
deactivate ENV
40+
41+
VEC -> ENV : tail container log file\n/var/log/pods/.../envoy/0.log\n(kubernetes_logs, gid=0)
42+
note right of VEC
43+
VRL transform:
44+
1. parse_json(.message) → top-level fields
45+
2. abort if route_name !~ httproute/<ns>/<name>
46+
3. derive subject = "project_name"
47+
4. emit 4 CloudEvents:
48+
• requests = "1"
49+
• ingress-bytes = bytes_received
50+
• egress-bytes = bytes_sent
51+
• connection-secs = duration_ms / 1000
52+
Each event carries dimensions including project_name.
53+
end note
54+
VEC -> BG : POST /v1/usage/events:batchIngest\n[4 CloudEvents, JSON body]
55+
56+
@enduml

0 commit comments

Comments
 (0)