Skip to content

Java: POTel Research #3232

@smeubank

Description

@smeubank
### Statement on OTel support - tech feasibility and quality
- [ ] Java (and frameworks)
- [ ] Kotlin
- [ ] Android
  • Currently working on a backend POC
    • trying to translate JS to Java then go from there and check for blockers, required API changes etc.
    • increasing internal list of spans
      • requests to sentry are not filtered properly
    • spans are reported mulitple times with increasing duration
    • Check differences in context handling
  • Also need to do a POC for Android + OTEL to check for blockers
  • Check how good auto instrumentation for Spring (Boot) is without the Agent

Context Handling

Attributes vs Context

  • Attributes can only hold few types (String, Boolean, Long, Double) and arrays thereof (see AttributeType)
  • Context can also hold complex types
  • Need to test whether Context.current() works in SpanProcessor
    • Initial test looks promising
    • but there might be more complex scenarios where this does not work
  • Attributes can be retrieved from ReadableSpan in SpanProcessor.onEnd()
  • Context can not be retrieved from ReadableSpan
  • Context has an internal split, when AgentContextWrapper is used (presumably this is always the case when using the OTEL Java Agent)
    • agentContext vs applicationContext

ContextStorageProvider

  • We could implement a custom ContextStorageProvider to intercept whenever context is replaced and fork scope
  • We'll also have to figure out when to fork isolation scope

Hubs and Scopes merge

Execution context

  • Should we explicitly have ExecutionContext in code to wrap isolation scope, current scope and maybe also current span?
  • Would be the replacement thread local variable for currentHub

Scopes

  • There will be multiple scopes
    • Global scope
      • Truely global scope (process wide), not per client
      • Can be written to even before Sentry.init
      • Can be used e.g. to add tags that will be added to all events etc.
        • before this basically required users to add tags immediately after Sentry.init was called
        • this can now be customized even from asynchronously running code, e.g. after some lib initializes or after some API call was made
      • Holds a reference to the default global client
      • to add tags, set user etc. users need to explicitly perform Sentry.getGlobalScope().setTag() etc.
    • Isolation scope
      • similar to a global scope, but tied to a specific section of your app/code that should be isolated
        • e.g. for the duration of handling a request server side
      • Users don't have to worry about writing to a scope that doesn't apply to the whole request by accident, they can now use isolation scope e.g. to add tags or set a user that is applied to the whole request
      • Global methods to set data, like Sentry.setTag() or Sentry.setUser(), will set on the isolation scope.
    • Current scope
      • Global methods that capture like Sentry.captureException(e) will capture on the current scope
      • While capturing global scope, isolation scope and current scope are merged and applied to events

Scope forking

See https://miro.com/app/board/uXjVNtPiOfI=/

  • When forking current scope (with new_scope)
    • forks current scope and sets the fork as active current scope
    • does not fork isolation scope (isolation scope remains untouched)
    • this is the only of these methods that's part of public API, others can be usable by users but not part of public API directly
  • When setting current scope (with use_scope(scope))
    • uses passed in scope as active current scope
    • does not fork current scope
    • isolation scope remains untouched
  • When forking isolation scope (with isolation_scope)
    • forks isolation scope and sets the fork as active isolation scope
    • forks current scope and sets the fork as active current scope
  • When setting isolation scope (with use_isolation_scope(isolation_scope))
    • uses passed in isolation scope as active isolation scope
    • do we even need this?
  • When setting isolation scope and current scope
    • uses passed in isolation scope as active isolation scope
    • uses passed in current scope as active current scope
    • does not fork either scope
    • maybe this should be called with execution_context(execution_context)

Applying scopes to events

  public captureEvent(event: Event, additionalScope?: Scope) {
    // Global scope is always applied first
    const scopeData = getGlobalScope().getScopeData();

    // Apply isolations cope next
    const isolationScope = getIsolationScope();
    merge(scopeData, isolationScope.getScopeData());

    // Now the scope data itself is added
    merge(scopeData, this.getScopeData());

    // If defined, add the captureContext/scope
    // This is e.g. what you pass to Sentry.captureException(error, { tags: [] })
    if (additionalScope) {
      merge(scopeData, additionalScope.getScopeData());
    }

    // Finally, this is merged with event data, where event data takes precedence!
    mergeIntoEvent(event, scopeData);
  }
}

Transferring execution context

  • When moving execution from one thread to another, we should set isolation scope and current scope to the same ones on the new thread
  • For reactive programming libraries we'll have to backup and restore isolation scope, current scope and maybe also the current span (if it's not set on the scope but directly on execution context)
  • We should probably not fork isolation scope likely to not diminish its usefulness
  • see https://miro.com/app/board/uXjVNtPiOfI=/

Tracing without Performance (TwP) / PropagationContext

  • Should this be put on isolation scope?
  • Should we have a more abstract API for isolation that
    • forks isolation scope
    • forks current scope
    • creates a new PropagationContext

Copy on write

  • Scopes should only be actually copied when there's a change.
    • For current scope there should be a very limited number of actual changes but lots of forks
    • Is the most likely change happening setting a new active span?

Locking

  • we usually lock write access to scope via withPropagationContext, withSession, setTransaction/clearTransaction, startSession/endSession, how will this behave in the future?

Lifecycle management

  • Things like pushScope and pushIsolationScope could return an AutoClosable where on close we restore the previous scope. This would allow users to simply write try(x = pushScope()) { ... } instead of manually having to popScope. It'd also allow for restoring previous state without risking an imbalance between pushScope and popScope calls. OTEL does this, e.g. in Context.makeCurrent().

Migration docs

  • May need to make it extra clear in changelog, migration guide etc. that top level API (e.g. Sentry.setTag()) now goes to isolation scope whereas e.g. Sentry.configureScope(s -> s.setTag()) goes to the current scope. This may lead to unexpected scope leaks for our users.
  • scope
    • pushScope+popScope -> ?
    • withScope -> ?
    • configureScope -> ?
  • direct hub usage
    • hub.captureX -> scope.captureX ?

Document common use cases

  • setting tags, breadcrumbs, user, ... for the current request -> Sentry.addTag()
  • setting tags, breadcrumbs, user, ... globally -> Sentry.getGlobalScope().setX etc.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions