feat(observability): add init_laminar_for_external helper for webhook integrations#2820
Conversation
… integrations This helper is designed for external integrations (GitHub, Slack, etc.) that need to: 1. Initialize Laminar if env vars are set 2. Capture the parent span context from the webhook trigger This enables proper trace hierarchy: webhook → integration handler → agent. The helper is a thin wrapper around maybe_init_laminar() and Laminar.get_laminar_span_context(), providing a simple one-liner for integrations to use instead of duplicating this logic.
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
|
@OpenHands /codereview-roasted |
|
I'm on it! juanmichelini can track my progress at all-hands.dev |
juanmichelini
left a comment
There was a problem hiding this comment.
🟢 Good taste - Elegant, simple solution. The PR adds a thin, well-documented wrapper that eliminates boilerplate duplication in webhook/integration handlers. No nesting issues, no breaking changes, no complexity concerns.
[STYLE NOTES] (nitpicks only)
- The docstring example in the function body is duplicated in the PR description. Consider keeping it in one place (either the docstring or the PR description, not both).
- No test added, but this is just a composition of existing functions. The value of testing this wrapper vs testing the underlying
maybe_init_laminar()is marginal.
VERDICT: ✅ Worth merging
KEY INSIGHT: The implementation is 4 lines of logic composing two existing functions. This is exactly the kind of DRY improvement that makes APIs pleasant to use.
Summary of Work CompletedI performed a code review on PR #2820 ("feat(observability): add init_laminar_for_external helper for webhook integrations") as requested via the What I did:
Key findings:
The review has been posted at: #2820 (review) |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean 4-line wrapper that eliminates real duplication. No complexity, no special cases, no breaking changes. Exactly what a helper function should be.
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
The new init_laminar_for_external() helper successfully simplifies Laminar initialization for webhook integrations, replacing verbose boilerplate with a clean one-liner.
Does this PR achieve its stated goal?
Yes. The PR delivers exactly what it promises: a convenience helper that (1) initializes Laminar if env vars are set via maybe_init_laminar(), (2) captures parent span context for proper trace hierarchy, and (3) as a bonus, automatically registers the LiteLLM callback for LLM tracing. The function works correctly in both enabled and disabled states, properly exports through the public API, and includes clear documentation with usage examples.
| Phase | Result |
|---|---|
| Environment Setup | ✅ Dependencies installed, project builds cleanly |
| CI & Tests | ✅ 27/27 checks passed (pre-commit, SDK tests, API checks, agent-server) |
| Functional Verification | ✅ All core behaviors verified, usage pattern validated |
Functional Verification
Test 1: Public API Export
Verified the function is properly exported:
uv run python -c "from openhands.sdk.observability import __all__; print(__all__)"Output:
['init_laminar_for_external', 'maybe_init_laminar', 'observe']
✓ Function is correctly exported in __all__ and importable from openhands.sdk.observability.
Test 2: Behavior Without Observability (Baseline)
Step 1 — Establish baseline (no env vars set):
Created test script that clears all observability env vars and calls the function:
import os
# Clear all observability env vars
for var in ['LMNR_PROJECT_API_KEY', 'OTEL_ENDPOINT',
'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT']:
os.environ.pop(var, None)
from openhands.sdk.observability import init_laminar_for_external
result = init_laminar_for_external()Ran uv run python /tmp/test_init_laminar_for_external.py:
Verifying no observability env vars are set:
LMNR_PROJECT_API_KEY: (not set)
OTEL_ENDPOINT: (not set)
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: (not set)
OTEL_EXPORTER_OTLP_ENDPOINT: (not set)
Calling init_laminar_for_external()...
Result: None
Result type: <class 'NoneType'>
✓ Correctly returned None when observability is disabled
This confirms the function gracefully handles the disabled state without errors.
Test 3: Behavior With Observability Enabled
Step 1 — Set observability env var:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://fake-endpoint:4317Step 2 — Call the helper and verify initialization:
result = init_laminar_for_external()
print(f"Result: {result}")
print(f"Laminar initialized: {Laminar.is_initialized()}")Output:
Set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://fake-endpoint:4317
Calling init_laminar_for_external()...
Result: None
Result type: <class 'NoneType'>
✓ Function executed successfully with observability enabled
Laminar initialized: True
✓ Laminar was properly initialized
✓ Laminar is initialized when env vars are present. The span context returns None because there's no active parent span (expected behavior — the function returns None when no parent context exists, which is normal for fresh webhook handlers).
Test 4: LiteLLM Callback Registration
Verified automatic callback registration:
import litellm
from lmnr import LaminarLiteLLMCallback
has_laminar_callback = any(
isinstance(cb, LaminarLiteLLMCallback) for cb in litellm.callbacks
)Output:
LiteLLM callbacks: 1
Has LaminarLiteLLMCallback: True
✓ LaminarLiteLLMCallback was properly registered
✓ The function automatically registers the LiteLLM callback for LLM tracing, which the old verbose approach required manual handling.
Test 5: Webhook Integration Pattern
Validated the documented usage pattern:
Implemented the example from the PR description:
from openhands.sdk.observability import init_laminar_for_external
from lmnr import Laminar
laminar_span_context = init_laminar_for_external()
if laminar_span_context:
with Laminar.start_as_current_span(
name='webhook-handler',
parent_span_context=laminar_span_context,
):
result = process_webhook()
else:
result = process_webhook()Output:
1. Initializing Laminar for external integration...
Span context: None
Span context type: <class 'NoneType'>
2. Processing without observability (no span context)...
Webhook processing result: Webhook processed successfully
✓ Webhook handler completed successfully
✓ The usage pattern works correctly and handles both enabled/disabled states gracefully.
Test 6: Comparison with Old Approach
Before (old verbose approach from PR description):
LAMINAR_ENABLED = os.environ.get('LMNR_PROJECT_API_KEY', '') != ''
if LAMINAR_ENABLED:
Laminar.initialize(project_api_key=...)
laminar_span_context = Laminar.get_laminar_span_context()Limitations:
- Only checks
LMNR_PROJECT_API_KEY(misses other OTEL env vars) - Doesn't register LiteLLM callback
- Requires duplication across integrations
After (new helper):
from openhands.sdk.observability import init_laminar_for_external
laminar_span_context = init_laminar_for_external()Benefits verified:
- ✓ One-liner instead of multiple lines
- ✓ Handles ALL env var configurations (via
should_enable_observability()) - ✓ Automatically registers LiteLLM callback
- ✓ Works with both Laminar and non-Laminar OTEL backends
- ✓ Centralized logic eliminates duplication
Issues Found
None.
Verdict: This PR is ready to merge. The helper function delivers exactly what external integrations need: a simple, robust way to initialize observability without duplicating complex setup logic.
Summary
Add
init_laminar_for_external()helper function for external integrations (GitHub, Slack, etc.) that need to:This enables proper trace hierarchy: webhook → integration handler → agent.
Motivation
Currently, integrations like the GitHub resolver in OpenHands have to duplicate Laminar initialization and parent span context capture:
This helper provides a simple one-liner that:
maybe_init_laminar()(handles all env var configuration)LaminarLiteLLMCallbackfor automatic LLM tracingUsage
Notes
This PR was co-authored by an AI agent on behalf of the user.
@juanmichelini can click here to continue refining the PR
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:9261201-pythonRun
All tags pushed for this build
About Multi-Architecture Support
9261201-python) is a multi-arch manifest supporting both amd64 and arm649261201-python-amd64) are also available if needed