Skip to content

feat: support an app-owned OpenTelemetry SDK in tracing middleware - #3250

Open
mvanhorn wants to merge 2 commits into
agentscope-ai:mainfrom
mvanhorn:fix/3229-app-owned-tracing-sdk
Open

mvanhorn wants to merge 2 commits into
agentscope-ai:mainfrom
mvanhorn:fix/3229-app-owned-tracing-sdk

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

AgentScope-Java Version

Add a public OtelTracingMiddleware(OpenTelemetry openTelemetry) constructor with a non-null contract and caller-owned lifecycle; use its io.agentscope tracer for all three existing hooks through getTracer(), without registering or replacing the global SDK. Preserve the no-argument constructor's lazy global lookup at hook execution, including middleware constructed before global registration, and preserve the existing once-per-JVM Reactor hook registration for both constructors. Keep the existing parent-context propagation, attributes, cancellation, and error behavior unchanged; no extra provider interface, builder, or test-only seam is needed.

The reporter uses StudioManager with a self-hosted agent-studio and reports missing traces when AliyunJavaAgent replaces the return value of TracerRegistry.get(). Disabling the agent's AgentScope instrumentation restores exported traces but reportedly loses the UI's expandable call tree; the UI cause has not been verified. The latest reporter comment explicitly requests a supported OpenTelemetry/Tracer injection option on OtelTracingMiddleware, in addition to describing the broader StudioManager coexistence goal. The local middleware exposes only a no-argument constructor and obtains every tracer from GlobalOpenTelemetry, preventing selection of an independent application SDK.

Fixes #3229

Description

Add a public OtelTracingMiddleware(OpenTelemetry openTelemetry) constructor with a non-null contract and caller-owned lifecycle; use its io.agentscope tracer for all three existing hooks through getTracer(), without registering or replacing the global SDK. Preserve the no-argument constructor's lazy global lookup at hook execution, including middleware constructed before global registration, and preserve the existing once-per-JVM Reactor hook registration for both constructors. Keep the existing parent-context propagation, attributes, cancellation, and error behavior unchanged; no extra provider interface, builder, or test-only seam is needed.

Checklist

Please check the following items before code is ready to be reviewed.

  • Code has been formatted with mvn spotless:apply
    mvn -pl agentscope-core spotless:check passes on c582368.
  • All tests are passing (mvn test)
    Full mvn test not run locally. OtelTracingMiddlewareTest passes 19/19 on c582368.
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.)
  • Code is ready for review

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Adds a public OtelTracingMiddleware(OpenTelemetry) constructor so spans can be recorded on an app-owned SDK instead of GlobalOpenTelemetry, keeps the no-arg constructor's lazy global lookup, and documents the path in both EN and ZH. The change is small, additive and null-checked, and the test coverage (injected SDK isolation, nesting across scheduler hops, late global registration, noop, error status, cancel) is genuinely thorough — the approach looks right and #3229's ask is addressed. Two non-blocking points below: one documentation gap around the JVM-wide Reactor hook the new constructor still registers, one tracer-caching nit, plus a CI/checklist note. Not approving yet: the checklist's format/test boxes are explicitly "Not run" and the build matrix was still pending at review time.


Automated review by github-manager-bot

public OtelTracingMiddleware(OpenTelemetry openTelemetry) {
this.openTelemetry =
Objects.requireNonNull(openTelemetry, "openTelemetry must not be null");
registerReactorHook();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Warning] This new constructor also runs registerReactorHook(), i.e. ContextPropagationOperator...registerOnEachOperator() — a JVM-wide Reactor hook that wraps every operator of every Flux/Mono in the process, independent of which SDK is used for tracer lookup. So "app-owned SDK" only isolates tracer lookup, not the global instrumentation side effect; an app that adopts this constructor precisely to avoid touching global state still gets the hook. Suggest either documenting the side effect in this Javadoc (plus the middleware docs), or adding an overload/flag that lets the caller decide whether the Reactor hook should be registered.


private Tracer getTracer() {
if (openTelemetry != null) {
return openTelemetry.getTracer(INSTRUMENTATION_NAME);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Info] getTracer() is called once per span creation. For the app-owned instance this hits the provider every time: SdkTracerProvider.get() is a concurrent-map lookup, but the SDK still allocates an obfuscated tracer wrapper on each call. Caching the Tracer per OpenTelemetry instance would remove that repeated work on the onModelCall / onActing hot paths, while keeping the deliberate lazy lookup for the global (no-arg) path.

* runs. When only the default no-op provider is active, the hooks still run the downstream
* chain.
*/
public OtelTracingMiddleware() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Info] The PR checklist has mvn spotless:apply and mvn test marked "Not run". validate is green and the ubuntu/windows build matrix jobs were still queued when this review was posted, so formatting/tests will be settled by CI — but please tick those boxes (or push a reformat commit) before merge so the record matches.

@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mvanhorn

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Both notes are addressed in c582368.

  • Reactor hook: the class and constructor Javadoc, plus the EN/ZH middleware docs, now say that the app-owned constructor still installs ContextPropagationOperator.registerOnEachOperator() once per JVM, and that it wraps every Flux/Mono whichever SDK records spans.
  • Tracer: the app-owned path now resolves the io.agentscope tracer once in the constructor and reuses it. The no-arg path still looks up GlobalOpenTelemetry lazily. A test pins the single lookup.

mvn spotless:check is clean, and OtelTracingMiddlewareTest passes 19/19 locally. I've updated the checklist to match.

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

The new commit documents the JVM-wide Reactor hook and caches the app-owned tracer. The cache is safely published, the no-argument path keeps lazy global lookup, and the hook side effect is now disclosed in both language docs — previous-round findings on hook disclosure and tracer lookup are resolved. One wording nit below; nothing blocking.

Previous-round follow-up

Earlier inline findings are addressed; the remaining CI-status item is a PR-process matter, not a source change.


Automated review by github-manager-bot


Passing `appSdk` does not replace `GlobalOpenTelemetry` and does not change `StudioManager`. `StudioManager` still installs deprecated `TracerRegistry` tracing on its own during `initialize()`. Vendor instrumentation that rewrites tracer lookup, and a missing Studio call-tree expansion control, need separate verification against the deployment that produces them. This middleware selects which OpenTelemetry SDK records the `onAgent`, `onModelCall`, and `onActing` spans. `OpenTelemetry.noop()` is also a valid argument: the downstream chain still runs, and no spans are recorded.

Constructing `OtelTracingMiddleware` — either `new OtelTracingMiddleware()` or `new OtelTracingMiddleware(appSdk)` — also registers a JVM-wide Reactor hook, `ContextPropagationOperator.registerOnEachOperator()`, the first time any instance is created. The hook wraps every operator of every `Flux` and `Mono` in the process so parent spans survive `publishOn` / `subscribeOn` hops. It is independent of which SDK records spans: an application-owned SDK isolates tracer lookup from `GlobalOpenTelemetry`, not this instrumentation side effect. The hook is installed at most once per JVM and is not removed when the middleware or the SDK is closed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reactor's onEachOperator hook decorates operators assembled after it is registered; it cannot retroactively wrap Flux/Mono chains that were already assembled. Qualify the claim that it wraps every operator in the process (and make the same correction in the Chinese documentation) so applications do not assume pre-assembled publishers gain propagation.

This branch has not been deployed

No deployments
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.

[Bug]:How to solve the OpenTelemetry conflict between Agentscope and AliyunJavaAgent

2 participants