Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delegate first class spans to native SDK when attached #556

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/lib/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ export class SpanAttributes {
Array.from(this.attributes).forEach(([key, value]) => { this.validateAttribute(key, value) })
return Array.from(this.attributes).map(([key, value]) => attributeToJson(key, value))
}

toObject () {
return Object.fromEntries(this.attributes)
}
}

export class ResourceAttributes extends SpanAttributes {
Expand Down
41 changes: 3 additions & 38 deletions packages/core/lib/batch-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import type ProbabilityManager from './probability-manager'
import type { Processor } from './processor'
import type { RetryQueue } from './retry-queue'
import type { ReadonlySampler } from './sampler'
import type { Span, SpanEnded } from './span'
import type { SpanEnded } from './span'

import { millisecondsToNanoseconds } from './clock'
import { spanEndedToSpan } from './span'

export type OnSpanEndCallback = (span: Span) => boolean | Promise<boolean>
export type OnSpanEndCallbacks = OnSpanEndCallback[]
import { runSpanEndCallbacks } from './span'

type MinimalProbabilityManager = Pick<ProbabilityManager, 'setProbability' | 'ensureFreshProbability'>

Expand Down Expand Up @@ -114,37 +110,6 @@ export class BatchProcessor<C extends Configuration> implements Processor {
await this.flushQueue
}

private async runCallbacks (span: Span): Promise<boolean> {
if (this.configuration.onSpanEnd) {
const callbackStartTime = performance.now()
let continueToBatch = true
for (const callback of this.configuration.onSpanEnd) {
try {
let result = callback(span)

// @ts-expect-error result may or may not be a promise
if (typeof result.then === 'function') {
result = await result
}

if (result === false) {
continueToBatch = false
break
}
} catch (err) {
this.configuration.logger.error('Error in onSpanEnd callback: ' + err)
}
}
if (continueToBatch) {
const duration = millisecondsToNanoseconds(performance.now() - callbackStartTime)
span.setAttribute('bugsnag.span.callbacks_duration', duration)
}
return continueToBatch
} else {
return true
}
}

private async prepareBatch (): Promise<SpanEnded[] | undefined> {
if (this.spans.length === 0) {
return
Expand All @@ -165,7 +130,7 @@ export class BatchProcessor<C extends Configuration> implements Processor {
if (this.sampler.sample(span)) {
// Run any callbacks that have been registered before batching
// as callbacks could cause the span to be discarded
const shouldAddToBatch = await this.runCallbacks(spanEndedToSpan(span))
const shouldAddToBatch = await runSpanEndCallbacks(span, this.configuration.logger, this.configuration.onSpanEnd)
if (shouldAddToBatch) batch.push(span)
}
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { OnSpanEndCallbacks } from './batch-processor'
import {
ATTRIBUTE_ARRAY_LENGTH_LIMIT_DEFAULT,
ATTRIBUTE_COUNT_LIMIT_DEFAULT,
Expand All @@ -8,6 +7,7 @@ import {
ATTRIBUTE_STRING_VALUE_LIMIT_MAX
} from './custom-attribute-limits'
import type { Plugin } from './plugin'
import type { Span } from './span'
import { isLogger, isNumber, isObject, isOnSpanEndCallbacks, isPluginArray, isString, isStringArray, isStringWithLength } from './validation'

type SetTraceCorrelation = (traceId: string, spanId: string) => void
Expand Down Expand Up @@ -39,6 +39,9 @@ export interface Logger {
error: (message: string) => void
}

export type OnSpanEndCallback = (span: Span) => boolean | Promise<boolean>
export type OnSpanEndCallbacks = OnSpanEndCallback[]

export interface Configuration {
apiKey: string
endpoint?: string
Expand Down
16 changes: 6 additions & 10 deletions packages/core/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type { Persistence } from './persistence'
import type { Plugin } from './plugin'
import ProbabilityFetcher from './probability-fetcher'
import ProbabilityManager from './probability-manager'
import type { Processor } from './processor'
import { BufferingProcessor } from './processor'
import type { RetryQueueFactory } from './retry-queue'
import Sampler from './sampler'
Expand Down Expand Up @@ -55,15 +54,14 @@ export type BugsnagPerformance <C extends Configuration, T> = Client<C> & T

export function createClient<S extends CoreSchema, C extends Configuration, T> (options: ClientOptions<S, C, T>): BugsnagPerformance<C, T> {
const bufferingProcessor = new BufferingProcessor()
let processor: Processor = bufferingProcessor
const spanContextStorage = options.spanContextStorage || new DefaultSpanContextStorage(options.backgroundingListener)
let logger = options.schema.logger.defaultValue
const sampler = new Sampler(1.0)

const SpanFactoryClass = options.spanFactory || SpanFactory

const spanFactory = new SpanFactoryClass(
processor,
bufferingProcessor,
sampler,
options.idGenerator,
options.spanAttributesSource,
Expand Down Expand Up @@ -101,6 +99,8 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (

options.spanAttributesSource.configure(configuration)

spanFactory.configure(configuration)

const probabilityManagerPromise = configuration.samplingProbability === undefined
? ProbabilityManager.create(
options.persistence,
Expand All @@ -110,7 +110,7 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (
: FixedProbabilityManager.create(sampler, configuration.samplingProbability)

probabilityManagerPromise.then((manager: ProbabilityManager | FixedProbabilityManager) => {
processor = new BatchProcessor(
const batchProcessor = new BatchProcessor(
delivery,
configuration,
options.retryQueueFactory(delivery, configuration.retryQueueMaxSize),
Expand All @@ -119,17 +119,14 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (
new TracePayloadEncoder(options.clock, configuration, options.resourceAttributesSource)
)

// ensure all spans started before .start() are added to the batch
for (const span of bufferingProcessor.spans) {
processor.add(span)
}
spanFactory.reprocessEarlySpans(batchProcessor)

// register with the backgrounding listener - we do this in 'start' as
// there's nothing to do if we're backgrounded before start is called
// e.g. we can't trigger delivery until we have the apiKey and endpoint
// from configuration
options.backgroundingListener.onStateChange(state => {
(processor as BatchProcessor<C>).flush()
batchProcessor.flush()

// ensure we have a fresh probability value when returning to the
// foreground
Expand All @@ -139,7 +136,6 @@ export function createClient<S extends CoreSchema, C extends Configuration, T> (
})

logger = configuration.logger
spanFactory.configure(processor, configuration)
})

for (const plugin of configuration.plugins) {
Expand Down
39 changes: 27 additions & 12 deletions packages/core/lib/span-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type { SpanAttribute, SpanAttributesLimits, SpanAttributesSource } from '
import { SpanAttributes } from './attributes'
import type { BackgroundingListener, BackgroundingListenerState } from './backgrounding-listener'
import type { Clock } from './clock'
import type { Configuration, InternalConfiguration, Logger } from './config'
import type { Configuration, InternalConfiguration, Logger, OnSpanEndCallbacks } from './config'
import { defaultSpanAttributeLimits } from './custom-attribute-limits'
import type { IdGenerator } from './id-generator'
import type { NetworkSpanOptions } from './network-span'
import type { Processor } from './processor'
import type { BufferingProcessor, Processor } from './processor'
import type { ReadonlySampler } from './sampler'
import type { InternalSpanOptions, ParentContext, Span, SpanOptionSchema, SpanOptions } from './span'
import { SpanInternal, coreSpanOptionSchema } from './span'
Expand All @@ -27,8 +27,9 @@ export class SpanFactory<C extends Configuration> {
private readonly spanAttributesSource: SpanAttributesSource<C>
protected readonly clock: Clock
private readonly spanContextStorage: SpanContextStorage
private logger: Logger
protected logger: Logger
private spanAttributeLimits: SpanAttributesLimits = defaultSpanAttributeLimits
protected onSpanEndCallbacks?: OnSpanEndCallbacks

private openSpans: WeakSet<SpanInternal> = new WeakSet<SpanInternal>()
private isInForeground: boolean = true
Expand Down Expand Up @@ -73,6 +74,9 @@ export class SpanFactory<C extends Configuration> {
: this.spanContextStorage.current

const attributes = new SpanAttributes(new Map(), this.spanAttributeLimits, name, this.logger)
if (typeof options.isFirstClass === 'boolean') {
attributes.set('bugsnag.span.first_class', options.isFirstClass)
}

const span = this.createSpanInternal(name, safeStartTime, parentContext, options.isFirstClass, attributes)

Expand All @@ -98,10 +102,6 @@ export class SpanFactory<C extends Configuration> {
const parentSpanId = parentContext ? parentContext.id : undefined
const traceId = parentContext ? parentContext.traceId : this.idGenerator.generate(128)

if (typeof isFirstClass === 'boolean') {
attributes.set('bugsnag.span.first_class', isFirstClass)
}

return new SpanInternal(spanId, traceId, name, startTime, attributes, this.clock, parentSpanId)
}

Expand All @@ -117,14 +117,23 @@ export class SpanFactory<C extends Configuration> {
return spanInternal
}

configure (processor: Processor, configuration: InternalConfiguration<C>) {
this.processor = processor
configure (configuration: InternalConfiguration<C>) {
this.logger = configuration.logger
this.spanAttributeLimits = {
attributeArrayLengthLimit: configuration.attributeArrayLengthLimit,
attributeCountLimit: configuration.attributeCountLimit,
attributeStringValueLimit: configuration.attributeStringValueLimit
}
this.onSpanEndCallbacks = configuration.onSpanEnd
}

reprocessEarlySpans (batchProcessor: Processor) {
// ensure all spans in the buffering processor are added to the batch
for (const span of (this.processor as BufferingProcessor).spans) {
batchProcessor.add(span)
}

this.processor = batchProcessor
}

endSpan (
Expand All @@ -148,8 +157,7 @@ export class SpanFactory<C extends Configuration> {
// - they are already invalid
// - they have an explicit discard end time
if (untracked || !isValidSpan || endTime === DISCARD_END_TIME) {
// we still call end on the span so that it is no longer considered valid
span.end(endTime, this.sampler.spanProbability)
this.discardSpan(span)
return
}

Expand All @@ -159,9 +167,16 @@ export class SpanFactory<C extends Configuration> {
}

this.spanAttributesSource.requestAttributes(span)
this.sendForProcessing(span, endTime)
}

const spanEnded = span.end(endTime, this.sampler.spanProbability)
protected discardSpan (span: SpanInternal) {
// we still call end on the span so that it is no longer considered valid
span.end(DISCARD_END_TIME, this.sampler.spanProbability)
}

protected sendForProcessing (span: SpanInternal, endTime: number) {
const spanEnded = span.end(endTime, this.sampler.spanProbability)
if (this.sampler.sample(spanEnded)) {
this.processor.add(spanEnded)
}
Expand Down
32 changes: 32 additions & 0 deletions packages/core/lib/span.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { SpanAttribute, SpanAttributes } from './attributes'
import { millisecondsToNanoseconds } from './clock'
import type { Clock } from './clock'
import type { Logger, OnSpanEndCallbacks } from './config'
import type { DeliverySpan } from './delivery'
import { SpanEvents } from './events'
import type { SpanContext } from './span-context'
Expand Down Expand Up @@ -86,6 +88,36 @@ export function spanEndedToSpan (span: SpanEnded): Span {
}
}

export async function runSpanEndCallbacks (spanEnded: SpanEnded, logger: Logger, callbacks?: OnSpanEndCallbacks) {
if (!callbacks) return true

const span = spanEndedToSpan(spanEnded)
const callbackStartTime = performance.now()
let shouldSample = true
for (const callback of callbacks) {
try {
let result = callback(span)

// @ts-expect-error result may or may not be a promise
if (typeof result.then === 'function') {
result = await result
}

if (result === false) {
shouldSample = false
break
}
} catch (err) {
logger.error('Error in onSpanEnd callback: ' + err)
}
}
if (shouldSample) {
const duration = millisecondsToNanoseconds(performance.now() - callbackStartTime)
span.setAttribute('bugsnag.span.callbacks_duration', duration)
}
return shouldSample
}

export class SpanInternal implements SpanContext {
readonly id: string
readonly traceId: string
Expand Down
3 changes: 1 addition & 2 deletions packages/core/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { OnSpanEndCallback } from './batch-processor'
import type { Configuration, Logger } from './config'
import type { Configuration, Logger, OnSpanEndCallback } from './config'
import type { PersistedProbability } from './persistence'
import type { Plugin } from './plugin'
import type { ParentContext } from './span'
Expand Down
Loading
Loading