1
0
mirror of synced 2026-08-05 19:17:10 +00:00

feat(observability): add Dash0 as partner observability integration (#1539)

feat(observability): add Dash0 as partner observability integration

Signed-off-by: Julia Furst Morgado <52685951+juliafmorgado@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julia Furst Morgado
2026-07-20 15:36:01 -04:00
committed by GitHub
parent 423e7d7f5d
commit 580e6b5e00
9 changed files with 545 additions and 0 deletions
@@ -0,0 +1,16 @@
# Dash0 credentials — get from https://app.dash0.com → Settings → Auth Tokens
DASH0_AUTH_TOKEN=your-dash0-auth-token
# OTLP ingress base URL for your region — get from Settings → Endpoints
# US West 2: https://ingress.us-west-2.aws.dash0.com
# EU West 1: https://ingress.eu-west-1.aws.dash0.com
DASH0_OTLP_ENDPOINT=https://ingress.us-west-2.aws.dash0.com
# Dataset to route telemetry to (default works for most setups)
DASH0_DATASET=default
# Service name visible in Dash0 tracing, metrics, and logs
OTEL_SERVICE_NAME=agentcore-travel-agent
# AWS region
AWS_DEFAULT_REGION=us-east-1
@@ -0,0 +1,68 @@
# AgentCore + Dash0 observability
Deploy a Strands travel agent to AgentCore Runtime with traces, metrics, and logs sent to [Dash0](https://www.dash0.com/) via OTLP HTTP.
## Architecture
```
AgentCore runtime → utils/travel_agent.py
└── OTel SDK
├── OTLPSpanExporter → {DASH0_OTLP_ENDPOINT}/v1/traces
├── OTLPMetricExporter → {DASH0_OTLP_ENDPOINT}/v1/metrics
└── OTLPLogExporter → {DASH0_OTLP_ENDPOINT}/v1/logs
headers: Authorization: Bearer <token>, Dash0-Dataset: <dataset>
└── Dash0 → Tracing, Metrics, Logs
```
`DISABLE_ADOT_OBSERVABILITY=true` bypasses the default CloudWatch ADOT pipeline so Dash0 receives all telemetry.
## Prerequisites
- Python 3.10+, [uv](https://docs.astral.sh/uv/)
- AWS credentials configured
- [Dash0 account](https://www.dash0.com/)
## Quick Start
```bash
pip install bedrock-agentcore boto3 python-dotenv
cp .env.example .env
# Edit .env: set DASH0_AUTH_TOKEN and DASH0_OTLP_ENDPOINT for your region
python deploy.py
python invoke.py
# View telemetry: https://app.dash0.com → Tracing / Metrics / Logs
python cleanup.py
```
## Dash0 Regions
| Region | DASH0_OTLP_ENDPOINT |
|:-------|:--------------------|
| US West 2 (default) | `https://ingress.us-west-2.aws.dash0.com` |
| EU West 1 | `https://ingress.eu-west-1.aws.dash0.com` |
> Find your endpoint at **app.dash0.com → Settings → Endpoints**.
## Environment Variables
| Variable | Description | Default |
|:---------|:------------|:--------|
| `DASH0_AUTH_TOKEN` | Auth token from **Settings → Auth Tokens** | _(required)_ |
| `DASH0_OTLP_ENDPOINT` | OTLP ingress base URL for your region | `https://ingress.us-west-2.aws.dash0.com` |
| `DASH0_DATASET` | Dataset to route telemetry to | `default` |
| `OTEL_SERVICE_NAME` | Service name shown in Dash0 | `agentcore-travel-agent` |
## Files
| File | Description |
|:-----|:------------|
| `utils/travel_agent.py` | Agent with Dash0 OTel setup (traces, metrics, logs) |
| `deploy.py` | Deploys to AgentCore Runtime with Dash0 env vars |
| `invoke.py` | Invokes the deployed agent with sample travel prompts |
| `cleanup.py` | Deletes all created AWS resources |
## Additional Resources
- [Dash0 Documentation](https://dash0.com/docs)
- [Dash0 Endpoints glossary](https://dash0.com/docs/dash0/miscellaneous/glossary/endpoints)
- [AgentCore observability](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html)
@@ -0,0 +1,53 @@
"""Delete all resources created by deploy.py. Reads runtime_config.json."""
import json
import time
import boto3
with open("runtime_config.json") as f:
config = json.load(f)
region = config["region"]
runtime_id = config["runtime_id"]
control = boto3.client("bedrock-agentcore-control", region_name=region)
iam = boto3.client("iam", region_name=region)
s3 = boto3.client("s3", region_name=region)
# Delete endpoint
print("Deleting endpoint 'default'...")
try:
control.delete_agent_runtime_endpoint(agentRuntimeId=runtime_id, name="default")
time.sleep(5)
except Exception as e:
print(f" {e}")
# Delete runtime
print(f"Deleting runtime {runtime_id}...")
try:
control.delete_agent_runtime(agentRuntimeId=runtime_id)
except Exception as e:
print(f" {e}")
# Delete IAM role
role_name = config.get("role_name")
if role_name:
print(f"Deleting IAM role {role_name}...")
try:
for policy in iam.list_role_policies(RoleName=role_name).get("PolicyNames", []):
iam.delete_role_policy(RoleName=role_name, PolicyName=policy)
iam.delete_role(RoleName=role_name)
except Exception as e:
print(f" {e}")
# Delete S3 objects
s3_bucket = config.get("s3_bucket")
s3_prefix = config.get("s3_prefix")
if s3_bucket and s3_prefix:
print(f"Deleting s3://{s3_bucket}/{s3_prefix}...")
try:
s3.delete_object(Bucket=s3_bucket, Key=s3_prefix)
except Exception as e:
print(f" {e}")
print("Cleanup complete.")
@@ -0,0 +1,234 @@
"""
Deploy the Travel Agent with Dash0 observability to AgentCore Runtime.
Usage:
cp .env.example .env # fill in your Dash0 auth token
python deploy.py
"""
import json
import os
import shutil
import subprocess
import sys
import time
import boto3
from boto3.session import Session
from dotenv import load_dotenv
load_dotenv()
AGENT_NAME = f"dash0_obs_{int(time.time()) % 100000}"
PROTOCOL = "HTTP"
PYTHON_RUNTIME = "PYTHON_3_13"
ENTRY_POINT = "travel_agent.py"
AGENT_FILES = ["utils/travel_agent.py"]
PLATFORM_ENV_VARS = {
"DASH0_AUTH_TOKEN": os.getenv("DASH0_AUTH_TOKEN", ""),
"DASH0_OTLP_ENDPOINT": os.getenv("DASH0_OTLP_ENDPOINT", "https://ingress.us-west-2.aws.dash0.com"),
"DASH0_DATASET": os.getenv("DASH0_DATASET", "default"),
"OTEL_SERVICE_NAME": os.getenv("OTEL_SERVICE_NAME", "agentcore-travel-agent"),
"DISABLE_ADOT_OBSERVABILITY": "true",
"BEDROCK_MODEL_ID": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
}
session = Session()
REGION = session.region_name
ACCOUNT_ID = session.client("sts").get_caller_identity()["Account"]
S3_BUCKET = f"agentcore-code-{ACCOUNT_ID}-{REGION}"
S3_PREFIX = f"{AGENT_NAME}/code.zip"
print(f"Region: {REGION}\nAccount: {ACCOUNT_ID}\nAgent: {AGENT_NAME}")
def create_execution_role() -> str:
iam = boto3.client("iam", region_name=REGION)
role_name = f"agentcore-{AGENT_NAME}-role"
trust_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {"StringEquals": {"aws:SourceAccount": ACCOUNT_ID}},
}
],
}
inline_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["logs:DescribeLogStreams", "logs:CreateLogGroup"],
"Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*"],
},
{
"Effect": "Allow",
"Action": ["logs:DescribeLogGroups"],
"Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:*"],
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"
],
},
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
f"arn:aws:bedrock:{REGION}:{ACCOUNT_ID}:*",
],
},
],
}
try:
resp = iam.create_role(
RoleName=role_name,
AssumeRolePolicyDocument=json.dumps(trust_policy),
Description=f"Execution role for {AGENT_NAME}",
)
role_arn = resp["Role"]["Arn"]
print(f"\nCreated IAM role: {role_arn}")
except iam.exceptions.EntityAlreadyExistsException:
role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}"
print(f"\nIAM role exists: {role_arn}")
iam.put_role_policy(
RoleName=role_name,
PolicyName=f"{AGENT_NAME}-policy",
PolicyDocument=json.dumps(inline_policy),
)
time.sleep(10)
return role_arn
def build_and_upload_package():
s3 = boto3.client("s3", region_name=REGION)
pkg_dir, zip_file = "deployment_package", "deployment_package.zip"
try:
if REGION == "us-east-1":
s3.create_bucket(Bucket=S3_BUCKET)
else:
s3.create_bucket(
Bucket=S3_BUCKET,
CreateBucketConfiguration={"LocationConstraint": REGION},
)
except (s3.exceptions.BucketAlreadyOwnedByYou, s3.exceptions.BucketAlreadyExists):
pass
if os.path.isdir(pkg_dir):
shutil.rmtree(pkg_dir)
if os.path.exists(zip_file):
os.remove(zip_file)
subprocess.run(
[
"uv",
"pip",
"install",
"--python-platform",
"aarch64-manylinux2014",
"--python-version",
"3.13",
"--target",
pkg_dir,
"--only-binary",
":all:",
"-r",
"requirements.txt",
],
check=True,
)
subprocess.run(
["zip", "-r", f"../{zip_file}", "."],
cwd=pkg_dir,
check=True,
capture_output=True,
)
for src_file in AGENT_FILES:
subprocess.run(["zip", zip_file, "-j", src_file], check=True, capture_output=True)
s3.upload_file(zip_file, S3_BUCKET, S3_PREFIX)
shutil.rmtree(pkg_dir)
os.remove(zip_file)
print(f" Package uploaded to s3://{S3_BUCKET}/{S3_PREFIX}")
def create_runtime(role_arn: str) -> dict:
control = boto3.client("bedrock-agentcore-control", region_name=REGION)
response = control.create_agent_runtime(
agentRuntimeName=AGENT_NAME,
agentRuntimeArtifact={
"codeConfiguration": {
"code": {"s3": {"bucket": S3_BUCKET, "prefix": S3_PREFIX}},
"runtime": PYTHON_RUNTIME,
"entryPoint": [ENTRY_POINT],
}
},
roleArn=role_arn,
networkConfiguration={"networkMode": "PUBLIC"},
protocolConfiguration={"serverProtocol": PROTOCOL},
environmentVariables=PLATFORM_ENV_VARS,
description="Travel agent with Dash0 observability",
)
runtime_id, runtime_arn = response["agentRuntimeId"], response["agentRuntimeArn"]
print(f" Runtime created: {runtime_id}")
while True:
status_resp = control.get_agent_runtime(agentRuntimeId=runtime_id)
status = status_resp["status"]
print(f" Status: {status}")
if status == "READY":
break
if status in ("CREATE_FAILED", "UPDATE_FAILED"):
sys.exit(1)
time.sleep(15)
return {"runtime_id": runtime_id, "runtime_arn": runtime_arn}
def create_endpoint(runtime_id: str):
control = boto3.client("bedrock-agentcore-control", region_name=REGION)
control.create_agent_runtime_endpoint(agentRuntimeId=runtime_id, name="default")
while True:
for ep in control.list_agent_runtime_endpoints(agentRuntimeId=runtime_id).get("runtimeEndpoints", []):
if ep["name"] == "default":
print(f" Status: {ep['status']}")
if ep["status"] == "READY":
return ep
if ep["status"] in ("CREATE_FAILED", "UPDATE_FAILED"):
sys.exit(1)
time.sleep(15)
def main():
if not PLATFORM_ENV_VARS.get("DASH0_AUTH_TOKEN"):
print("ERROR: DASH0_AUTH_TOKEN not set. Copy .env.example → .env and fill in your credentials.")
sys.exit(1)
print("=" * 60)
print("Deploying Travel Agent with Dash0 Observability")
print("=" * 60)
role_arn = create_execution_role()
build_and_upload_package()
runtime = create_runtime(role_arn)
create_endpoint(runtime["runtime_id"])
config = {
"agent_name": AGENT_NAME,
"runtime_id": runtime["runtime_id"],
"runtime_arn": runtime["runtime_arn"],
"region": REGION,
"role_name": f"agentcore-{AGENT_NAME}-role",
"s3_bucket": S3_BUCKET,
"s3_prefix": S3_PREFIX,
}
with open("runtime_config.json", "w") as f:
json.dump(config, f, indent=2)
print(f"\nDeployment complete! Runtime ARN: {runtime['runtime_arn']}")
print("Next: python invoke.py | Open app.dash0.com → Tracing")
if __name__ == "__main__":
main()
@@ -0,0 +1,29 @@
"""Invoke the deployed travel agent. Reads runtime_config.json written by deploy.py."""
import json
import boto3
with open("runtime_config.json") as f:
config = json.load(f)
client = boto3.client("bedrock-agentcore", region_name=config["region"])
prompts = [
"I'm planning a weekend trip to Kyoto in spring. What are the must-visit places?",
"What are the best beaches in Thailand for a budget traveler?",
"Suggest a 5-day itinerary for first-time visitors to Paris.",
]
for prompt in prompts:
print(f"\nPrompt: {prompt}")
print("-" * 60)
response = client.invoke_agent_runtime(
agentRuntimeArn=config["runtime_arn"],
qualifier="DEFAULT",
payload=json.dumps({"prompt": prompt}).encode(),
)
body = response["response"].read().decode()
try:
print(json.loads(body))
except json.JSONDecodeError:
print(body)
@@ -0,0 +1,8 @@
bedrock-agentcore>=1.5.0
boto3
strands-agents[otel]
strands-agents-tools
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-http
ddgs
python-dotenv
@@ -0,0 +1,135 @@
"""
Travel agent for AgentCore Runtime with Dash0 observability.
Configures OTel TracerProvider, MeterProvider, and LoggerProvider exporting
traces, metrics, and logs to Dash0's OTLP HTTP endpoint via Bearer token auth.
DISABLE_ADOT_OBSERVABILITY=true bypasses CloudWatch so Dash0 receives the telemetry.
Required env vars (set via deploy.py → create_agent_runtime environmentVariables):
DASH0_AUTH_TOKEN — Auth token from Settings → Auth Tokens
DASH0_OTLP_ENDPOINT — OTLP ingress base URL (default: https://ingress.us-west-2.aws.dash0.com)
DASH0_DATASET — Dataset name (default: default)
OTEL_SERVICE_NAME — Service name visible in Dash0
DISABLE_ADOT_OBSERVABILITY — must be "true"
"""
import logging
import os
logging.basicConfig(level=logging.ERROR, format="[%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
logger.setLevel(os.getenv("AGENT_RUNTIME_LOG_LEVEL", "INFO").upper())
# ── Dash0 OTel Setup ───────────────────────────────────────────────────────────
# Must be configured BEFORE any other OTel imports.
auth_token = os.environ.get("DASH0_AUTH_TOKEN", "")
otlp_base = os.environ.get("DASH0_OTLP_ENDPOINT", "https://ingress.us-west-2.aws.dash0.com").rstrip("/")
dataset = os.environ.get("DASH0_DATASET", "default")
service_name = os.environ.get("OTEL_SERVICE_NAME", "agentcore-travel-agent")
if auth_token:
from opentelemetry import metrics, trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk._logs import LoggingHandler
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
headers = {"Authorization": f"Bearer {auth_token}", "Dash0-Dataset": dataset}
resource = Resource.create({"service.name": service_name})
# Traces
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(
SimpleSpanProcessor(OTLPSpanExporter(endpoint=f"{otlp_base}/v1/traces", headers=headers))
)
trace.set_tracer_provider(trace_provider)
# Metrics
metrics.set_meter_provider(
MeterProvider(
resource=resource,
metric_readers=[
PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=f"{otlp_base}/v1/metrics", headers=headers))
],
)
)
# Logs
log_provider = LoggerProvider(resource=resource)
log_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(endpoint=f"{otlp_base}/v1/logs", headers=headers))
)
set_logger_provider(log_provider)
logging.getLogger().addHandler(LoggingHandler(level=logging.NOTSET, logger_provider=log_provider))
logger.info(
"Dash0 OTel configured (service: %s, dataset: %s, endpoint: %s)",
service_name,
dataset,
otlp_base,
)
else:
logger.warning("DASH0_AUTH_TOKEN not set — telemetry will not be sent to Dash0")
# ── Agent ──────────────────────────────────────────────────────────────────────
from bedrock_agentcore.runtime import BedrockAgentCoreApp # noqa: E402
from ddgs import DDGS # noqa: E402
from strands import Agent, tool # noqa: E402
from strands.models import BedrockModel # noqa: E402
app = BedrockAgentCoreApp()
@tool
def web_search(query: str) -> str:
"""Search the web for current travel information."""
try:
ddgs = DDGS()
results = ddgs.text(query, max_results=5)
formatted = []
for i, r in enumerate(results, 1):
formatted.append(
f"{i}. {r.get('title', 'No title')}\n"
f" {r.get('body', 'No summary')}\n"
f" Source: {r.get('href', 'No URL')}\n"
)
return "\n".join(formatted) if formatted else "No results found."
except Exception as e:
return f"Search error: {str(e)}"
def create_agent():
model_id = os.getenv("BEDROCK_MODEL_ID", "global.anthropic.claude-haiku-4-5-20251001-v1:0")
region = os.getenv("AWS_DEFAULT_REGION", "us-east-1")
model = BedrockModel(model_id=model_id, region_name=region, temperature=0.0, max_tokens=1024)
return Agent(
model=model,
system_prompt=(
"You are an experienced travel agent. Use web_search for destination research "
"and provide concise, well-sourced recommendations."
),
tools=[web_search],
)
@app.entrypoint
def invoke(payload, context=None):
user_input = payload.get("prompt", "")
logger.info("[%s] %s", getattr(context, "session_id", "local"), user_input)
agent = create_agent()
response = agent(user_input)
return response.message["content"][0]["text"]
if __name__ == "__main__":
app.run()
+1
View File
@@ -40,6 +40,7 @@ This folder contains framework and protocol integrations that demonstrate how to
* **[Arize](./observability/arize/)**: LLM observability and evaluation with Arize Phoenix
* **[Braintrust](./observability/braintrust/)**: AI evaluation and observability platform integration
* **[Datadog](./observability/datadog/)**: Infrastructure and LLM monitoring with Datadog
* **[Dash0](./3p-observability/dash0/)**: OpenTelemetry-native observability platform with traces, metrics, and logs via OTLP HTTP
* **[Dynatrace](./observability/dynatrace/)**: Application performance monitoring integration with travel agent example
* **[Honeycomb](./observability/honeycomb/)**: Distributed tracing and observability with Honeycomb
* **[Instana](./observability/instana/)**: IBM Instana application performance monitoring
+1
View File
@@ -118,6 +118,7 @@
- Cristiano Scandura (scandura)
- palbiren
- Gui Ruggiero (guiruggiero)
- Julia Furst Morgado (juliafmorgado)
- Visakh Madathil (vmmadathil)
- JobRamos (jobdram)
- Will Matos (wilmatos)