Skip to content

Commit d7f0f7c

Browse files
committed
minimal impl
1 parent 05b2105 commit d7f0f7c

41 files changed

Lines changed: 2845 additions & 4 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/xds-example/build.gradle

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
id 'application'
3+
}
4+
5+
dependencies {
6+
implementation project(':xds')
7+
implementation project(':prometheus1')
8+
implementation libs.jackson.databind
9+
implementation libs.jackson.dataformat.yaml
10+
implementation libs.micrometer.prometheus
11+
implementation libs.protobuf.java.util
12+
runtimeOnly libs.slf4j.simple
13+
14+
testImplementation project(':junit5')
15+
testImplementation libs.assertj
16+
testImplementation libs.awaitility
17+
testImplementation libs.junit5.jupiter.api
18+
}
19+
20+
application {
21+
mainClass.set('example.armeria.xds.Main')
22+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Prometheus + Grafana for xDS metrics visualization.
2+
#
3+
# Usage:
4+
# 1. Start the xDS example: ./gradlew :examples:xds-example:run
5+
# 2. Start monitoring: docker compose up -d
6+
# 3. Open Grafana at http://localhost:3000 (anonymous access enabled)
7+
# 4. The "Armeria xDS Metrics" dashboard is auto-provisioned.
8+
9+
services:
10+
prometheus:
11+
image: prom/prometheus:latest
12+
ports:
13+
- "9090:9090"
14+
volumes:
15+
- ./prometheus.yml:/etc/prometheus/prometheus.yml
16+
extra_hosts:
17+
- "host.docker.internal:host-gateway"
18+
19+
grafana:
20+
image: grafana/grafana:latest
21+
ports:
22+
- "3000:3000"
23+
environment:
24+
- GF_SECURITY_ADMIN_PASSWORD=admin
25+
- GF_AUTH_ANONYMOUS_ENABLED=true
26+
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
27+
- GF_AUTH_DISABLE_LOGIN_FORM=true
28+
- GF_DASHBOARDS_MIN_REFRESH_INTERVAL=1s
29+
volumes:
30+
- ./grafana/provisioning:/etc/grafana/provisioning
31+
- ./grafana/dashboards:/var/lib/grafana/dashboards
32+
depends_on:
33+
- prometheus
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
{
2+
"title": "Armeria xDS Metrics",
3+
"uid": "armeria-xds",
4+
"editable": true,
5+
"time": {"from": "now-5m", "to": "now"},
6+
"refresh": "1s",
7+
"timepicker": {"refresh_intervals": ["1s", "2s", "5s", "10s", "30s"]},
8+
"templating": {
9+
"list": [
10+
{
11+
"name": "cluster",
12+
"type": "query",
13+
"query": "label_values(armeria_xds_lb_membership, cluster)",
14+
"datasource": "Prometheus",
15+
"refresh": 2,
16+
"includeAll": true,
17+
"multi": true,
18+
"current": {"text": "All", "value": "$__all"}
19+
}
20+
]
21+
},
22+
"panels": [
23+
{"title": "Armeria", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, "collapsed": false},
24+
{
25+
"title": "L1 Requests (by status)",
26+
"description": "Per-attempt counts (includes retry attempts)",
27+
"type": "timeseries",
28+
"gridPos": {"h": 6, "w": 8, "x": 0, "y": 1},
29+
"interval": "1s",
30+
"targets": [{"expr": "sum by (http_status)(rate(layer1_requests_total[3s]))", "legendFormat": "{{http_status}}"}],
31+
"fieldConfig": {"defaults": {"unit": "reqps"}}
32+
},
33+
{
34+
"title": "L2 Requests (by status)",
35+
"description": "Aggregate Layer 2 server requests from Layer 1",
36+
"type": "timeseries",
37+
"gridPos": {"h": 6, "w": 8, "x": 8, "y": 1},
38+
"interval": "1s",
39+
"targets": [{"expr": "sum by (http_status)(rate(layer2_requests_total{side=\"server\"}[3s]))", "legendFormat": "{{http_status}}"}],
40+
"fieldConfig": {"defaults": {"unit": "reqps"}}
41+
},
42+
{
43+
"title": "Retries",
44+
"description": "Rate of retry attempts (actual requests minus logical requests)",
45+
"type": "timeseries",
46+
"gridPos": {"h": 6, "w": 8, "x": 16, "y": 1},
47+
"interval": "1s",
48+
"targets": [
49+
{"expr": "sum(rate(layer1_actual_requests_attempts_sum[3s])) - sum(rate(layer1_actual_requests_attempts_count[3s]))", "legendFormat": "L1 retries"},
50+
{
51+
"expr": "sum(rate(layer2_actual_requests_attempts_sum{side=\"client\"}[3s])) - sum(rate(layer2_actual_requests_attempts_count{side=\"client\"}[3s]))",
52+
"legendFormat": "L2 retries"
53+
}
54+
],
55+
"fieldConfig": {"defaults": {"unit": "reqps"}}
56+
},
57+
{
58+
"title": "Route Success (L1 → L2 → L3)",
59+
"description": "Successful request routing path (2xx)",
60+
"type": "timeseries",
61+
"gridPos": {"h": 6, "w": 8, "x": 0, "y": 7},
62+
"interval": "1s",
63+
"targets": [
64+
{"expr": "sum by (layer2_node, layer3_node)(rate(layer1_route_total{http_status=\"200\"}[3s]))", "legendFormat": "{{layer2_node}} → {{layer3_node}}"}
65+
],
66+
"fieldConfig": {"defaults": {"unit": "reqps"}}
67+
},
68+
{
69+
"title": "Route Failure (L1 → L2 → L3)",
70+
"description": "Failed request routing path (non-2xx)",
71+
"type": "timeseries",
72+
"gridPos": {"h": 6, "w": 8, "x": 8, "y": 7},
73+
"interval": "1s",
74+
"targets": [
75+
{"expr": "sum by (layer2_node, layer3_node, http_status)(rate(layer1_route_total{http_status!=\"200\"}[3s]))", "legendFormat": "{{layer2_node}} → {{layer3_node}} {{http_status}}"}
76+
],
77+
"fieldConfig": {"defaults": {"unit": "reqps"}}
78+
},
79+
{
80+
"title": "Active Connections (by protocol)",
81+
"description": "Client connection pool — h2 = TLS, h2c = cleartext. Cycles every 5s due to max connection age.",
82+
"type": "timeseries",
83+
"gridPos": {"h": 6, "w": 8, "x": 16, "y": 7},
84+
"interval": "1s",
85+
"targets": [
86+
{"expr": "layer1_client_active_connections", "legendFormat": "L1 {{protocol}} → {{remote_ip}}"},
87+
{"expr": "layer2_client_active_connections", "legendFormat": "L2 {{protocol}} → {{remote_ip}}"}
88+
]
89+
},
90+
{
91+
"title": "Route Distribution (L2)",
92+
"description": "Percentage of successful traffic routed to each L2 node",
93+
"type": "piechart",
94+
"gridPos": {"h": 6, "w": 8, "x": 0, "y": 13},
95+
"interval": "1s",
96+
"targets": [{"expr": "sum by (layer2_node)(rate(layer1_route_total{http_status=\"200\"}[10s]))", "legendFormat": "{{layer2_node}}"}],
97+
"options": {
98+
"reduceOptions": {"calcs": ["lastNotNull"]},
99+
"legend": {"displayMode": "table", "placement": "right", "values": ["percent"]},
100+
"pieType": "donut"
101+
}
102+
},
103+
{
104+
"title": "Route Distribution (L3)",
105+
"description": "Percentage of successful traffic routed to each L3 node",
106+
"type": "piechart",
107+
"gridPos": {"h": 6, "w": 8, "x": 8, "y": 13},
108+
"interval": "1s",
109+
"targets": [{"expr": "sum by (layer3_node)(rate(layer1_route_total{http_status=\"200\"}[10s]))", "legendFormat": "{{layer3_node}}"}],
110+
"options": {
111+
"reduceOptions": {"calcs": ["lastNotNull"]},
112+
"legend": {"displayMode": "table", "placement": "right", "values": ["percent"]},
113+
"pieType": "donut"
114+
}
115+
},
116+
{
117+
"title": "Route Distribution (L2 → L3)",
118+
"description": "Percentage of successful traffic per L2→L3 path",
119+
"type": "piechart",
120+
"gridPos": {"h": 6, "w": 8, "x": 16, "y": 13},
121+
"interval": "1s",
122+
"targets": [
123+
{"expr": "sum by (layer2_node, layer3_node)(rate(layer1_route_total{http_status=\"200\"}[10s]))", "legendFormat": "{{layer2_node}} → {{layer3_node}}"}
124+
],
125+
"options": {
126+
"reduceOptions": {"calcs": ["lastNotNull"]},
127+
"legend": {"displayMode": "table", "placement": "right", "values": ["percent"]},
128+
"pieType": "donut"
129+
}
130+
},
131+
{"title": "Control Plane", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 20}, "collapsed": false},
132+
{
133+
"title": "File Loaded",
134+
"type": "timeseries",
135+
"gridPos": {"h": 6, "w": 6, "x": 0, "y": 21},
136+
"interval": "1s",
137+
"targets": [{"expr": "sum by (node) (rate(armeria_xds_configsource_file_loaded_total[3s]))", "legendFormat": "{{node}}"}],
138+
"fieldConfig": {"defaults": {"unit": "ops"}}
139+
},
140+
{
141+
"title": "File Errors",
142+
"type": "timeseries",
143+
"gridPos": {"h": 6, "w": 6, "x": 6, "y": 21},
144+
"interval": "1s",
145+
"targets": [{"expr": "sum by (node) (rate(armeria_xds_configsource_file_error_total[3s]))", "legendFormat": "{{node}}"}],
146+
"fieldConfig": {"defaults": {"unit": "ops"}}
147+
},
148+
{
149+
"title": "Resources Active",
150+
"type": "timeseries",
151+
"gridPos": {"h": 6, "w": 6, "x": 12, "y": 21},
152+
"interval": "1s",
153+
"targets": [{"expr": "armeria_xds_resource_node_revision", "legendFormat": "{{type}}/{{name}}"}]
154+
},
155+
{
156+
"title": "Resource Errors",
157+
"type": "timeseries",
158+
"gridPos": {"h": 6, "w": 6, "x": 18, "y": 21},
159+
"interval": "1s",
160+
"targets": [
161+
{"expr": "rate(armeria_xds_resource_node_error_total[3s])", "legendFormat": "error {{type}}/{{name}}"},
162+
{"expr": "rate(armeria_xds_resource_node_missing_total[3s])", "legendFormat": "missing {{type}}/{{name}}"}
163+
],
164+
"fieldConfig": {"defaults": {"unit": "ops"}}
165+
},
166+
{"title": "Load Balancer", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 27}, "collapsed": false},
167+
{
168+
"title": "Membership per Locality",
169+
"type": "timeseries",
170+
"gridPos": {"h": 6, "w": 8, "x": 0, "y": 28},
171+
"interval": "1s",
172+
"targets": [{"expr": "armeria_xds_lb_membership{cluster=~\"$cluster\"}", "legendFormat": "{{cluster}} / {{region}}"}]
173+
},
174+
{
175+
"title": "Healthy / Degraded Membership",
176+
"type": "timeseries",
177+
"gridPos": {"h": 6, "w": 8, "x": 8, "y": 28},
178+
"interval": "1s",
179+
"targets": [
180+
{"expr": "armeria_xds_lb_membership_healthy{cluster=~\"$cluster\"}", "legendFormat": "healthy {{cluster}}/{{region}}"},
181+
{"expr": "armeria_xds_lb_membership_degraded{cluster=~\"$cluster\"}", "legendFormat": "degraded {{cluster}}/{{region}}"}
182+
]
183+
},
184+
{
185+
"title": "Locality Weights",
186+
"type": "timeseries",
187+
"gridPos": {"h": 6, "w": 8, "x": 16, "y": 28},
188+
"interval": "1s",
189+
"targets": [{"expr": "armeria_xds_lb_locality_weight{cluster=~\"$cluster\"}", "legendFormat": "{{cluster}} / {{region}}"}]
190+
},
191+
{
192+
"title": "Priority Load",
193+
"description": "Traffic distribution across priorities. In panic mode, all endpoints are treated as healthy regardless of actual health status.",
194+
"type": "timeseries",
195+
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 34},
196+
"interval": "1s",
197+
"targets": [
198+
{"expr": "armeria_xds_lb_state_load_healthy{cluster=~\"$cluster\"}", "legendFormat": "healthy p{{priority}} {{cluster}}"},
199+
{"expr": "armeria_xds_lb_state_load_degraded{cluster=~\"$cluster\"}", "legendFormat": "degraded p{{priority}} {{cluster}}"}
200+
]
201+
},
202+
{
203+
"title": "Panic Mode",
204+
"type": "timeseries",
205+
"gridPos": {"h": 6, "w": 6, "x": 12, "y": 34},
206+
"interval": "1s",
207+
"targets": [{"expr": "armeria_xds_lb_state_panic{cluster=~\"$cluster\"}", "legendFormat": "{{cluster}} p{{priority}}"}]
208+
},
209+
{
210+
"title": "Subsets",
211+
"type": "timeseries",
212+
"gridPos": {"h": 6, "w": 6, "x": 18, "y": 34},
213+
"interval": "1s",
214+
"targets": [{"expr": "armeria_xds_lb_state_subsets{cluster=~\"$cluster\"}", "legendFormat": "{{cluster}}"}]
215+
},
216+
{"title": "Zone-Aware Routing", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 40}, "collapsed": false},
217+
{
218+
"title": "ZAR Local Percentage",
219+
"description": "Percentage of traffic routed to the local zone (computed by Armeria ZAR algorithm)",
220+
"type": "bargauge",
221+
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 41},
222+
"interval": "1s",
223+
"targets": [
224+
{"expr": "armeria_xds_lb_zar_local_percentage{cluster=~\"$cluster\",cluster!~\".*local.*\"}", "legendFormat": "{{node}} / {{cluster}}"}
225+
],
226+
"options": {
227+
"orientation": "horizontal",
228+
"displayMode": "gradient",
229+
"reduceOptions": {"calcs": ["lastNotNull"]}
230+
},
231+
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1}}
232+
},
233+
{
234+
"title": "ZAR Residual Percentage per Locality",
235+
"description": "Residual traffic percentage for each locality after local zone allocation",
236+
"type": "table",
237+
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 41},
238+
"interval": "1s",
239+
"targets": [
240+
{
241+
"expr": "armeria_xds_lb_zar_residual_percentage{cluster=~\"$cluster\",cluster!~\".*local.*\"}",
242+
"legendFormat": "{{node}} / {{cluster}} / {{region}}",
243+
"instant": true,
244+
"format": "table"
245+
}
246+
],
247+
"transformations": [
248+
{
249+
"id": "organize",
250+
"options": {"excludeByName": {"Time": true, "__name__": true, "instance": true, "job": true, "priority": true}, "renameByName": {"Value": "residual %"}}
251+
}
252+
],
253+
"fieldConfig": {"defaults": {"unit": "percentunit"}, "overrides": []}
254+
},
255+
{"title": "LB Selection", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 47}, "collapsed": false},
256+
{
257+
"title": "Selection Rate (hit vs miss)",
258+
"type": "timeseries",
259+
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 48},
260+
"interval": "1s",
261+
"targets": [
262+
{"expr": "rate(armeria_xds_lb_select_total{result=\"hit\",cluster=~\"$cluster\"}[3s])", "legendFormat": "hit {{cluster}}/{{region}}"},
263+
{"expr": "rate(armeria_xds_lb_select_total{result=\"miss\",cluster=~\"$cluster\"}[3s])", "legendFormat": "miss {{cluster}}/{{region}}"}
264+
],
265+
"fieldConfig": {"defaults": {"unit": "ops"}}
266+
},
267+
{
268+
"title": "Errors (no_endpoints / no_host_source)",
269+
"type": "timeseries",
270+
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 48},
271+
"interval": "1s",
272+
"targets": [
273+
{"expr": "rate(armeria_xds_lb_select_total{result=\"no_endpoints\",cluster=~\"$cluster\"}[3s])", "legendFormat": "no_endpoints {{cluster}}"},
274+
{"expr": "rate(armeria_xds_lb_select_total{result=\"no_host_source\",cluster=~\"$cluster\"}[3s])", "legendFormat": "no_host_source {{cluster}}"}
275+
],
276+
"fieldConfig": {"defaults": {"unit": "ops"}}
277+
},
278+
{"title": "LB Update Events", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 54}, "collapsed": false},
279+
{
280+
"title": "Resource / Endpoint / State Updates",
281+
"type": "timeseries",
282+
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 55},
283+
"interval": "1s",
284+
"targets": [
285+
{"expr": "rate(armeria_xds_lb_resource_updated_count_total{cluster=~\"$cluster\"}[3s])", "legendFormat": "resource {{cluster}}"},
286+
{"expr": "rate(armeria_xds_lb_endpoints_updated_count_total{cluster=~\"$cluster\"}[3s])", "legendFormat": "endpoints {{cluster}}"},
287+
{"expr": "rate(armeria_xds_lb_state_updated_count_total{cluster=~\"$cluster\"}[3s])", "legendFormat": "state {{cluster}}"},
288+
{"expr": "rate(armeria_xds_lb_state_rejected_count_total{cluster=~\"$cluster\"}[3s])", "legendFormat": "rejected {{cluster}}"}
289+
],
290+
"fieldConfig": {"defaults": {"unit": "ops"}}
291+
},
292+
{
293+
"title": "Update Revisions",
294+
"type": "timeseries",
295+
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 55},
296+
"interval": "1s",
297+
"targets": [
298+
{"expr": "armeria_xds_lb_resource_updated_revision{cluster=~\"$cluster\"}", "legendFormat": "resource {{cluster}}"},
299+
{"expr": "armeria_xds_lb_state_updated_revision{cluster=~\"$cluster\"}", "legendFormat": "state {{cluster}}"}
300+
]
301+
}
302+
]
303+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: 1
2+
3+
providers:
4+
- name: 'default'
5+
orgId: 1
6+
folder: ''
7+
type: file
8+
allowUiUpdates: true
9+
options:
10+
path: /var/lib/grafana/dashboards
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: 1
2+
3+
datasources:
4+
- name: Prometheus
5+
type: prometheus
6+
access: proxy
7+
url: http://prometheus:9090
8+
isDefault: true
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
global:
2+
scrape_interval: 1s
3+
4+
scrape_configs:
5+
- job_name: 'armeria-xds-example'
6+
metrics_path: '/metrics'
7+
static_configs:
8+
- targets: ['host.docker.internal:8080']

0 commit comments

Comments
 (0)