@@ -44,6 +44,14 @@ JAVA_SERVER_RUN_SH="$(find "$P_DIR" -type f -path '*/java-server*/run.sh' -print
4444JAVA_CLIENT_RUN_SH=" $( find " $P_DIR " -type f -path ' */java-client*/run.sh' -print -quit || true) "
4545JAVA_SERVER_PID=" "
4646GO_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
4856JAVA_ENABLED=true
4957if { [ -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}
8294trap 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+
84266resolve_config_path () {
85267 local role=" $1 "
86268 local conf_dir=" $P_DIR /$role /conf"
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+
141396wait_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