-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(sdk,cli): 'rocketride otel' — OpenTelemetry bridge over the documented ingester protocol (zero engine changes) #1612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
8d22978
dd78d46
8c9bc3f
704090c
c23b312
7fbf513
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,11 @@ test = [ | |
| "python-dotenv>=1.0.0", | ||
| ] | ||
|
|
||
| otel = [ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dependency discipline is the make-or-break packaging decision here: everything OpenTelemetry lives behind the |
||
| "opentelemetry-sdk>=1.27.0", | ||
| "opentelemetry-exporter-otlp-proto-http>=1.27.0", | ||
| ] | ||
|
|
||
| [project.urls] | ||
| Homepage = "https://github.com/rocketride-ai/rocketride-server" | ||
| Documentation = "https://github.com/rocketride-ai/rocketride-server#readme" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| # MIT License | ||
| # | ||
| # Copyright (c) 2026 Aparavi Software AG | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in all | ||
| # copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
|
|
||
| """ | ||
| RocketRide CLI OpenTelemetry Bridge Command Implementation. | ||
|
|
||
| This module provides the OtelCommand class for exporting live pipeline traces | ||
| and metrics to any OpenTelemetry collector over OTLP through the RocketRide | ||
| CLI. Use this command to observe pipeline executions in backends such as | ||
| Jaeger, Grafana Tempo, Datadog, Langfuse, or LangSmith without any engine or | ||
| server changes: the bridge is a pure consumer of the engine's documented | ||
| WebSocket monitor protocol. | ||
|
|
||
| The command subscribes to the TASK, SUMMARY, FLOW, and SSE monitor event | ||
| types with the wildcard token scope and maps them to OTel spans (pipeline | ||
| flow) and metrics (task status snapshots), exporting both over the same OTLP | ||
| endpoint. | ||
|
|
||
| Trace level: the bridge can only observe runs; it cannot change the trace | ||
| level of a run it did not start. Pipeline FLOW spans appear only for runs | ||
| started with the ``pipelineTraceLevel`` execute argument, e.g. | ||
| ``client.use(pipelineTraceLevel='summary')``. Without it, the bridge still | ||
| exports task lifecycle spans and status metrics. | ||
|
|
||
| Key Features: | ||
| - OTLP export of pipeline flow spans and task status metrics | ||
| - Wildcard monitor subscription covering all tasks for the API key | ||
| - Standard OTEL_EXPORTER_OTLP_ENDPOINT/_HEADERS/OTEL_SERVICE_NAME support | ||
| (with no endpoint configured at all, the exporters' own env semantics | ||
| apply, including OTEL_EXPORTER_OTLP_TRACES/METRICS_ENDPOINT) | ||
| - Privacy by default: payload content excluded unless --include-content | ||
| - Automatic reconnection with capped exponential backoff | ||
| - Graceful shutdown on SIGINT/SIGTERM (spans closed, exporters flushed) | ||
|
|
||
| Usage: | ||
| rocketride otel --apikey <key> | ||
| rocketride otel --endpoint http://localhost:4318 --service-name my-engine | ||
| rocketride otel --headers 'Langsmith-Project=my-project' --no-metrics | ||
| rocketride otel --include-content --apikey <key> | ||
|
|
||
| Secrets (collector auth values) are best passed via OTEL_EXPORTER_OTLP_HEADERS | ||
| rather than --headers: command-line arguments land in shell history and are | ||
| visible in process listings. | ||
|
|
||
| Requires the optional OpenTelemetry dependencies: | ||
| pip install 'rocketride[otel]' | ||
|
|
||
| Components: | ||
| OtelCommand: Main command implementation for the OpenTelemetry bridge | ||
| """ | ||
|
|
||
| import importlib.util | ||
| import sys | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from .base import BaseCommand | ||
| from ...otelbridge.bridge import run_bridge | ||
| from ...otelbridge.config import OtelConfig | ||
|
|
||
| # Safe without the 'otel' extra: setup.py keeps all opentelemetry imports lazy. | ||
| from ...otelbridge.setup import OtelNotInstalledError | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ..main import RocketRideClient | ||
|
|
||
| # Exact install hint for the missing-extra error path (contract: exit code 2) | ||
| OTEL_INSTALL_HINT = "pip install 'rocketride[otel]'" | ||
|
|
||
|
|
||
| def _otel_available() -> bool: | ||
| """ | ||
| Check whether the optional OpenTelemetry SDK is installed. | ||
|
|
||
| Probes for the ``opentelemetry`` and ``opentelemetry.sdk`` modules | ||
| without importing them, so the check itself never pays import cost and | ||
| never fails on a partially installed distribution. | ||
|
|
||
| Returns: | ||
| bool: True when the 'rocketride[otel]' extra appears importable. | ||
| """ | ||
| try: | ||
| return ( | ||
| importlib.util.find_spec('opentelemetry') is not None | ||
| and importlib.util.find_spec('opentelemetry.sdk') is not None | ||
| ) | ||
| except (ImportError, ValueError): | ||
| return False | ||
|
|
||
|
|
||
| class OtelCommand(BaseCommand): | ||
| """ | ||
| Command implementation for the OpenTelemetry bridge. | ||
|
|
||
| Exports live pipeline traces and metrics over OTLP by consuming the | ||
| engine's WebSocket monitor protocol. Runs until interrupted (Ctrl+C or | ||
| SIGTERM), closing open spans and flushing exporters on shutdown. | ||
|
|
||
| Example: | ||
| ```python | ||
| # Initialize and execute the OTel bridge | ||
| command = OtelCommand(cli, args) | ||
| exit_code = await command.execute(client) | ||
| ``` | ||
|
|
||
| Key Features: | ||
| - Lazy dependency guard: clear exit code 2 with install hint when | ||
| the 'rocketride[otel]' extra is missing (checked before connecting) | ||
| - Configuration precedence: CLI args > OTEL_* env vars > defaults | ||
| - Traces and metrics over one OTLP endpoint (http/protobuf default) | ||
| - Content privacy gate via --include-content | ||
| """ | ||
|
|
||
| def __init__(self, cli, args): | ||
| """ | ||
| Initialize OtelCommand with CLI context and parsed arguments. | ||
|
|
||
| Args: | ||
| cli: CLI instance providing cancellation state and event handling | ||
| args: Parsed command line arguments containing exporter options | ||
| and connection configuration | ||
| """ | ||
| super().__init__(cli, args) | ||
|
|
||
| async def execute(self, client: 'RocketRideClient') -> int: | ||
| """ | ||
| Execute the OpenTelemetry bridge command. | ||
|
|
||
| Verifies the optional OpenTelemetry dependencies are installed, | ||
| resolves the exporter configuration, and runs the bridge loop until | ||
| interrupted. | ||
|
|
||
| Args: | ||
| client: RocketRideClient instance for server communication | ||
| (connected by the bridge if not already connected) | ||
|
|
||
| Returns: | ||
| Exit code: 0 for graceful shutdown, 1 for unexpected errors, | ||
| 2 when dependencies are missing or the startup connection fails | ||
|
|
||
| Process Flow: | ||
| 1. Guard: missing 'rocketride[otel]' extra -> exit 2 with hint | ||
| (before any connection attempt) | ||
| 2. Resolve OtelConfig (CLI args > OTEL_* env vars > defaults) | ||
| 3. Print the effective bridge configuration | ||
| 4. Run the bridge loop (connect, subscribe, dispatch, reconnect) | ||
| 5. Graceful shutdown on SIGINT/SIGTERM: spans closed, exporters | ||
| flushed, exit 0 | ||
| """ | ||
| # Dependency guard MUST run before any connection attempt | ||
| if not _otel_available(): | ||
| print( | ||
| f"Error: 'rocketride otel' requires the OpenTelemetry extra. Install it with: {OTEL_INSTALL_HINT}", | ||
| file=sys.stderr, | ||
| ) | ||
| return 2 | ||
|
|
||
| # Resolve configuration: CLI args > OTEL_* env vars > defaults | ||
| config = OtelConfig.from_args_env(self.args) | ||
|
|
||
| # --trace-level is documentation-surface only: the monitor protocol | ||
| # has no way to change the trace level of runs the bridge didn't start | ||
| trace_level = getattr(self.args, 'trace_level', None) | ||
| if trace_level == 'none': | ||
| # 'none' disables flow tracing, so "emit FLOW events at that | ||
| # level" would be nonsense for it. | ||
| print( | ||
| "Note: --trace-level=none is informational only. 'none' means flow tracing stays " | ||
| 'disabled: runs started without a pipelineTraceLevel (or with ' | ||
| "pipelineTraceLevel='none') emit no FLOW events, so only task lifecycle spans " | ||
| 'and metrics are exported.' | ||
| ) | ||
| elif trace_level: | ||
| print( | ||
| f'Note: --trace-level={trace_level} is informational only. The bridge cannot change the ' | ||
| f'trace level of runs it did not start; start runs with ' | ||
| f"pipelineTraceLevel='{trace_level}' (e.g. client.use(pipelineTraceLevel='{trace_level}')) " | ||
| f'to emit FLOW events at that level.' | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| # Show the effective configuration before entering the run loop. | ||
| # With no endpoint configured the OTLP exporters resolve their own: | ||
| # OTEL_EXPORTER_OTLP_TRACES/METRICS_ENDPOINT, else the SDK default. | ||
| if config.endpoint: | ||
| endpoint_display = config.endpoint | ||
| else: | ||
| sdk_default = 'localhost:4317' if config.protocol == 'grpc' else 'http://localhost:4318' | ||
| endpoint_display = f'exporter default (OTEL_EXPORTER_OTLP_*_ENDPOINT or {sdk_default})' | ||
| print(f'OpenTelemetry bridge starting (endpoint: {endpoint_display}, protocol: {config.protocol})') | ||
| print(f' service.name: {config.service_name}') | ||
| print(f' metrics: {"disabled" if config.no_metrics else "enabled"}') | ||
| print(f' payload content: {"included (size-capped)" if config.include_content else "excluded"}') | ||
| print( | ||
| " FLOW spans require runs started with pipelineTraceLevel (e.g. client.use(pipelineTraceLevel='summary'))" | ||
| ) | ||
|
|
||
| try: | ||
| # Run the bridge until stopped (returns 0) or startup fails (2) | ||
| return await run_bridge(client, config) | ||
|
|
||
| except (ImportError, OtelNotInstalledError) as e: | ||
| # Missing optional dependency surfaced after the guard, e.g. | ||
| # --protocol grpc without the OTLP gRPC exporter package | ||
| # (build_providers raises OtelNotInstalledError for that path). | ||
| print(f'Error: {e}', file=sys.stderr) | ||
| return 2 | ||
|
|
||
| except KeyboardInterrupt: | ||
| # Fallback when signal handlers could not be installed | ||
| print('\nStopping OpenTelemetry bridge...') | ||
| return 0 | ||
|
|
||
| except Exception as e: | ||
| print(f'Error: OpenTelemetry bridge failed: {e}', file=sys.stderr) | ||
| return 1 | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Replace the unsupported
rrext_storecommand.RocketRideCLI.setup_parser()only registersstore;rocketride rrext_store get_all_projectswill be rejected as an unknown command. Replace it with a supportedrocketride store ...example or remove it.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verified and you're right that the example is wrong (
main.pyregistersstore, andget_all_projectsdoesn't exist) — butgit diffshows that line is pre-existing content this PR never touched (our diff only added the adjacentotelline). Keeping this PR's scope tight; flagged it for a separate docs fix.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nihalnihalani, agreed—thanks for verifying. The documentation issue is valid but unrelated to this PR’soteladdition, so deferring it to a focused docs fix is appropriate.Would you like me to open a follow-up GitHub issue for the invalid
rrext_store get_all_projectsexample?