Skip to content

Commit 629ce66

Browse files
authored
feat(observability): add end-to-end sample (#1128)
* feat(observability): add end-to-end observability sample * docs(observability): trim integration README scope
1 parent 4bb14f2 commit 629ce66

46 files changed

Lines changed: 1716 additions & 35 deletions

Some content is hidden

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

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,15 @@ $(OUT_DIR)/$(PROJECT_NAME)$(EXT_NAME): $(SOURCES)
7878
@mkdir -p $(OUT_DIR)
7979
@CGO_ENABLED=$(CGO) GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build $(GCFLAGS) -ldflags=$(LDFLAGS) -o $(OUT_DIR)/$(PROJECT_NAME)$(EXT_NAME) $(SOURCES)
8080

81+
# Port 4318 is owned by sample-level OpenTelemetry dependencies on macOS/OrbStack.
8182
## start: Start the application (for server)
8283
.PHONY: start
8384
start: export DUBBO_GO_CONFIG_PATH ?= $(PROJECT_DIR)/go-server/conf/dubbogo.yml
8485
start: build
8586
$(info > Starting application $(PROJECT_NAME), output is redirected to $(LOG_FILE))
8687
@ls $(OUT_DIR)/$(PROJECT_NAME)$(EXT_NAME) >/dev/null
8788
@-pkill -f "$(OUT_DIR)/$(PROJECT_NAME)$(EXT_NAME)" 2>/dev/null || true
88-
@-command -v lsof >/dev/null 2>&1 && lsof -P -sTCP:LISTEN -tiTCP:20000 -iTCP:20001 -iTCP:20002 -iTCP:20022 -iTCP:4318 -iTCP:50051 -iTCP:50052 | xargs -r kill -9 || true
89+
@-command -v lsof >/dev/null 2>&1 && lsof -P -sTCP:LISTEN -tiTCP:20000 -iTCP:20001 -iTCP:20002 -iTCP:20022 -iTCP:50051 -iTCP:50052 | xargs -r kill -9 || true
8990
@sleep 1
9091
@-cd $(PROJECT_DIR) && $(OUT_DIR)/$(PROJECT_NAME)$(EXT_NAME) > $(LOG_FILE) 2>&1 & echo $$! > $(PID)
9192
@sed 's/^/ \> PID: /' $(PID)
@@ -98,4 +99,4 @@ stop:
9899
@-test -f $(PID) && kill `cat $(PID)` 2>/dev/null || true
99100
@-pkill -f "$(OUT_DIR)/$(PROJECT_NAME)$(EXT_NAME)" 2>/dev/null || true
100101
@-rm -f $(PID)
101-
@-command -v lsof >/dev/null 2>&1 && lsof -P -sTCP:LISTEN -tiTCP:20000 -iTCP:20001 -iTCP:20002 -iTCP:20022 -iTCP:4318 -iTCP:50051 -iTCP:50052 | xargs -r kill -9 || true
102+
@-command -v lsof >/dev/null 2>&1 && lsof -P -sTCP:LISTEN -tiTCP:20000 -iTCP:20001 -iTCP:20002 -iTCP:20022 -iTCP:50051 -iTCP:50052 | xargs -r kill -9 || true

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ Please refer to [HOWTO.md](HOWTO.md) for detailed instructions on running the sa
4747
* `logger/rolling`: Output to file.
4848
* `logger/custom`: Custom logger.
4949
* `logger/trace-integration`: Integrate OpenTelemetry trace information, automatically injecting trace_id, span_id and other information into logs.
50-
* `metrics`: Observability-related samples.
51-
* `metrics/prometheus_grafana`: Shows how to collect and expose metrics from Dubbo-go services, supporting both Prometheus Push and Pull modes. Also includes the `pgw-cleaner` tool for cleaning zombie metrics in Push mode.
52-
* `metrics/probe`: Demonstrates Dubbo-go Kubernetes probe endpoints (`/live`, `/ready`, `/startup`) and deployment usage.
50+
* `observability`: Observability samples for Dubbo-go.
51+
* `observability/integration`: End-to-end integration-validation sample combining Nacos discovery, Dubbo Triple tracing, an OpenTelemetry Collector, Prometheus/Grafana, and trace-correlated logs.
52+
* `observability/prometheus_grafana`: Shows how to collect and expose metrics from Dubbo-go services, supporting both Prometheus Push and Pull modes. Also includes the `pgw-cleaner` tool for cleaning zombie metrics in Push mode.
53+
* `observability/probe`: Demonstrates Dubbo-go Kubernetes probe endpoints (`/live`, `/ready`, `/startup`) and deployment usage.
5354
* `mesh`: Proxy-based service mesh example showing how to deploy Dubbo-go services with Envoy on Kubernetes.
5455
* `online_boutique`: Microservices “online boutique” demo built with Dubbo-go.
5556
* `otel/tracing`: Distributed tracing examples using OpenTelemetry.

README_CN.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@
4747
* `logger/rolling`:输出到文件。
4848
* `logger/custom`:自定义 logger。
4949
* `logger/trace-integration`:集成 OpenTelemetry trace 信息,自动将 trace_id、span_id 等信息注入日志。
50-
* `metrics`:可观测性相关示例。
51-
* `metrics/prometheus_grafana`:展示如何采集并暴露 Dubbo-go 服务指标,支持 Prometheus Push 和 Pull 两种模式;同时包含用于清理 Push 模式僵尸指标的 `pgw-cleaner` 工具。
52-
* `metrics/probe`:演示 Dubbo-go 在 Kubernetes 场景下的探针端点(`/live``/ready``/startup`)及部署方式。
50+
* `observability`:Dubbo-go 可观测性相关样例。
51+
* `observability/integration`:端到端可观测性集成验证样例,组合 Nacos 服务发现、Dubbo Triple 链路追踪、OpenTelemetry Collector、Prometheus/Grafana 以及带 Trace 关联的日志。
52+
* `observability/prometheus_grafana`:展示如何采集并暴露 Dubbo-go 服务指标,支持 Prometheus Push 和 Pull 两种模式;同时包含用于清理 Push 模式僵尸指标的 `pgw-cleaner` 工具。
53+
* `observability/probe`:演示 Dubbo-go 在 Kubernetes 场景下的探针端点(`/live``/ready``/startup`)及部署方式。
5354
* `mesh`:基于代理的服务网格示例,展示如何在 Kubernetes 上结合 Envoy 部署 Dubbo-go 服务。
5455
* `online_boutique`:基于 Dubbo-go 构建的微服务 “在线商城” 演示项目。
5556
* `otel/tracing`:使用 OpenTelemetry 的分布式链路追踪示例。

integrate_test.sh

Lines changed: 272 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ JAVA_SERVER_RUN_SH="$(find "$P_DIR" -type f -path '*/java-server*/run.sh' -print
4444
JAVA_CLIENT_RUN_SH="$(find "$P_DIR" -type f -path '*/java-client*/run.sh' -print -quit || true)"
4545
JAVA_SERVER_PID=""
4646
GO_AUX_PIDS=()
47+
SAMPLE_COMPOSE_FILE=""
48+
SAMPLE_COMPOSE_SERVICES=()
49+
DOCKER_COMPOSE_CMD=()
50+
51+
if [ "$SAMPLE" = "observability/integration" ]; then
52+
SAMPLE_COMPOSE_FILE="$P_DIR/docker-compose.yaml"
53+
SAMPLE_COMPOSE_SERVICES=(jaeger otel-collector prometheus grafana)
54+
fi
4755

4856
JAVA_ENABLED=true
4957
if { [ -n "$JAVA_SERVER_RUN_SH" ] || [ -n "$JAVA_CLIENT_RUN_SH" ]; } && ! command -v mvn >/dev/null 2>&1; then
@@ -77,10 +85,184 @@ cleanup() {
7785
kill_if_running "$server_pid"
7886
rm -f "$PID_FILE"
7987
fi
88+
stop_sample_dependencies
89+
if [ -n "$SAMPLE_COMPOSE_FILE" ]; then
90+
wait_for_tcp_port_closed "127.0.0.1" "4318" 30 || true
91+
fi
8092
run_make_target stop >/dev/null 2>&1 || true
8193
}
8294
trap cleanup EXIT
8395

96+
start_sample_dependencies() {
97+
if [ -z "$SAMPLE_COMPOSE_FILE" ]; then
98+
return 0
99+
fi
100+
101+
if ! wait_for_http_url "http://127.0.0.1:8848/nacos/v1/console/health/liveness" 90 5; then
102+
echo "Root Nacos liveness did not remain healthy on 127.0.0.1:8848"
103+
return 1
104+
fi
105+
if ! wait_for_tcp_port "127.0.0.1" "9848" 60; then
106+
echo "Root Nacos gRPC endpoint did not become ready on 127.0.0.1:9848"
107+
return 1
108+
fi
109+
110+
if docker compose version >/dev/null 2>&1; then
111+
DOCKER_COMPOSE_CMD=(docker compose)
112+
elif command -v docker-compose >/dev/null 2>&1; then
113+
DOCKER_COMPOSE_CMD=(docker-compose)
114+
else
115+
echo "Docker Compose is required for sample dependencies: $SAMPLE"
116+
return 1
117+
fi
118+
119+
echo "Starting sample dependencies: ${SAMPLE_COMPOSE_SERVICES[*]}"
120+
"${DOCKER_COMPOSE_CMD[@]}" -f "$SAMPLE_COMPOSE_FILE" up -d "${SAMPLE_COMPOSE_SERVICES[@]}"
121+
122+
if ! wait_for_tcp_port "127.0.0.1" "4318" 60; then
123+
echo "OpenTelemetry Collector did not become ready on 127.0.0.1:4318"
124+
return 1
125+
fi
126+
if ! wait_for_tcp_port "127.0.0.1" "9090" 60; then
127+
echo "Prometheus did not become ready on 127.0.0.1:9090"
128+
return 1
129+
fi
130+
}
131+
132+
stop_sample_dependencies() {
133+
if [ -z "$SAMPLE_COMPOSE_FILE" ] || [ "${#DOCKER_COMPOSE_CMD[@]}" -eq 0 ]; then
134+
return 0
135+
fi
136+
137+
"${DOCKER_COMPOSE_CMD[@]}" -f "$SAMPLE_COMPOSE_FILE" stop "${SAMPLE_COMPOSE_SERVICES[@]}" >/dev/null 2>&1 || true
138+
"${DOCKER_COMPOSE_CMD[@]}" -f "$SAMPLE_COMPOSE_FILE" rm -f "${SAMPLE_COMPOSE_SERVICES[@]}" >/dev/null 2>&1 || true
139+
}
140+
141+
# verify_observability_semantics asserts that the telemetry pipeline actually
142+
# received and stored the expected signals before the sample stack is torn
143+
# down: Prometheus scrape targets are up and have scraped RPC metrics, Jaeger
144+
# holds a cross-service trace (consumer and provider spans of the same trace),
145+
# and Grafana reports the provisioned data source and dashboard. Any failed
146+
# check exits non-zero so a broken scrape target, OTLP endpoint, or dashboard
147+
# provisioning cannot silently pass the integration.
148+
verify_observability_semantics() {
149+
if [ "$SAMPLE" != "observability/integration" ]; then
150+
return 0
151+
fi
152+
153+
echo "Verifying observability telemetry semantics before teardown..."
154+
if ! python3 <<'PY'
155+
import base64
156+
import json
157+
import time
158+
import urllib.request
159+
160+
PROMETHEUS = "http://127.0.0.1:9090"
161+
JAEGER = "http://127.0.0.1:16686"
162+
GRAFANA = "http://127.0.0.1:3000"
163+
GRAFANA_HEADERS = {
164+
"Authorization": "Basic " + base64.b64encode(b"admin:admin").decode("ascii"),
165+
}
166+
167+
168+
def fetch(url, headers=None):
169+
request = urllib.request.Request(url, headers=headers or {})
170+
with urllib.request.urlopen(request, timeout=2) as response:
171+
return response.status, response.read()
172+
173+
174+
def prometheus_targets_up():
175+
_, body = fetch(PROMETHEUS + "/api/v1/targets")
176+
jobs = {}
177+
for target in json.loads(body)["data"].get("activeTargets", []):
178+
jobs.setdefault(target["labels"].get("job"), []).append(target["health"])
179+
for job in ("dubbo-observability-server", "dubbo-observability-client"):
180+
if "up" not in jobs.get(job, []):
181+
return False
182+
return True
183+
184+
185+
def prometheus_metrics_scraped():
186+
_, body = fetch(PROMETHEUS + "/api/v1/query?query=dubbo_provider_requests_succeed_total")
187+
result = json.loads(body).get("data", {}).get("result", [])
188+
return any(float(series["value"][1]) > 0 for series in result)
189+
190+
191+
def jaeger_cross_service_trace():
192+
_, body = fetch(JAEGER + "/api/traces?service=dubbo-observability-client&lookback=1h&limit=5")
193+
for trace in json.loads(body).get("data", []):
194+
processes = {process.get("serviceName") for process in trace.get("processes", {}).values()}
195+
if {"dubbo-observability-client", "dubbo-observability-server"} <= processes:
196+
return True
197+
return False
198+
199+
200+
def grafana_provisioned():
201+
status, _ = fetch(GRAFANA + "/api/health")
202+
if status != 200:
203+
return False
204+
_, body = fetch(GRAFANA + "/api/datasources/uid/prometheus", GRAFANA_HEADERS)
205+
datasource = json.loads(body)
206+
if datasource.get("uid") != "prometheus" or datasource.get("type") != "prometheus":
207+
return False
208+
_, body = fetch(GRAFANA + "/api/datasources/uid/prometheus/health", GRAFANA_HEADERS)
209+
if json.loads(body).get("status") != "OK":
210+
return False
211+
_, body = fetch(
212+
GRAFANA + "/api/datasources/proxy/uid/prometheus/api/v1/query"
213+
"?query=dubbo_provider_requests_succeed_total",
214+
GRAFANA_HEADERS,
215+
)
216+
query_result = json.loads(body).get("data", {}).get("result", [])
217+
if not any(float(series["value"][1]) > 0 for series in query_result):
218+
return False
219+
status, _ = fetch(
220+
GRAFANA + "/api/dashboards/uid/dubbo-go-observability",
221+
GRAFANA_HEADERS,
222+
)
223+
return status == 200
224+
225+
226+
checks = [
227+
(prometheus_targets_up, "Prometheus scrape targets are up"),
228+
(prometheus_metrics_scraped, "Prometheus has scraped dubbo_provider_requests_succeed_total"),
229+
(jaeger_cross_service_trace, "Jaeger holds a consumer/provider cross-service trace"),
230+
(grafana_provisioned, "Grafana data source and dashboard are provisioned"),
231+
]
232+
pending = checks
233+
last_errors = {}
234+
deadline = time.monotonic() + 90
235+
while pending and time.monotonic() < deadline:
236+
next_pending = []
237+
for check, description in pending:
238+
try:
239+
if check():
240+
print(" ok: " + description)
241+
continue
242+
except Exception as exc: # noqa: BLE001
243+
last_errors[description] = exc
244+
next_pending.append((check, description))
245+
pending = next_pending
246+
if pending:
247+
time.sleep(2)
248+
249+
if pending:
250+
for _, description in pending:
251+
last_error = last_errors.get(description)
252+
suffix = " (last error: %s)" % last_error if last_error else ""
253+
print(" failed: " + description + suffix)
254+
print("observability semantic verification failed for: " + ", ".join(
255+
description for _, description in pending
256+
))
257+
raise SystemExit(1)
258+
PY
259+
then
260+
echo "Observability telemetry semantic verification failed for: $SAMPLE"
261+
return 1
262+
fi
263+
echo "Observability telemetry semantics verified"
264+
}
265+
84266
resolve_config_path() {
85267
local role="$1"
86268
local conf_dir="$P_DIR/$role/conf"
@@ -138,6 +320,79 @@ PY
138320
return 1
139321
}
140322

323+
wait_for_tcp_port_closed() {
324+
local host="$1"
325+
local port="$2"
326+
local timeout_seconds="$3"
327+
local elapsed=0
328+
329+
while [ "$elapsed" -lt "$timeout_seconds" ]; do
330+
if python3 - "$host" "$port" <<'PY' >/dev/null 2>&1
331+
import socket
332+
import sys
333+
334+
host = sys.argv[1]
335+
port = int(sys.argv[2])
336+
337+
for af, socktype, proto, _, sockaddr in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
338+
sock = None
339+
try:
340+
sock = socket.socket(af, socktype, proto)
341+
sock.settimeout(1.0)
342+
sock.connect(sockaddr)
343+
sys.exit(1)
344+
except OSError:
345+
continue
346+
finally:
347+
if sock is not None:
348+
sock.close()
349+
350+
sys.exit(0)
351+
PY
352+
then
353+
return 0
354+
fi
355+
sleep 1
356+
elapsed=$((elapsed + 1))
357+
done
358+
359+
return 1
360+
}
361+
362+
wait_for_http_url() {
363+
local url="$1"
364+
local timeout_seconds="$2"
365+
local required_successes="${3:-1}"
366+
local elapsed=0
367+
local successes=0
368+
369+
while [ "$elapsed" -lt "$timeout_seconds" ]; do
370+
if python3 - "$url" <<'PY' >/dev/null 2>&1
371+
import sys
372+
import urllib.request
373+
374+
try:
375+
with urllib.request.urlopen(sys.argv[1], timeout=1) as response:
376+
if response.status != 200:
377+
raise RuntimeError(f"unexpected HTTP status: {response.status}")
378+
except Exception:
379+
sys.exit(1)
380+
PY
381+
then
382+
successes=$((successes + 1))
383+
if [ "$successes" -ge "$required_successes" ]; then
384+
return 0
385+
fi
386+
else
387+
successes=0
388+
fi
389+
sleep 1
390+
elapsed=$((elapsed + 1))
391+
done
392+
393+
return 1
394+
}
395+
141396
wait_for_process_exit() {
142397
local pid="$1"
143398
local timeout_seconds="$2"
@@ -447,11 +702,27 @@ main() {
447702
fi
448703

449704
start_go_server
705+
start_sample_dependencies
450706
start_aux_go_servers
451707

452-
run_go_client
708+
if [ "$SAMPLE" = "observability/integration" ]; then
709+
if ! run_go_client; then
710+
echo "Observability integration client validation failed for: $SAMPLE"
711+
return 1
712+
fi
713+
else
714+
run_go_client
715+
fi
453716
run_java_client_if_present
454717

718+
if ! verify_observability_semantics; then
719+
return 1
720+
fi
721+
722+
if [ -n "$SAMPLE_COMPOSE_FILE" ]; then
723+
stop_sample_dependencies
724+
wait_for_tcp_port_closed "127.0.0.1" "4318" 30 || true
725+
fi
455726
stop_go_server
456727

457728
if start_java_server_if_present; then

0 commit comments

Comments
 (0)