Add Modal Sandbox instrumentation#281
Conversation
Instrument Modal Sandbox.create and Sandbox.exec methods to capture traces and logs. The instrumentation handles Modal's synchronicity pattern by wrapping both the public Sandbox class and internal _Sandbox class separately. Key features: - Sandbox.create spans with sandbox ID, timeout, and config attributes - Sandbox.exec spans with command and workdir attributes - stdout/stderr log emission via tee-wrapped iterators that emit OTel logs as the user reads output, without consuming the data - Both sync and async code paths instrumented Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move MODAL after MISTRAL in both the Instruments enum and INSTRUMENTATION_INITIALIZERS dict to maintain alphabetical order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The async iterator protocol requires __aiter__ to be a regular (synchronous) method that returns self. Using async def causes it to return a coroutine object, breaking async for iteration in Python 3.10+. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ility _TeeStreamIterator only implemented iterator protocols but not other methods like read() that Modal's StreamReader exposes. This caused AttributeError when users called process.stdout.read(). Adding __getattr__ transparently proxies all non-iterator attribute access to the original stream. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compute the command string once in _wrap_exec/_awrap_exec and pass it to _set_exec_request_attributes instead of computing it independently in both places. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@laminar-coding-agent explain what is internal _Sandbox class and why instrumenting it brings any value |
…ented Modal's `synchronicity` library generates separate sync/async class pairs where `_Sandbox` (internal async) and `Sandbox` (public sync) have entirely distinct method objects. Patching one does not affect the other, so both must be instrumented to capture all traces. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
What is Modal uses the Why instrument both?
This means patching We must instrument both classes to capture traces regardless of whether user code calls the sync or async interface. I've updated the module docstring and inline comments in |
|
The CLAUDE.md already contains a well-written section on "Synchronicity-based SDKs (e.g. Modal)" that covers the key insight about |
|
@laminar-coding-agent
It looks like Sandbox is a wrapper around Sandbox to handle both sync and async contexts. Users are using only public Sandbox class. Let's drop the _Sandbox instrumentation. |
Sandbox is a wrapper around _Sandbox that users interact with directly. Instrumenting _Sandbox separately is unnecessary since users only use the public Sandbox API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Changes pushed:
Only the public |
Modal's StreamReader.read() is a primary usage pattern but was delegated via __getattr__ without emitting OTel logs. This adds an explicit read() override that emits log records, matching the behavior of iteration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without @dont_throw, an exception raised inside _record_exception (e.g., from a broken __str__ on the user exception) would bubble up past the call site's span.end(), leaking the active span context and masking the original user-facing exception with an instrumentation error. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f02d1eb. Configure here.
Modal and Daytona wrappers each carried nearly identical copies of the LogStream enum and a provider-scoped _emit_log function that only differed by the attribute/event prefix. Both now share shared/log_emission.py (emit_log parameterized by provider name), and the two wrapper modules bind their provider with functools.partial so call-sites are unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_TeeStreamIterator.__next__ previously called next(self._original), which only works when the original stream is itself an iterator. If Modal's StreamReader.__iter__ ever returns a fresh iterator (the general iterable protocol), next() would raise TypeError. Now we cache the iterator produced by iter()/__aiter__() and advance that, which works for both self-iterator and fresh-iterator conventions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Summary
createandexecmethodsSandboxand internal_SandboxclassesImplementation
opentelemetry/instrumentation/modal/following the Daytona instrumentation patternModalSandboxInstrumentorregistered inInstrumentsenum andINSTRUMENTATION_INITIALIZERS_TeeStreamIteratorwrapsContainerProcess.stdout/stderrto emit logs transparentlyTest plan
ModalSandboxInstrumentorclass instantiates and reports correct scope/dependenciesSandbox.create,Sandbox.exec,_Sandbox.create,_Sandbox.exec) instrumented successfully via wraptmodal.sandbox.createandmodal.sandbox.execspans ingested into ClickHouse with correct input/output attributesmodal.log.stdoutandmodal.log.stderrlogs ingested into ClickHouse with correct body and attributestest_initialize.py) pass without regression🤖 Generated with Claude Code
Note
Medium Risk
Adds a new auto-instrumentor and modifies shared tracing/log-emission utilities, including monkeypatching Modal
ContainerProcessstream access, which could affect runtime behavior across different Modal versions. Changes also refactor Daytona log emission to a shared helper, so regressions could impact existing sandbox log capture.Overview
Adds Modal Sandbox auto-instrumentation: new
ModalSandboxInstrumentorwrapsSandbox.create/exec(sync + async) to emit spans with input/output attributes, and teesContainerProcess.stdout/stderrso reading output producesmodal.log.stdout/modal.log.stderrOTel log records.Refactors sandbox log emission by introducing shared
shared/log_emission.py(commonLogStream+emit_log) and updating Daytona to use it. Extendsshared/utils.pywithsafe_*helpers (no-op spans, safe end/is_recording/context) to make wrappers resilient to Laminar/OTel failures, and registers the new instrumentor viaInstruments.MODALand a corresponding initializer.Reviewed by Cursor Bugbot for commit 7b3ead5. Bugbot is set up for automated code reviews on this repo. Configure here.