For the complete documentation index, see llms.txt. Markdown versions are available by appending .md to documentation URLs.

Daytona Sandbox Monitoring & Tracing with OpenTelemetry

SigNoz Cloud - This page applies to SigNoz Cloud editions.
Self-Host - This page applies to self-hosted SigNoz editions.

Overview

Daytona runs AI-generated code in isolated sandboxes. Your agent creates a sandbox, executes code in it, reads files back, and tears it down. Two things go wrong in that loop, and each needs its own telemetry: the work inside the sandbox fails or hangs, and the sandbox lifecycle itself gets slow or errors out.

Prerequisites

  • An instance of SigNoz (either Cloud or Self-Hosted)
  • A Daytona account and an API key with write:sandboxes permission
  • Python 3.10 or later, with the daytona SDK installed. The SDK requires 3.10.
  • A Daytona plan that allows per-sandbox network rules. Tier 1 and Tier 2 accounts cannot override the default egress policy, which blocks SigNoz. See Network Limits.

How it works

Daytona exposes four OpenTelemetry paths. They do not overlap, so pick the ones that answer your question.

PathWhat you getWhat it costs you
App instrumentation inside the sandboxSpans, logs, and custom metrics from the code your agent runsCode you write and ship into the sandbox
Daytona's built-in telemetryPer-sandbox CPU, memory, and disk, plus toolbox API spansOne dashboard setting
Organization quota metricsCPU, memory, storage, and GPU used against your org quotaThe same dashboard setting
SDK tracingLatency and failures for create, start, stop, delete, file, and process callsOne flag on the client

Start with the first path. It shows what your agent did inside the sandbox, which nothing else reconstructs. The rest are collapsed below; expand them when you need them. All four use OpenTelemetry and export to the same SigNoz endpoint.

Monitor Work Inside a Daytona Sandbox

This path shows what your agent did: which steps ran, how long each took, what it logged, and where it failed.

Step 1: Allow SigNoz through the sandbox firewall

Sandbox egress is deny-by-default. Package registries stay reachable and Daytona blocks the rest, so an OTLP exporter fails with a TLS reset until you allow the ingestion host. Pass domain_allow_list when you create the sandbox:

run_agent.py
import asyncio
 
from daytona import AsyncDaytona, DaytonaConfig, CreateSandboxFromSnapshotParams
 
 
async def main():
    async with AsyncDaytona(DaytonaConfig(api_key="<your-daytona-api-key>")) as daytona:
        sandbox = await daytona.create(
            CreateSandboxFromSnapshotParams(
                domain_allow_list="ingest.<region>.signoz.cloud,pypi.org,files.pythonhosted.org,*.daytona.io",
                env_vars={
                    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://ingest.<region>.signoz.cloud:443",
                    "OTEL_EXPORTER_OTLP_HEADERS": "signoz-ingestion-key=<your-ingestion-key>",
                    "OTEL_SERVICE_NAME": "<your-service-name>",
                },
            )
        )
        print("created", sandbox.id)
 
 
asyncio.run(main())

Verify these values:

  • <region>: Your SigNoz Cloud region.
  • <your-ingestion-key>: Your SigNoz ingestion key.
  • <your-daytona-api-key>: Created under Daytona Dashboard, Keys.
  • <your-service-name>: What your sandbox telemetry appears under in SigNoz, and what you filter on in Validate.

The allow list takes at most 20 comma-separated domains and supports wildcards such as *.example.com. It is mutually exclusive with networkBlockAll and networkAllowList, so set only one of the three per sandbox. Setting a list also drops the default allowances, so include the registries your code installs from.

The env_vars values reach every process in the sandbox, so the OpenTelemetry SDK picks up the endpoint and headers with no further configuration.

Step 2: Instrument the code that runs inside the sandbox

This is ordinary OpenTelemetry setup apart from the resource attributes, which read environment variables Daytona injects into every sandbox. Use them to scope telemetry to one sandbox later. The service name needs no line of its own, because Resource.create() reads OTEL_SERVICE_NAME from the environment you set in Step 1.

sandbox_app.py
import logging
import os
 
from opentelemetry import metrics, trace
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, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
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 BatchSpanProcessor
 
# service.name is not set here: Resource.create() runs OpenTelemetry's
# environment detector, which picks up the OTEL_SERVICE_NAME set in Step 1.
resource = Resource.create(
    {
        "daytona.sandbox.id": os.environ.get("DAYTONA_SANDBOX_ID", ""),
        "daytona.organization.id": os.environ.get("DAYTONA_ORGANIZATION_ID", ""),
        "daytona.region.id": os.environ.get("DAYTONA_REGION_ID", ""),
        "daytona.snapshot": os.environ.get("DAYTONA_SANDBOX_SNAPSHOT", ""),
    }
)
 
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(tracer_provider)
 
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
logging.getLogger().addHandler(LoggingHandler(logger_provider=logger_provider))
logging.getLogger().setLevel(logging.INFO)
 
reader = PeriodicExportingMetricReader(OTLPMetricExporter())
meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(meter_provider)
 
tracer = trace.get_tracer("agent-work")
meter = metrics.get_meter("agent-work")
tasks = meter.create_counter("sandbox.tasks.completed")
log = logging.getLogger("sandbox-app")
 
with tracer.start_as_current_span("agent.task") as span:
    span.set_attribute("task.kind", "code-execution")
    log.info("starting work")
    with tracer.start_as_current_span("agent.step.compute"):
        total = sum(i * i for i in range(200000))
    log.info("compute finished with total %s", total)
    tasks.add(1, {"task.kind": "code-execution"})
 
tracer_provider.force_flush()
logger_provider.force_flush()
meter_provider.force_flush()

Flush all three providers before the process exits. Sandbox processes are short-lived, and an unflushed batch dies with the process.

Step 3: Upload the script, install the SDK, and run it

run_agent.py
import asyncio
 
from daytona import AsyncDaytona, DaytonaConfig, CreateSandboxFromSnapshotParams
 
 
async def main():
    async with AsyncDaytona(DaytonaConfig(api_key="<your-daytona-api-key>")) as daytona:
        sandbox = await daytona.create(
            CreateSandboxFromSnapshotParams(
                domain_allow_list="ingest.<region>.signoz.cloud,pypi.org,files.pythonhosted.org,*.daytona.io",
                env_vars={
                    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://ingest.<region>.signoz.cloud:443",
                    "OTEL_EXPORTER_OTLP_HEADERS": "signoz-ingestion-key=<your-ingestion-key>",
                    "OTEL_SERVICE_NAME": "<your-service-name>",
                },
            )
        )
 
        with open("sandbox_app.py", "rb") as f:
            await sandbox.fs.upload_file(f.read(), "/home/daytona/sandbox_app.py")
 
        await sandbox.process.exec(
            "pip install --quiet opentelemetry-sdk opentelemetry-exporter-otlp-proto-http"
        )
 
        result = await sandbox.process.exec("python3 /home/daytona/sandbox_app.py")
        print(result.exit_code, result.result)
 
 
asyncio.run(main())

To skip the install on every run, bake the OpenTelemetry packages into a snapshot and create sandboxes from it.

Trace Daytona SDK Operations

SDK tracing covers the other half: how long Daytona itself takes to create, start, stop, and delete sandboxes, and which of those calls fail. It runs in your application process, not in the sandbox, so the firewall rules above do not apply.

Set otel_enabled on the client and point the standard OpenTelemetry variables at SigNoz:

export DAYTONA_API_KEY="<your-daytona-api-key>"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
export OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
sdk_tracing.py
import asyncio
 
from daytona import AsyncDaytona, DaytonaConfig
 
 
async def main():
    # The API key is read from DAYTONA_API_KEY, exported above.
    async with AsyncDaytona(DaytonaConfig(otel_enabled=True)) as daytona:
        sandbox = await daytona.create()
        await sandbox.process.code_run('print("hello")')
        await daytona.delete(sandbox)
    # Traces flush when the context manager exits
 
 
asyncio.run(main())

Set DAYTONA_OTEL_ENABLED=true instead of the constructor argument if you would rather keep it in the environment. Daytona also ships TypeScript, Ruby, Go, and Java SDKs with the same flag; see OpenTelemetry Collection.

The Python SDK bundles only the OTLP HTTP exporter, so http/protobuf is the only protocol. Do not set OTEL_EXPORTER_OTLP_PROTOCOL=grpc.

Spans flush when you close the client. The async client exposes close() and works as a context manager. The synchronous Daytona client has neither, so its spans flush only at interpreter shutdown.

Collect Daytona's Built-in Telemetry

One dashboard setting turns on everything Daytona instruments itself: per-sandbox resource metrics, toolbox API spans, and organization quota metrics. You write no code for any of it.

  1. Open the Daytona Dashboard and go to the OpenTelemetry section. Only organization owners see it.
  2. Set OTLP Endpoint to https://ingest.<region>.signoz.cloud/. Leave the port off, and keep the trailing slash. Daytona appends the signal path itself.
  3. Add a header with key signoz-ingestion-key and your ingestion key as the value.
  4. Save. Daytona takes up to five minutes to apply the change.
Daytona Dashboard OpenTelemetry settings with the SigNoz OTLP endpoint and ingestion key header
The OTLP endpoint and the signoz-ingestion-key header in Daytona's OpenTelemetry settings.

Sandboxes must also be able to reach *.daytona.io, as described in Step 1. Without it the per-sandbox signals below never leave the sandbox.

Per-sandbox metrics

The daemon reports every sandbox under service.name = sandbox-<sandbox-id>, with the sandbox id repeated in service.instance.id and the daemon build in service.version. Ten gauges arrive:

MetricUnitDescription
daytona.sandbox.cpu.utilizationpercentCPU used as a share of the limit
daytona.sandbox.cpu.limitcoresCPU cores the sandbox may use
daytona.sandbox.memory.utilizationpercentMemory used as a share of the limit
daytona.sandbox.memory.usagebytesMemory in use
daytona.sandbox.memory.limitbytesMemory ceiling
daytona.sandbox.memory.cachebytesPage cache
daytona.sandbox.filesystem.utilizationpercentDisk used as a share of the total
daytona.sandbox.filesystem.usagebytesDisk in use
daytona.sandbox.filesystem.availablebytesDisk free
daytona.sandbox.filesystem.totalbytesDisk size

Each carries daytona_organization_id, daytona_region_id, and daytona_snapshot as resource attributes. Note the underscores: Daytona's own telemetry uses snake_case, while attributes you set in your own code follow whatever convention you choose.

Toolbox API spans

The daemon traces the API your SDK calls into, as server spans named for the route: POST /process/execute when you run a command, POST /files/bulk-upload when you upload a file. They carry http.route, url.path, and client.address, and give you the sandbox-side duration of work your SDK spans only see from the outside.

Organization quota metrics

Pushed every 60 seconds, covering consumption against your plan limits across every sandbox. The Daytona dashboard template charts all of these already:

MetricUnitDescription
daytona.sandbox.used_cpu{cpu}CPU cores consumed by active sandboxes
daytona.sandbox.total_cpu{cpu}CPU quota for the organization
daytona.sandbox.used_ramGiByMemory consumed by active sandboxes
daytona.sandbox.total_ramGiByMemory quota for the organization
daytona.sandbox.used_storageGiByDisk consumed by sandboxes
daytona.sandbox.total_storageGiByDisk quota for the organization
daytona.sandbox.used_gpu{gpu}GPU consumed by active sandboxes
daytona.sandbox.total_gpu{gpu}GPU quota for the organization

Each carries organization.id as a resource attribute, plus region.id and sandbox.class as data point attributes. sandbox.class separates container from windows sandboxes, which Daytona meters against the same quota, so a headline usage number without it hides the split.

Validate

Wait a minute after your first run, then check each signal.

Traces: Open the Traces explorer and filter on service.name = '<your-service-name>'.

Logs: Open the Logs explorer and filter on the same service name.

Metrics: Open the Metrics explorer and search for sandbox.tasks.completed. Search daytona.sandbox for the eighteen gauges Daytona itself sends, which appear once you finish Collect Daytona's Built-in Telemetry.

Daytona's own signals: Filter either explorer on service.name starting with sandbox- to confirm the daemon is reporting. Metrics and toolbox API spans arrive within about a minute of the sandbox starting.

Daytona SDK spans in the SigNoz Traces explorer filtered by service.name daytona-python-sdk
SDK spans for one sandbox lifecycle. Daytona.list and AsyncSandbox.start appear together here, which is the sync and async naming split in practice.
A Daytona sandbox log record in SigNoz showing daytona resource attributes and a trace ID
One expanded log record from inside a sandbox, with its resource attributes and trace ID.
The sandbox.tasks.completed metric and the daytona.sandbox quota gauges in the SigNoz Metrics explorer
The metric list after both paths are set up, with sandbox.tasks.completed selected.

Attribute and Span Reference

  • SDK spans carry no daytona.* attributes. They carry no sandbox id, organization id, or region. The sandbox id appears only inside http_url, for example https://proxy.app-eu.daytona.io/toolbox/<sandbox-id>/process/code-run, so grouping SDK spans by sandbox means parsing that URL. Per-sandbox attribution comes from the telemetry you emit inside the sandbox.
  • OTEL_SERVICE_NAME does not apply to SDK tracing. The SDK sets service.name to daytona-python-sdk and service.version to the SDK version, no matter what you set. Every application using the SDK reports under that one name, so you cannot split SDK traffic per app through the service name.
  • Span names include the client class. The async client emits AsyncDaytona.create and AsyncSandbox.stop. The synchronous client drops the Async prefix from every span name. A filter written for one client returns nothing for the other.
  • daytona.snapshot is a registry digest. It resolves to a value such as cr.app.daytona.io/sbox/daytona-<sha>:daytona, not the friendly daytonaio/sandbox:0.8.0 name you passed at creation.

The async client emits internal spans for AsyncDaytona.create, get, list, list.fetch_page, start, stop, and delete; AsyncSandbox.start, stop, delete, refresh_data, wait_for_sandbox_start, and wait_for_sandbox_stop; AsyncProcess.code_run; and AsyncFileSystem.upload_file and upload_files. Outbound HTTP calls appear as client spans named for the method alone: GET, POST, DELETE.

Sandboxes expose DAYTONA_SANDBOX_ID, DAYTONA_ORGANIZATION_ID, DAYTONA_REGION_ID, and DAYTONA_SANDBOX_SNAPSHOT to every process. Use them for resource attributes, as the sandbox script above does.

Troubleshooting

Connection reset when exporting from inside a sandbox

Symptom: the exporter retries and gives up, with Connection reset by peer or OpenSSL SSL_connect: Connection reset by peer.

Likely cause: sandbox egress is deny-by-default and the SigNoz ingestion host is not allowed. Package registries work, which makes the block look selective.

Fix: create the sandbox with domain_allow_list including ingest.<region>.signoz.cloud, as in Step 1. Tier 1 and Tier 2 accounts cannot set this, so upgrade the plan or export through a host that is already reachable.

Verify: from inside the sandbox, curl -s -o /dev/null -w '%{http_code}' https://ingest.<region>.signoz.cloud returns 404. That means TLS completed and egress is open. 000 means the connection is still blocked.

No SDK spans arrive

Symptom: sandbox operations succeed, but no daytona-python-sdk service appears in SigNoz.

Likely cause: tracing is off, or the process exited before spans flushed.

Fix: confirm otel_enabled=True or DAYTONA_OTEL_ENABLED=true, and close the client. Use async with AsyncDaytona(...) or call await daytona.close().

Verify: service.name = 'daytona-python-sdk' returns spans in the Traces explorer.

Sandbox CPU, memory, and filesystem metrics never arrive

Symptom: organization gauges arrive and your own spans arrive, but nothing under service.name = 'sandbox-<sandbox-id>'. Nothing reports an error.

Likely cause: the sandbox cannot reach otel-collector.app.daytona.io. A domain_allow_list that omits *.daytona.io blocks the daemon's exporter while leaving your own exporter working, so the failure is invisible from inside your code.

Fix: add *.daytona.io to domain_allow_list and create a new sandbox. Existing sandboxes keep the rules they were created with.

Verify: from inside the sandbox, curl -s -o /dev/null -w '%{http_code}' https://otel-collector.app.daytona.io returns 404. 000 means the daemon is still firewalled.

Organization metrics are missing

Symptom: no daytona.sandbox.used_cpu after saving the configuration.

Likely cause: the push interval has not elapsed, or the header is wrong.

Fix: Daytona takes up to five minutes to apply an endpoint change, then pushes on a 60-second interval. Wait out both with at least one sandbox running, and confirm the header key is exactly signoz-ingestion-key.

Verify: search for daytona.sandbox in the Metrics explorer.

Limitations

  • Sandbox stdout and stderr are not exported. Daytona documents application logs as part of sandbox telemetry, but the only log records that arrive are the daemon's own startup diagnostics, four per sandbox. Anything your process prints stays in the sandbox, so route it through the OpenTelemetry logs SDK as Step 2 does.
  • otelEndpointOverride on sandbox creation has no effect. The API accepts the field and returns 200, then reports the sandbox with otelEndpointOverride: null.
  • The documented otel-config API call does not work with an API key. PUT /api/organizations/<org>/otel-config returns 403 Invalid authentication context for a dtn_ key regardless of its permissions. Use the Dashboard.
  • A long-lived WebSocket span distorts SDK latency. The SDK opens a connection to wss://app.daytona.io/api/socket.io/ that lives as long as the client. It appears as a root span lasting the whole session and dominates p99 for the daytona-python-sdk service. Exclude it when measuring operation latency.
  • SDK operations are separate traces. Each call roots its own trace rather than nesting under a session, so there is no single trace covering create through delete. Correlate with your own parent span if you need that view.

Next Steps

Get Help

If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack. If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.

Is this page helpful

Last updated—September 05, 2026

Edit on GitHub