feat(genai): add pre-authorized OSS uploader for multimodal content - #264
Conversation
Add the `presign` multimodal uploader: instead of holding OSS credentials,
the SDK asks the server for a short-lived presigned URL (authenticated with
a license key) and then writes the object straight to the server-owned
bucket. Objects are addressed as
`sls://{project}/{logstore}[/{prefix}]/{objectName}`, which is fully
resolvable locally, so span attributes can be rewritten before the upload
is even issued.
The identity used for the presign request can now come from dedicated
environment variables, so an application using the SDK standalone no
longer needs to run under an agent:
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_LICENSE_KEY
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_WORKSPACE
Both fall back to ARMS_LICENSE_KEY / ARMS_WORKSPACE so a co-located agent
keeps working without extra configuration.
Also add `OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_OSS_PATH_PREFIX` to give an
application a shared object path prefix. `..._OSS_BUCKET` is ignored in
this mode because the server decides which bucket backs the objects.
When the project, endpoint or license key cannot be resolved, the uploader
and pre-uploader both return None and the whole multimodal path degrades to
disabled, leaving the original content in place rather than pointing at an
object that was never written.
Ship a manual-instrumentation example and document the SDK-only flow. Two
configuration requirements are easy to miss there, since neither fails
loudly: content capturing needs both OTEL_SEMCONV_STABILITY_OPT_IN and
CAPTURE_MESSAGE_CONTENT (the latter takes a ContentCapturingMode enum name,
not a boolean), and the logstore must already exist. The example validates
its configuration up front so these surface as errors instead of an empty
span.
Verified end to end against a real presign endpoint: presign request and
OSS PUT both returned 200, and the URI written to the span matched the
uploaded object key segment for segment, using both the context-manager
and the explicit start/stop APIs.
Change-Id: I0308e9e0ea5887c9d73fbd3c7b49c0d3c1f49429
Co-developed-by: Qoder <noreply@qoder.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Adds a new “presign” multimodal uploader that requests short-lived presigned URLs from an ARMS endpoint (authenticated via license key) and uploads directly to a server-owned OSS bucket, avoiding local OSS credentials.
Changes:
- Introduces
MultimodalPresignClient+PresignUploaderand wires them as entry points (presign) for uploader and pre-uploader. - Extends runtime/env configuration to support presign endpoint, license/workspace fallback behavior, and a shared OSS path prefix.
- Adds comprehensive tests plus SDK-only manual instrumentation docs and an executable example.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_client.py | New presign API client: endpoint resolution, payload construction, response parsing, error classification |
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_uploader.py | New uploader: bounded async queue, presign+PUT retry loop, optional download of source URIs |
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/config.py | Adds presign hook support, env snapshot fields, SLS base-path formatting helper, prefix/bucket normalization |
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_environment_variables.py | Documents new env vars for presign endpoint/auth/timeout and shared object path prefix |
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/init.py | Exposes presign hook constants/types in package exports |
| util/opentelemetry-util-genai/pyproject.toml | Registers presign as entry points for uploader + pre-uploader |
| util/opentelemetry-util-genai/tests/_multimodal_upload/test_presign_uploader.py | Adds presign-specific unit tests across client, uploader, hooks, and config normalization |
| util/opentelemetry-util-genai/examples/multimodal_presign_manual.py | Adds SDK-only runnable example that validates env/config before running |
| util/opentelemetry-util-genai/docs/manual-instrumentation.md | Adds manual instrumentation guide including presign mode wiring and pitfalls |
| util/opentelemetry-util-genai/README-loongsuite.rst | Documents presign mode behavior, env vars, and operational expectations |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The example only put an image on the output side, which left the impression that uploads apply to model output alone. Both sides are uploaded, so give the input message an image part too — now the request carries text plus an image (a visual question) and the two sides use different images, making it visible that these are two independent uploads. Fix a wrong hint while here: it described `span_only` as a valid upload_mode. The valid values are none / input / output / both, and anything else is silently normalized to none, so following that hint would have disabled uploading altogether. `span_only` belongs to CAPTURE_MESSAGE_CONTENT, not here. Since upload_mode also decides *which* side is processed, the example now reads the library's own config snapshot and rejects a mode that does not cover both sides, rather than letting one side quietly stay as base64. Verified against a real presign endpoint: two presign requests and two OSS PUTs all returned 200, `gen_ai.input.messages` and `gen_ai.output.messages` each carried a different `sls://` URI matching its uploaded object key, and the text part was left untouched. Modes `input`, `output` and `span_only` are each rejected up front. Change-Id: I0bad5743eb5294cf9c63171cd1df19dbf63f5774 Co-developed-by: Qoder <noreply@qoder.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Change-Id: If110a24f6f5433db5d8e249c4774c23d4279cba3
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Change-Id: I8303f0abe27d7495d46919d5e8aec70ce29cdf55
There was a problem hiding this comment.
🟡 Changes recommended
The new presign uploader path parsing/validation has a few concrete edge cases (invalid project and URI variants) that should be tightened to avoid accepting malformed sls:// addresses and endpoints.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/config.py:373
presign_endpointis read from the environment without trimming whitespace. This is inconsistent with_first_env(used for license key/workspace) and can make a seemingly-valid endpoint fail readiness checks or generate invalid request URLs.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_uploader.py:245
- When
UploadItem.urluses ansls://...URI,_object_key_from_urlcurrently ignores URL params/query/fragment and doesn't verify the parsed scheme; this can allow multiple textual variants of the same object key (or malformed URIs) to be treated as valid.
if target.startswith("sls://"):
parsed = urlparse(target)
logstore, _, remainder = parsed.path.lstrip("/").partition("/")
if parsed.netloc != self._project or logstore != self._logstore:
raise PresignUploadConfigError(
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Lite
Change-Id: I9b0faf8110473d91e78962b1d8ed7ee8bcb9010e
There was a problem hiding this comment.
🟡 Changes recommended
The new presign client/uploader have confirmed httpx response lifecycle issues (responses not closed on error/success paths) that can exhaust connection pools in production.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/config.py:364
presign_endpointis read from the environment without trimming whitespace. A value like' https://example.com 'will be treated as non-empty but later produce invalid URLs; it should be normalized (at least.strip()) at snapshot creation time to match how other string fields are handled.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_client.py:352- HTTPX responses should be closed to return the connection to the pool. In the current implementation, non-success presign responses raise without reading/closing the body, which can leak connections under load.
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_multimodal_upload/presign_uploader.py:522 - HTTPX responses should be closed to release the connection back to the pool.
_put_presignedcurrently returns/raises without closing the response on both success and error paths, which can exhaust the connection pool under retries or high throughput.
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Lite
| ssl_verify = os.environ.get( | ||
| OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_DOWNLOAD_SSL_VERIFY, | ||
| "true", | ||
| ).lower() not in ("false", "0", "no") | ||
| # Require readiness before enabling URI rewriting. Keep resolving on each |
What
Adds a
presignmultimodal uploader toopentelemetry-util-genai. Instead of holding OSS credentials, the SDK asks the server for a short-lived presigned URL (authenticated with a license key) and then writes the object straight to the server-owned bucket.Objects are addressed as
sls://{project}/{logstore}[/{prefix}]/{objectName}. That address is fully resolvable locally, so span attributes are rewritten before the upload is even issued — the span never has to wait on the network.Why
The existing uploader requires the application to hold OSS credentials. This mode removes that requirement, which matters for the SDK-only (manual instrumentation) case where there is no agent in the process to supply anything.
Configuration
OTEL_INSTRUMENTATION_GENAI_MULTIMODAL_UPLOADER=presignOTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_ENDPOINTOTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_LICENSE_KEYARMS_LICENSE_KEYOTEL_INSTRUMENTATION_GENAI_MULTIMODAL_PRESIGN_WORKSPACEARMS_WORKSPACEAPSARA_APM_COLLECTOR_MULTIMODAL_SLS_PROJECT/..._SLS_LOGSTOREOTEL_INSTRUMENTATION_GENAI_MULTIMODAL_OSS_PATH_PREFIXThe dedicated
PRESIGN_*variables take precedence, with a fallback to theARMS_*ones so a co-located agent keeps working with no extra configuration...._OSS_BUCKETis ignored here, since the server decides which bucket backs the objects.Failure behavior
If project, endpoint or license key cannot be resolved, both the uploader and the pre-uploader return
Noneand the whole multimodal path degrades to disabled. The original content stays in the span rather than being replaced by a URI pointing at an object that was never written.Manual instrumentation
Also included:
examples/multimodal_presign_manual.pyplusdocs/manual-instrumentation.mdcovering the SDK-only flow.Two configuration requirements there are easy to miss because neither fails loudly:
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimentalandOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. The latter takes aContentCapturingModeenum name (SPAN_ONLY/SPAN_AND_EVENT) — passingtrueis an invalid value and silently falls back toNO_CONTENT, producing a span with no message content and no upload at all.The example validates its configuration up front so both surface as explicit errors instead of an empty span.
Testing
tests/_multimodal_upload/PUTboth returned 200, and the URI written to the span matched the uploaded object key segment for segment. Exercised through bothwith handler.llm(...)and the explicitstart_llm/stop_llmAPIs.CAPTURE_MESSAGE_CONTENTvalue are each rejected by the example's config check rather than degrading silently.