Restructure solution layout by module
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		| @@ -1,77 +1,77 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| set -euo pipefail | ||||
|  | ||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||
| CERT_DIR="${SCRIPT_DIR}/../../deploy/telemetry/certs" | ||||
|  | ||||
| mkdir -p "${CERT_DIR}" | ||||
|  | ||||
| CA_KEY="${CERT_DIR}/ca.key" | ||||
| CA_CRT="${CERT_DIR}/ca.crt" | ||||
| COL_KEY="${CERT_DIR}/collector.key" | ||||
| COL_CSR="${CERT_DIR}/collector.csr" | ||||
| COL_CRT="${CERT_DIR}/collector.crt" | ||||
| CLIENT_KEY="${CERT_DIR}/client.key" | ||||
| CLIENT_CSR="${CERT_DIR}/client.csr" | ||||
| CLIENT_CRT="${CERT_DIR}/client.crt" | ||||
|  | ||||
| echo "[*] Generating OpenTelemetry dev CA and certificates in ${CERT_DIR}" | ||||
|  | ||||
| # Root CA | ||||
| if [[ ! -f "${CA_KEY}" ]]; then | ||||
|   openssl genrsa -out "${CA_KEY}" 4096 >/dev/null 2>&1 | ||||
| fi | ||||
| openssl req -x509 -new -key "${CA_KEY}" -days 365 -sha256 \ | ||||
|   -out "${CA_CRT}" -subj "/CN=StellaOps Dev Telemetry CA" \ | ||||
|   -config <(cat <<'EOF' | ||||
| [req] | ||||
| distinguished_name = req_distinguished_name | ||||
| prompt = no | ||||
| [req_distinguished_name] | ||||
| EOF | ||||
| ) >/dev/null 2>&1 | ||||
|  | ||||
| # Collector certificate (server + client auth) | ||||
| openssl req -new -nodes -newkey rsa:4096 \ | ||||
|   -keyout "${COL_KEY}" \ | ||||
|   -out "${COL_CSR}" \ | ||||
|   -subj "/CN=stellaops-otel-collector" >/dev/null 2>&1 | ||||
|  | ||||
| openssl x509 -req -in "${COL_CSR}" -CA "${CA_CRT}" -CAkey "${CA_KEY}" \ | ||||
|   -CAcreateserial -out "${COL_CRT}" -days 365 -sha256 \ | ||||
|   -extensions v3_req -extfile <(cat <<'EOF' | ||||
| [v3_req] | ||||
| subjectAltName = @alt_names | ||||
| extendedKeyUsage = serverAuth, clientAuth | ||||
| [alt_names] | ||||
| DNS.1 = stellaops-otel-collector | ||||
| DNS.2 = localhost | ||||
| IP.1 = 127.0.0.1 | ||||
| EOF | ||||
| ) >/dev/null 2>&1 | ||||
|  | ||||
| # Client certificate | ||||
| openssl req -new -nodes -newkey rsa:4096 \ | ||||
|   -keyout "${CLIENT_KEY}" \ | ||||
|   -out "${CLIENT_CSR}" \ | ||||
|   -subj "/CN=stellaops-otel-client" >/dev/null 2>&1 | ||||
|  | ||||
| openssl x509 -req -in "${CLIENT_CSR}" -CA "${CA_CRT}" -CAkey "${CA_KEY}" \ | ||||
|   -CAcreateserial -out "${CLIENT_CRT}" -days 365 -sha256 \ | ||||
|   -extensions v3_req -extfile <(cat <<'EOF' | ||||
| [v3_req] | ||||
| extendedKeyUsage = clientAuth | ||||
| subjectAltName = @alt_names | ||||
| [alt_names] | ||||
| DNS.1 = stellaops-otel-client | ||||
| DNS.2 = localhost | ||||
| IP.1 = 127.0.0.1 | ||||
| EOF | ||||
| ) >/dev/null 2>&1 | ||||
|  | ||||
| rm -f "${COL_CSR}" "${CLIENT_CSR}" | ||||
| rm -f "${CERT_DIR}/ca.srl" | ||||
|  | ||||
| echo "[✓] Certificates ready:" | ||||
| ls -1 "${CERT_DIR}" | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| set -euo pipefail | ||||
|  | ||||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||
| CERT_DIR="${SCRIPT_DIR}/../../deploy/telemetry/certs" | ||||
|  | ||||
| mkdir -p "${CERT_DIR}" | ||||
|  | ||||
| CA_KEY="${CERT_DIR}/ca.key" | ||||
| CA_CRT="${CERT_DIR}/ca.crt" | ||||
| COL_KEY="${CERT_DIR}/collector.key" | ||||
| COL_CSR="${CERT_DIR}/collector.csr" | ||||
| COL_CRT="${CERT_DIR}/collector.crt" | ||||
| CLIENT_KEY="${CERT_DIR}/client.key" | ||||
| CLIENT_CSR="${CERT_DIR}/client.csr" | ||||
| CLIENT_CRT="${CERT_DIR}/client.crt" | ||||
|  | ||||
| echo "[*] Generating OpenTelemetry dev CA and certificates in ${CERT_DIR}" | ||||
|  | ||||
| # Root CA | ||||
| if [[ ! -f "${CA_KEY}" ]]; then | ||||
|   openssl genrsa -out "${CA_KEY}" 4096 >/dev/null 2>&1 | ||||
| fi | ||||
| openssl req -x509 -new -key "${CA_KEY}" -days 365 -sha256 \ | ||||
|   -out "${CA_CRT}" -subj "/CN=StellaOps Dev Telemetry CA" \ | ||||
|   -config <(cat <<'EOF' | ||||
| [req] | ||||
| distinguished_name = req_distinguished_name | ||||
| prompt = no | ||||
| [req_distinguished_name] | ||||
| EOF | ||||
| ) >/dev/null 2>&1 | ||||
|  | ||||
| # Collector certificate (server + client auth) | ||||
| openssl req -new -nodes -newkey rsa:4096 \ | ||||
|   -keyout "${COL_KEY}" \ | ||||
|   -out "${COL_CSR}" \ | ||||
|   -subj "/CN=stellaops-otel-collector" >/dev/null 2>&1 | ||||
|  | ||||
| openssl x509 -req -in "${COL_CSR}" -CA "${CA_CRT}" -CAkey "${CA_KEY}" \ | ||||
|   -CAcreateserial -out "${COL_CRT}" -days 365 -sha256 \ | ||||
|   -extensions v3_req -extfile <(cat <<'EOF' | ||||
| [v3_req] | ||||
| subjectAltName = @alt_names | ||||
| extendedKeyUsage = serverAuth, clientAuth | ||||
| [alt_names] | ||||
| DNS.1 = stellaops-otel-collector | ||||
| DNS.2 = localhost | ||||
| IP.1 = 127.0.0.1 | ||||
| EOF | ||||
| ) >/dev/null 2>&1 | ||||
|  | ||||
| # Client certificate | ||||
| openssl req -new -nodes -newkey rsa:4096 \ | ||||
|   -keyout "${CLIENT_KEY}" \ | ||||
|   -out "${CLIENT_CSR}" \ | ||||
|   -subj "/CN=stellaops-otel-client" >/dev/null 2>&1 | ||||
|  | ||||
| openssl x509 -req -in "${CLIENT_CSR}" -CA "${CA_CRT}" -CAkey "${CA_KEY}" \ | ||||
|   -CAcreateserial -out "${CLIENT_CRT}" -days 365 -sha256 \ | ||||
|   -extensions v3_req -extfile <(cat <<'EOF' | ||||
| [v3_req] | ||||
| extendedKeyUsage = clientAuth | ||||
| subjectAltName = @alt_names | ||||
| [alt_names] | ||||
| DNS.1 = stellaops-otel-client | ||||
| DNS.2 = localhost | ||||
| IP.1 = 127.0.0.1 | ||||
| EOF | ||||
| ) >/dev/null 2>&1 | ||||
|  | ||||
| rm -f "${COL_CSR}" "${CLIENT_CSR}" | ||||
| rm -f "${CERT_DIR}/ca.srl" | ||||
|  | ||||
| echo "[✓] Certificates ready:" | ||||
| ls -1 "${CERT_DIR}" | ||||
|   | ||||
| @@ -1,136 +1,136 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """Package telemetry collector assets for offline/air-gapped installs. | ||||
|  | ||||
| Outputs a tarball containing the collector configuration, Compose overlay, | ||||
| Helm defaults, and operator README. A SHA-256 checksum sidecar is emitted, and | ||||
| optional Cosign signing can be enabled with --sign. | ||||
| """ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import hashlib | ||||
| import os | ||||
| import subprocess | ||||
| import sys | ||||
| import tarfile | ||||
| from pathlib import Path | ||||
| from typing import Iterable | ||||
|  | ||||
| REPO_ROOT = Path(__file__).resolve().parents[3] | ||||
| DEFAULT_OUTPUT = REPO_ROOT / "out" / "telemetry" / "telemetry-offline-bundle.tar.gz" | ||||
| BUNDLE_CONTENTS: tuple[Path, ...] = ( | ||||
|     Path("deploy/telemetry/README.md"), | ||||
|     Path("deploy/telemetry/otel-collector-config.yaml"), | ||||
|     Path("deploy/telemetry/storage/README.md"), | ||||
|     Path("deploy/telemetry/storage/prometheus.yaml"), | ||||
|     Path("deploy/telemetry/storage/tempo.yaml"), | ||||
|     Path("deploy/telemetry/storage/loki.yaml"), | ||||
|     Path("deploy/telemetry/storage/tenants/tempo-overrides.yaml"), | ||||
|     Path("deploy/telemetry/storage/tenants/loki-overrides.yaml"), | ||||
|     Path("deploy/helm/stellaops/files/otel-collector-config.yaml"), | ||||
|     Path("deploy/helm/stellaops/values.yaml"), | ||||
|     Path("deploy/helm/stellaops/templates/otel-collector.yaml"), | ||||
|     Path("deploy/compose/docker-compose.telemetry.yaml"), | ||||
|     Path("deploy/compose/docker-compose.telemetry-storage.yaml"), | ||||
|     Path("docs/ops/telemetry-collector.md"), | ||||
|     Path("docs/ops/telemetry-storage.md"), | ||||
| ) | ||||
|  | ||||
|  | ||||
| def compute_sha256(path: Path) -> str: | ||||
|     sha = hashlib.sha256() | ||||
|     with path.open("rb") as handle: | ||||
|         for chunk in iter(lambda: handle.read(1024 * 1024), b""): | ||||
|             sha.update(chunk) | ||||
|     return sha.hexdigest() | ||||
|  | ||||
|  | ||||
| def validate_files(paths: Iterable[Path]) -> None: | ||||
|     missing = [str(p) for p in paths if not (REPO_ROOT / p).exists()] | ||||
|     if missing: | ||||
|         raise FileNotFoundError(f"Missing bundle artefacts: {', '.join(missing)}") | ||||
|  | ||||
|  | ||||
| def create_bundle(output_path: Path) -> Path: | ||||
|     output_path.parent.mkdir(parents=True, exist_ok=True) | ||||
|     with tarfile.open(output_path, "w:gz") as tar: | ||||
|         for rel_path in BUNDLE_CONTENTS: | ||||
|             abs_path = REPO_ROOT / rel_path | ||||
|             tar.add(abs_path, arcname=str(rel_path)) | ||||
|     return output_path | ||||
|  | ||||
|  | ||||
| def write_checksum(bundle_path: Path) -> Path: | ||||
|     digest = compute_sha256(bundle_path) | ||||
|     sha_path = bundle_path.with_suffix(bundle_path.suffix + ".sha256") | ||||
|     sha_path.write_text(f"{digest}  {bundle_path.name}\n", encoding="utf-8") | ||||
|     return sha_path | ||||
|  | ||||
|  | ||||
| def cosign_sign(bundle_path: Path, key_ref: str | None, identity_token: str | None) -> None: | ||||
|     cmd = ["cosign", "sign-blob", "--yes", str(bundle_path)] | ||||
|     if key_ref: | ||||
|         cmd.extend(["--key", key_ref]) | ||||
|     env = os.environ.copy() | ||||
|     if identity_token: | ||||
|         env["COSIGN_IDENTITY_TOKEN"] = identity_token | ||||
|     try: | ||||
|         subprocess.run(cmd, check=True, env=env) | ||||
|     except FileNotFoundError as exc: | ||||
|         raise RuntimeError("cosign not found on PATH; install cosign or omit --sign") from exc | ||||
|     except subprocess.CalledProcessError as exc: | ||||
|         raise RuntimeError(f"cosign sign-blob failed: {exc}") from exc | ||||
|  | ||||
|  | ||||
| def parse_args(argv: list[str] | None = None) -> argparse.Namespace: | ||||
|     parser = argparse.ArgumentParser(description=__doc__) | ||||
|     parser.add_argument( | ||||
|         "--output", | ||||
|         type=Path, | ||||
|         default=DEFAULT_OUTPUT, | ||||
|         help=f"Output bundle path (default: {DEFAULT_OUTPUT})", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--sign", | ||||
|         action="store_true", | ||||
|         help="Sign the bundle using cosign (requires cosign on PATH)", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--cosign-key", | ||||
|         type=str, | ||||
|         default=os.environ.get("COSIGN_KEY_REF"), | ||||
|         help="Cosign key reference (file:..., azurekms://..., etc.)", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--identity-token", | ||||
|         type=str, | ||||
|         default=os.environ.get("COSIGN_IDENTITY_TOKEN"), | ||||
|         help="OIDC identity token for keyless signing", | ||||
|     ) | ||||
|     return parser.parse_args(argv) | ||||
|  | ||||
|  | ||||
| def main(argv: list[str] | None = None) -> int: | ||||
|     args = parse_args(argv) | ||||
|     validate_files(BUNDLE_CONTENTS) | ||||
|  | ||||
|     bundle_path = args.output.resolve() | ||||
|     print(f"[*] Creating telemetry bundle at {bundle_path}") | ||||
|     create_bundle(bundle_path) | ||||
|     sha_path = write_checksum(bundle_path) | ||||
|     print(f"[✓] SHA-256 written to {sha_path}") | ||||
|  | ||||
|     if args.sign: | ||||
|         print("[*] Signing bundle with cosign") | ||||
|         cosign_sign(bundle_path, args.cosign_key, args.identity_token) | ||||
|         sig_path = bundle_path.with_suffix(bundle_path.suffix + ".sig") | ||||
|         if sig_path.exists(): | ||||
|             print(f"[✓] Cosign signature written to {sig_path}") | ||||
|         else: | ||||
|             print("[!] Cosign completed but signature file not found (ensure cosign version >= 2.2)") | ||||
|  | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     sys.exit(main()) | ||||
| #!/usr/bin/env python3 | ||||
| """Package telemetry collector assets for offline/air-gapped installs. | ||||
|  | ||||
| Outputs a tarball containing the collector configuration, Compose overlay, | ||||
| Helm defaults, and operator README. A SHA-256 checksum sidecar is emitted, and | ||||
| optional Cosign signing can be enabled with --sign. | ||||
| """ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import hashlib | ||||
| import os | ||||
| import subprocess | ||||
| import sys | ||||
| import tarfile | ||||
| from pathlib import Path | ||||
| from typing import Iterable | ||||
|  | ||||
| REPO_ROOT = Path(__file__).resolve().parents[3] | ||||
| DEFAULT_OUTPUT = REPO_ROOT / "out" / "telemetry" / "telemetry-offline-bundle.tar.gz" | ||||
| BUNDLE_CONTENTS: tuple[Path, ...] = ( | ||||
|     Path("deploy/telemetry/README.md"), | ||||
|     Path("deploy/telemetry/otel-collector-config.yaml"), | ||||
|     Path("deploy/telemetry/storage/README.md"), | ||||
|     Path("deploy/telemetry/storage/prometheus.yaml"), | ||||
|     Path("deploy/telemetry/storage/tempo.yaml"), | ||||
|     Path("deploy/telemetry/storage/loki.yaml"), | ||||
|     Path("deploy/telemetry/storage/tenants/tempo-overrides.yaml"), | ||||
|     Path("deploy/telemetry/storage/tenants/loki-overrides.yaml"), | ||||
|     Path("deploy/helm/stellaops/files/otel-collector-config.yaml"), | ||||
|     Path("deploy/helm/stellaops/values.yaml"), | ||||
|     Path("deploy/helm/stellaops/templates/otel-collector.yaml"), | ||||
|     Path("deploy/compose/docker-compose.telemetry.yaml"), | ||||
|     Path("deploy/compose/docker-compose.telemetry-storage.yaml"), | ||||
|     Path("docs/ops/telemetry-collector.md"), | ||||
|     Path("docs/ops/telemetry-storage.md"), | ||||
| ) | ||||
|  | ||||
|  | ||||
| def compute_sha256(path: Path) -> str: | ||||
|     sha = hashlib.sha256() | ||||
|     with path.open("rb") as handle: | ||||
|         for chunk in iter(lambda: handle.read(1024 * 1024), b""): | ||||
|             sha.update(chunk) | ||||
|     return sha.hexdigest() | ||||
|  | ||||
|  | ||||
| def validate_files(paths: Iterable[Path]) -> None: | ||||
|     missing = [str(p) for p in paths if not (REPO_ROOT / p).exists()] | ||||
|     if missing: | ||||
|         raise FileNotFoundError(f"Missing bundle artefacts: {', '.join(missing)}") | ||||
|  | ||||
|  | ||||
| def create_bundle(output_path: Path) -> Path: | ||||
|     output_path.parent.mkdir(parents=True, exist_ok=True) | ||||
|     with tarfile.open(output_path, "w:gz") as tar: | ||||
|         for rel_path in BUNDLE_CONTENTS: | ||||
|             abs_path = REPO_ROOT / rel_path | ||||
|             tar.add(abs_path, arcname=str(rel_path)) | ||||
|     return output_path | ||||
|  | ||||
|  | ||||
| def write_checksum(bundle_path: Path) -> Path: | ||||
|     digest = compute_sha256(bundle_path) | ||||
|     sha_path = bundle_path.with_suffix(bundle_path.suffix + ".sha256") | ||||
|     sha_path.write_text(f"{digest}  {bundle_path.name}\n", encoding="utf-8") | ||||
|     return sha_path | ||||
|  | ||||
|  | ||||
| def cosign_sign(bundle_path: Path, key_ref: str | None, identity_token: str | None) -> None: | ||||
|     cmd = ["cosign", "sign-blob", "--yes", str(bundle_path)] | ||||
|     if key_ref: | ||||
|         cmd.extend(["--key", key_ref]) | ||||
|     env = os.environ.copy() | ||||
|     if identity_token: | ||||
|         env["COSIGN_IDENTITY_TOKEN"] = identity_token | ||||
|     try: | ||||
|         subprocess.run(cmd, check=True, env=env) | ||||
|     except FileNotFoundError as exc: | ||||
|         raise RuntimeError("cosign not found on PATH; install cosign or omit --sign") from exc | ||||
|     except subprocess.CalledProcessError as exc: | ||||
|         raise RuntimeError(f"cosign sign-blob failed: {exc}") from exc | ||||
|  | ||||
|  | ||||
| def parse_args(argv: list[str] | None = None) -> argparse.Namespace: | ||||
|     parser = argparse.ArgumentParser(description=__doc__) | ||||
|     parser.add_argument( | ||||
|         "--output", | ||||
|         type=Path, | ||||
|         default=DEFAULT_OUTPUT, | ||||
|         help=f"Output bundle path (default: {DEFAULT_OUTPUT})", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--sign", | ||||
|         action="store_true", | ||||
|         help="Sign the bundle using cosign (requires cosign on PATH)", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--cosign-key", | ||||
|         type=str, | ||||
|         default=os.environ.get("COSIGN_KEY_REF"), | ||||
|         help="Cosign key reference (file:..., azurekms://..., etc.)", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--identity-token", | ||||
|         type=str, | ||||
|         default=os.environ.get("COSIGN_IDENTITY_TOKEN"), | ||||
|         help="OIDC identity token for keyless signing", | ||||
|     ) | ||||
|     return parser.parse_args(argv) | ||||
|  | ||||
|  | ||||
| def main(argv: list[str] | None = None) -> int: | ||||
|     args = parse_args(argv) | ||||
|     validate_files(BUNDLE_CONTENTS) | ||||
|  | ||||
|     bundle_path = args.output.resolve() | ||||
|     print(f"[*] Creating telemetry bundle at {bundle_path}") | ||||
|     create_bundle(bundle_path) | ||||
|     sha_path = write_checksum(bundle_path) | ||||
|     print(f"[✓] SHA-256 written to {sha_path}") | ||||
|  | ||||
|     if args.sign: | ||||
|         print("[*] Signing bundle with cosign") | ||||
|         cosign_sign(bundle_path, args.cosign_key, args.identity_token) | ||||
|         sig_path = bundle_path.with_suffix(bundle_path.suffix + ".sig") | ||||
|         if sig_path.exists(): | ||||
|             print(f"[✓] Cosign signature written to {sig_path}") | ||||
|         else: | ||||
|             print("[!] Cosign completed but signature file not found (ensure cosign version >= 2.2)") | ||||
|  | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     sys.exit(main()) | ||||
|   | ||||
| @@ -1,197 +1,197 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| Smoke test for the StellaOps OpenTelemetry Collector deployment. | ||||
|  | ||||
| The script sends sample traces, metrics, and logs over OTLP/HTTP with mutual TLS | ||||
| and asserts that the collector accepted the payloads by checking its Prometheus | ||||
| metrics endpoint. | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import json | ||||
| import ssl | ||||
| import sys | ||||
| import time | ||||
| import urllib.request | ||||
| from pathlib import Path | ||||
|  | ||||
| TRACE_PAYLOAD = { | ||||
|     "resourceSpans": [ | ||||
|         { | ||||
|             "resource": { | ||||
|                 "attributes": [ | ||||
|                     {"key": "service.name", "value": {"stringValue": "smoke-client"}}, | ||||
|                     {"key": "tenant.id", "value": {"stringValue": "dev"}}, | ||||
|                 ] | ||||
|             }, | ||||
|             "scopeSpans": [ | ||||
|                 { | ||||
|                     "scope": {"name": "smoke-test"}, | ||||
|                     "spans": [ | ||||
|                         { | ||||
|                             "traceId": "00000000000000000000000000000001", | ||||
|                             "spanId": "0000000000000001", | ||||
|                             "name": "smoke-span", | ||||
|                             "kind": 1, | ||||
|                             "startTimeUnixNano": "1730000000000000000", | ||||
|                             "endTimeUnixNano": "1730000000500000000", | ||||
|                             "status": {"code": 0}, | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  | ||||
| METRIC_PAYLOAD = { | ||||
|     "resourceMetrics": [ | ||||
|         { | ||||
|             "resource": { | ||||
|                 "attributes": [ | ||||
|                     {"key": "service.name", "value": {"stringValue": "smoke-client"}}, | ||||
|                     {"key": "tenant.id", "value": {"stringValue": "dev"}}, | ||||
|                 ] | ||||
|             }, | ||||
|             "scopeMetrics": [ | ||||
|                 { | ||||
|                     "scope": {"name": "smoke-test"}, | ||||
|                     "metrics": [ | ||||
|                         { | ||||
|                             "name": "smoke_gauge", | ||||
|                             "gauge": { | ||||
|                                 "dataPoints": [ | ||||
|                                     { | ||||
|                                         "asDouble": 1.0, | ||||
|                                         "timeUnixNano": "1730000001000000000", | ||||
|                                         "attributes": [ | ||||
|                                             {"key": "phase", "value": {"stringValue": "ingest"}} | ||||
|                                         ], | ||||
|                                     } | ||||
|                                 ] | ||||
|                             }, | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  | ||||
| LOG_PAYLOAD = { | ||||
|     "resourceLogs": [ | ||||
|         { | ||||
|             "resource": { | ||||
|                 "attributes": [ | ||||
|                     {"key": "service.name", "value": {"stringValue": "smoke-client"}}, | ||||
|                     {"key": "tenant.id", "value": {"stringValue": "dev"}}, | ||||
|                 ] | ||||
|             }, | ||||
|             "scopeLogs": [ | ||||
|                 { | ||||
|                     "scope": {"name": "smoke-test"}, | ||||
|                     "logRecords": [ | ||||
|                         { | ||||
|                             "timeUnixNano": "1730000002000000000", | ||||
|                             "severityNumber": 9, | ||||
|                             "severityText": "Info", | ||||
|                             "body": {"stringValue": "StellaOps collector smoke log"}, | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  | ||||
|  | ||||
| def _load_context(ca: Path, cert: Path, key: Path) -> ssl.SSLContext: | ||||
|     context = ssl.create_default_context(cafile=str(ca)) | ||||
|     context.check_hostname = False | ||||
|     context.verify_mode = ssl.CERT_REQUIRED | ||||
|     context.load_cert_chain(certfile=str(cert), keyfile=str(key)) | ||||
|     return context | ||||
|  | ||||
|  | ||||
| def _post_json(url: str, payload: dict, context: ssl.SSLContext) -> None: | ||||
|     data = json.dumps(payload).encode("utf-8") | ||||
|     request = urllib.request.Request( | ||||
|         url, | ||||
|         data=data, | ||||
|         headers={ | ||||
|             "Content-Type": "application/json", | ||||
|             "User-Agent": "stellaops-otel-smoke/1.0", | ||||
|         }, | ||||
|         method="POST", | ||||
|     ) | ||||
|     with urllib.request.urlopen(request, context=context, timeout=10) as response: | ||||
|         if response.status // 100 != 2: | ||||
|             raise RuntimeError(f"{url} returned HTTP {response.status}") | ||||
|  | ||||
|  | ||||
| def _fetch_metrics(url: str, context: ssl.SSLContext) -> str: | ||||
|     request = urllib.request.Request( | ||||
|         url, | ||||
|         headers={ | ||||
|             "User-Agent": "stellaops-otel-smoke/1.0", | ||||
|         }, | ||||
|     ) | ||||
|     with urllib.request.urlopen(request, context=context, timeout=10) as response: | ||||
|         return response.read().decode("utf-8") | ||||
|  | ||||
|  | ||||
| def _assert_counter(metrics: str, metric_name: str) -> None: | ||||
|     for line in metrics.splitlines(): | ||||
|         if line.startswith(metric_name): | ||||
|             try: | ||||
|                 _, value = line.split(" ") | ||||
|                 if float(value) > 0: | ||||
|                     return | ||||
|             except ValueError: | ||||
|                 continue | ||||
|     raise AssertionError(f"{metric_name} not incremented") | ||||
|  | ||||
|  | ||||
| def main() -> int: | ||||
|     parser = argparse.ArgumentParser(description=__doc__) | ||||
|     parser.add_argument("--host", default="localhost", help="Collector host (default: %(default)s)") | ||||
|     parser.add_argument("--otlp-port", type=int, default=4318, help="OTLP/HTTP port") | ||||
|     parser.add_argument("--metrics-port", type=int, default=9464, help="Prometheus metrics port") | ||||
|     parser.add_argument("--health-port", type=int, default=13133, help="Health check port") | ||||
|     parser.add_argument("--ca", type=Path, default=Path("deploy/telemetry/certs/ca.crt"), help="CA certificate path") | ||||
|     parser.add_argument("--cert", type=Path, default=Path("deploy/telemetry/certs/client.crt"), help="Client certificate path") | ||||
|     parser.add_argument("--key", type=Path, default=Path("deploy/telemetry/certs/client.key"), help="Client key path") | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     for path in (args.ca, args.cert, args.key): | ||||
|         if not path.exists(): | ||||
|             print(f"[!] missing TLS material: {path}", file=sys.stderr) | ||||
|             return 1 | ||||
|  | ||||
|     context = _load_context(args.ca, args.cert, args.key) | ||||
|  | ||||
|     otlp_base = f"https://{args.host}:{args.otlp_port}/v1" | ||||
|     print(f"[*] Sending OTLP traffic to {otlp_base}") | ||||
|     _post_json(f"{otlp_base}/traces", TRACE_PAYLOAD, context) | ||||
|     _post_json(f"{otlp_base}/metrics", METRIC_PAYLOAD, context) | ||||
|     _post_json(f"{otlp_base}/logs", LOG_PAYLOAD, context) | ||||
|  | ||||
|     # Allow Prometheus exporter to update metrics | ||||
|     time.sleep(2) | ||||
|  | ||||
|     metrics_url = f"https://{args.host}:{args.metrics_port}/metrics" | ||||
|     print(f"[*] Fetching collector metrics from {metrics_url}") | ||||
|     metrics = _fetch_metrics(metrics_url, context) | ||||
|  | ||||
|     _assert_counter(metrics, "otelcol_receiver_accepted_spans") | ||||
|     _assert_counter(metrics, "otelcol_receiver_accepted_logs") | ||||
|     _assert_counter(metrics, "otelcol_receiver_accepted_metric_points") | ||||
|  | ||||
|     print("[✓] Collector accepted traces, logs, and metrics.") | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     raise SystemExit(main()) | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| Smoke test for the StellaOps OpenTelemetry Collector deployment. | ||||
|  | ||||
| The script sends sample traces, metrics, and logs over OTLP/HTTP with mutual TLS | ||||
| and asserts that the collector accepted the payloads by checking its Prometheus | ||||
| metrics endpoint. | ||||
| """ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import argparse | ||||
| import json | ||||
| import ssl | ||||
| import sys | ||||
| import time | ||||
| import urllib.request | ||||
| from pathlib import Path | ||||
|  | ||||
| TRACE_PAYLOAD = { | ||||
|     "resourceSpans": [ | ||||
|         { | ||||
|             "resource": { | ||||
|                 "attributes": [ | ||||
|                     {"key": "service.name", "value": {"stringValue": "smoke-client"}}, | ||||
|                     {"key": "tenant.id", "value": {"stringValue": "dev"}}, | ||||
|                 ] | ||||
|             }, | ||||
|             "scopeSpans": [ | ||||
|                 { | ||||
|                     "scope": {"name": "smoke-test"}, | ||||
|                     "spans": [ | ||||
|                         { | ||||
|                             "traceId": "00000000000000000000000000000001", | ||||
|                             "spanId": "0000000000000001", | ||||
|                             "name": "smoke-span", | ||||
|                             "kind": 1, | ||||
|                             "startTimeUnixNano": "1730000000000000000", | ||||
|                             "endTimeUnixNano": "1730000000500000000", | ||||
|                             "status": {"code": 0}, | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  | ||||
| METRIC_PAYLOAD = { | ||||
|     "resourceMetrics": [ | ||||
|         { | ||||
|             "resource": { | ||||
|                 "attributes": [ | ||||
|                     {"key": "service.name", "value": {"stringValue": "smoke-client"}}, | ||||
|                     {"key": "tenant.id", "value": {"stringValue": "dev"}}, | ||||
|                 ] | ||||
|             }, | ||||
|             "scopeMetrics": [ | ||||
|                 { | ||||
|                     "scope": {"name": "smoke-test"}, | ||||
|                     "metrics": [ | ||||
|                         { | ||||
|                             "name": "smoke_gauge", | ||||
|                             "gauge": { | ||||
|                                 "dataPoints": [ | ||||
|                                     { | ||||
|                                         "asDouble": 1.0, | ||||
|                                         "timeUnixNano": "1730000001000000000", | ||||
|                                         "attributes": [ | ||||
|                                             {"key": "phase", "value": {"stringValue": "ingest"}} | ||||
|                                         ], | ||||
|                                     } | ||||
|                                 ] | ||||
|                             }, | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  | ||||
| LOG_PAYLOAD = { | ||||
|     "resourceLogs": [ | ||||
|         { | ||||
|             "resource": { | ||||
|                 "attributes": [ | ||||
|                     {"key": "service.name", "value": {"stringValue": "smoke-client"}}, | ||||
|                     {"key": "tenant.id", "value": {"stringValue": "dev"}}, | ||||
|                 ] | ||||
|             }, | ||||
|             "scopeLogs": [ | ||||
|                 { | ||||
|                     "scope": {"name": "smoke-test"}, | ||||
|                     "logRecords": [ | ||||
|                         { | ||||
|                             "timeUnixNano": "1730000002000000000", | ||||
|                             "severityNumber": 9, | ||||
|                             "severityText": "Info", | ||||
|                             "body": {"stringValue": "StellaOps collector smoke log"}, | ||||
|                         } | ||||
|                     ], | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  | ||||
|  | ||||
| def _load_context(ca: Path, cert: Path, key: Path) -> ssl.SSLContext: | ||||
|     context = ssl.create_default_context(cafile=str(ca)) | ||||
|     context.check_hostname = False | ||||
|     context.verify_mode = ssl.CERT_REQUIRED | ||||
|     context.load_cert_chain(certfile=str(cert), keyfile=str(key)) | ||||
|     return context | ||||
|  | ||||
|  | ||||
| def _post_json(url: str, payload: dict, context: ssl.SSLContext) -> None: | ||||
|     data = json.dumps(payload).encode("utf-8") | ||||
|     request = urllib.request.Request( | ||||
|         url, | ||||
|         data=data, | ||||
|         headers={ | ||||
|             "Content-Type": "application/json", | ||||
|             "User-Agent": "stellaops-otel-smoke/1.0", | ||||
|         }, | ||||
|         method="POST", | ||||
|     ) | ||||
|     with urllib.request.urlopen(request, context=context, timeout=10) as response: | ||||
|         if response.status // 100 != 2: | ||||
|             raise RuntimeError(f"{url} returned HTTP {response.status}") | ||||
|  | ||||
|  | ||||
| def _fetch_metrics(url: str, context: ssl.SSLContext) -> str: | ||||
|     request = urllib.request.Request( | ||||
|         url, | ||||
|         headers={ | ||||
|             "User-Agent": "stellaops-otel-smoke/1.0", | ||||
|         }, | ||||
|     ) | ||||
|     with urllib.request.urlopen(request, context=context, timeout=10) as response: | ||||
|         return response.read().decode("utf-8") | ||||
|  | ||||
|  | ||||
| def _assert_counter(metrics: str, metric_name: str) -> None: | ||||
|     for line in metrics.splitlines(): | ||||
|         if line.startswith(metric_name): | ||||
|             try: | ||||
|                 _, value = line.split(" ") | ||||
|                 if float(value) > 0: | ||||
|                     return | ||||
|             except ValueError: | ||||
|                 continue | ||||
|     raise AssertionError(f"{metric_name} not incremented") | ||||
|  | ||||
|  | ||||
| def main() -> int: | ||||
|     parser = argparse.ArgumentParser(description=__doc__) | ||||
|     parser.add_argument("--host", default="localhost", help="Collector host (default: %(default)s)") | ||||
|     parser.add_argument("--otlp-port", type=int, default=4318, help="OTLP/HTTP port") | ||||
|     parser.add_argument("--metrics-port", type=int, default=9464, help="Prometheus metrics port") | ||||
|     parser.add_argument("--health-port", type=int, default=13133, help="Health check port") | ||||
|     parser.add_argument("--ca", type=Path, default=Path("deploy/telemetry/certs/ca.crt"), help="CA certificate path") | ||||
|     parser.add_argument("--cert", type=Path, default=Path("deploy/telemetry/certs/client.crt"), help="Client certificate path") | ||||
|     parser.add_argument("--key", type=Path, default=Path("deploy/telemetry/certs/client.key"), help="Client key path") | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     for path in (args.ca, args.cert, args.key): | ||||
|         if not path.exists(): | ||||
|             print(f"[!] missing TLS material: {path}", file=sys.stderr) | ||||
|             return 1 | ||||
|  | ||||
|     context = _load_context(args.ca, args.cert, args.key) | ||||
|  | ||||
|     otlp_base = f"https://{args.host}:{args.otlp_port}/v1" | ||||
|     print(f"[*] Sending OTLP traffic to {otlp_base}") | ||||
|     _post_json(f"{otlp_base}/traces", TRACE_PAYLOAD, context) | ||||
|     _post_json(f"{otlp_base}/metrics", METRIC_PAYLOAD, context) | ||||
|     _post_json(f"{otlp_base}/logs", LOG_PAYLOAD, context) | ||||
|  | ||||
|     # Allow Prometheus exporter to update metrics | ||||
|     time.sleep(2) | ||||
|  | ||||
|     metrics_url = f"https://{args.host}:{args.metrics_port}/metrics" | ||||
|     print(f"[*] Fetching collector metrics from {metrics_url}") | ||||
|     metrics = _fetch_metrics(metrics_url, context) | ||||
|  | ||||
|     _assert_counter(metrics, "otelcol_receiver_accepted_spans") | ||||
|     _assert_counter(metrics, "otelcol_receiver_accepted_logs") | ||||
|     _assert_counter(metrics, "otelcol_receiver_accepted_metric_points") | ||||
|  | ||||
|     print("[✓] Collector accepted traces, logs, and metrics.") | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     raise SystemExit(main()) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user