Skip to content

feat(observability): add init_laminar_for_external helper for webhook integrations#2820

Merged
juanmichelini merged 1 commit intomainfrom
feat/add-init-laminar-for-external-helper
Apr 14, 2026
Merged

feat(observability): add init_laminar_for_external helper for webhook integrations#2820
juanmichelini merged 1 commit intomainfrom
feat/add-init-laminar-for-external-helper

Conversation

@juanmichelini
Copy link
Copy Markdown
Collaborator

@juanmichelini juanmichelini commented Apr 14, 2026

Summary

Add init_laminar_for_external() helper function 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.

Motivation

Currently, integrations like the GitHub resolver in OpenHands have to duplicate Laminar initialization and parent span context capture:

# Current approach (verbose, duplicated)
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()

This helper provides a simple one-liner that:

  • Calls maybe_init_laminar() (handles all env var configuration)
  • Registers LaminarLiteLLMCallback for automatic LLM tracing
  • Returns the parent span context for proper trace hierarchy

Usage

from openhands.sdk.observability import init_laminar_for_external
from lmnr import Laminar

# At the start of handling an external event (webhook, etc.)
laminar_span_context = init_laminar_for_external()

if laminar_span_context:
    with Laminar.start_as_current_span(
        name='my-integration',
        parent_span_context=laminar_span_context,
    ):
        await do_something()
else:
    await do_something()

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

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:9261201-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-9261201-python \
  ghcr.io/openhands/agent-server:9261201-python

All tags pushed for this build

ghcr.io/openhands/agent-server:9261201-golang-amd64
ghcr.io/openhands/agent-server:9261201-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:9261201-golang-arm64
ghcr.io/openhands/agent-server:9261201-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:9261201-java-amd64
ghcr.io/openhands/agent-server:9261201-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:9261201-java-arm64
ghcr.io/openhands/agent-server:9261201-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:9261201-python-amd64
ghcr.io/openhands/agent-server:9261201-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:9261201-python-arm64
ghcr.io/openhands/agent-server:9261201-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:9261201-golang
ghcr.io/openhands/agent-server:9261201-java
ghcr.io/openhands/agent-server:9261201-python

About Multi-Architecture Support

  • Each variant tag (e.g., 9261201-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 9261201-python-amd64) are also available if needed

… 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.
@github-actions
Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@juanmichelini
Copy link
Copy Markdown
Collaborator Author

@OpenHands /codereview-roasted

@openhands-ai
Copy link
Copy Markdown

openhands-ai bot commented Apr 14, 2026

I'm on it! juanmichelini can track my progress at all-hands.dev

Copy link
Copy Markdown
Collaborator Author

@juanmichelini juanmichelini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

@openhands-ai
Copy link
Copy Markdown

openhands-ai bot commented Apr 14, 2026

Summary of Work Completed

I performed a code review on PR #2820 ("feat(observability): add init_laminar_for_external helper for webhook integrations") as requested via the /codereview-roasted command.

What I did:

  1. Retrieved PR details via GitHub API (title, state, description, file changes)
  2. Analyzed the code changes - The PR adds a 4-line init_laminar_for_external() function that composes maybe_init_laminar() and should_enable_observability() to eliminate boilerplate in webhook/integration handlers
  3. Applied Linus-style code review - Evaluated for data structure choices, complexity, pragmatism, breaking changes, and correctness
  4. Posted review comment to the PR with verdict: ✅ Worth merging

Key findings:

  • No critical issues - Clean implementation, no nesting problems, pure additive change
  • Good taste - The solution is simple and solves a real duplication problem
  • No breaking changes - Only exports a new function, doesn't modify existing behavior

The review has been posted at: #2820 (review)

@juanmichelini juanmichelini marked this pull request as ready for review April 14, 2026 02:39
Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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:4317

Step 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:

  1. ✓ One-liner instead of multiple lines
  2. ✓ Handles ALL env var configurations (via should_enable_observability())
  3. ✓ Automatically registers LiteLLM callback
  4. ✓ Works with both Laminar and non-Laminar OTEL backends
  5. ✓ 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.

@juanmichelini juanmichelini merged commit ed0eb38 into main Apr 14, 2026
39 checks passed
@juanmichelini juanmichelini deleted the feat/add-init-laminar-for-external-helper branch April 14, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants