Skip to content

Commit

Permalink
Pull elements of SpansService apart for better organization and depen…
Browse files Browse the repository at this point in the history
…dency management (#313)

## Goal

Pull apart the `SpansService` interface  and implementations into constituent components. This is a bit messy, but there's no easy step by step way of breaking them down, so the idea is to do it roughly first then clean up the concerns in subsequent PRs.

There's no new logic, just stuff being moved around. Tests all pass after modifications
  • Loading branch information
bidetofevil committed Jan 31, 2024
1 parent c2f7901 commit a6cb018
Show file tree
Hide file tree
Showing 28 changed files with 549 additions and 364 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ final class EmbraceImpl {
this.essentialServiceModuleSupplier = essentialServiceModuleSupplier;
this.dataCaptureServiceModuleSupplier = dataCaptureServiceModuleSupplier;
this.deliveryModuleSupplier = deliveryModuleSupplier;
this.tracer = initModule.getTracer();
this.tracer = initModule.getEmbraceTracer();
uninitializedSdkInternalInterface =
LazyKt.lazy(() -> new UninitializedSdkInternalInterfaceImpl(new InternalTracer(tracer, sdkClock)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal class EmbraceInternalInterfaceImpl(
private val embraceImpl: EmbraceImpl,
private val initModule: InitModule,
private val configService: ConfigService,
internalTracer: InternalTracer = InternalTracer(initModule.tracer, initModule.clock)
internalTracer: InternalTracer = InternalTracer(initModule.embraceTracer, initModule.clock)
) : EmbraceInternalInterface, InternalTracingApi by internalTracer {

override fun logInfo(message: String, properties: Map<String, Any>?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
package io.embrace.android.embracesdk.injection

import io.embrace.android.embracesdk.BuildConfig
import io.embrace.android.embracesdk.internal.OpenTelemetryClock
import io.embrace.android.embracesdk.internal.clock.Clock
import io.embrace.android.embracesdk.internal.clock.NormalizedIntervalClock
import io.embrace.android.embracesdk.internal.clock.SystemClock
import io.embrace.android.embracesdk.internal.spans.CurrentSessionSpan
import io.embrace.android.embracesdk.internal.spans.CurrentSessionSpanImpl
import io.embrace.android.embracesdk.internal.spans.EmbraceSpanExporter
import io.embrace.android.embracesdk.internal.spans.EmbraceSpanProcessor
import io.embrace.android.embracesdk.internal.spans.EmbraceSpansService
import io.embrace.android.embracesdk.internal.spans.EmbraceTracer
import io.embrace.android.embracesdk.internal.spans.SpansService
import io.embrace.android.embracesdk.internal.spans.SpansSink
import io.embrace.android.embracesdk.internal.spans.SpansSinkImpl
import io.embrace.android.embracesdk.telemetry.EmbraceTelemetryService
import io.embrace.android.embracesdk.telemetry.TelemetryService
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.trace.SdkTracerProvider

/**
* A module of components and services required at [EmbraceImpl] instantiation time, i.e. before the SDK evens starts
Expand All @@ -24,6 +35,14 @@ internal interface InitModule {
*/
val telemetryService: TelemetryService

val spansSink: SpansSink

val openTelemetrySdk: OpenTelemetry

val tracer: Tracer

val currentSessionSpan: CurrentSessionSpan

/**
* Service to log traces
*/
Expand All @@ -32,12 +51,36 @@ internal interface InitModule {
/**
* Implementation of public tracing API
*/
val tracer: EmbraceTracer
val embraceTracer: EmbraceTracer
}

internal class InitModuleImpl(
override val clock: Clock = NormalizedIntervalClock(systemClock = SystemClock()),
openTelemetryClock: OpenTelemetryClock = OpenTelemetryClock(clock),
override val telemetryService: TelemetryService = EmbraceTelemetryService(),
override val spansService: SpansService = EmbraceSpansService(OpenTelemetryClock(clock), telemetryService),
override val tracer: EmbraceTracer = EmbraceTracer(spansService)
override val spansSink: SpansSink = SpansSinkImpl(),
override val openTelemetrySdk: OpenTelemetry =
OpenTelemetrySdk.builder()
.setTracerProvider(
SdkTracerProvider
.builder()
.addSpanProcessor(EmbraceSpanProcessor(EmbraceSpanExporter(spansSink)))
.setClock(openTelemetryClock)
.build()
)
.build(),
override val tracer: Tracer = openTelemetrySdk.getTracer(BuildConfig.LIBRARY_PACKAGE_NAME, BuildConfig.VERSION_NAME),
override val currentSessionSpan: CurrentSessionSpan =
CurrentSessionSpanImpl(
clock = openTelemetryClock,
telemetryService = telemetryService,
spansSink = spansSink,
tracer = tracer
),
override val spansService: SpansService = EmbraceSpansService(
spansSink = spansSink,
currentSessionSpan = currentSessionSpan,
tracer = tracer,
),
override val embraceTracer: EmbraceTracer = EmbraceTracer(spansService)
) : InitModule
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ internal class SessionModuleImpl(
dataCaptureServiceModule.breadcrumbService,
essentialServiceModule.userService,
androidServicesModule.preferencesService,
initModule.spansService,
initModule.currentSessionSpan,
initModule.clock,
sessionPropertiesService,
dataCaptureServiceModule.startupService
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.embrace.android.embracesdk.internal.spans

import io.embrace.android.embracesdk.spans.EmbraceSpan

internal interface CurrentSessionSpan {

fun startInitialSession(sdkInitStartTimeNanos: Long)

fun endSession(appTerminationCause: EmbraceAttributes.AppTerminationCause? = null): List<EmbraceSpanData>

fun completedSpans(): List<EmbraceSpanData>

fun validateAndUpdateContext(parent: EmbraceSpan?, internal: Boolean): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package io.embrace.android.embracesdk.internal.spans

import io.embrace.android.embracesdk.spans.EmbraceSpan
import io.embrace.android.embracesdk.telemetry.TelemetryService
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.sdk.common.Clock
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference

internal class CurrentSessionSpanImpl(
private val clock: Clock,
private val telemetryService: TelemetryService,
private val spansSink: SpansSink,
private val tracer: Tracer,
) : CurrentSessionSpan {
/**
* Number of traces created in the current session. This value will be reset when a new session is created.
*/
private val currentSessionTraceCount = AtomicInteger(0)

private val currentSessionChildSpansCount = mutableMapOf<String, Int>()

/**
* The span that models the lifetime of the current session or background activity
*/
private val currentSessionSpan: AtomicReference<Span> = AtomicReference()

override fun startInitialSession(sdkInitStartTimeNanos: Long) {
synchronized(currentSessionSpan) {
currentSessionSpan.set(startSessionSpan(sdkInitStartTimeNanos))
}
}

/**
* Creating a new Span is only possible if the current session span is active, the parent has already been started, and the total
* session trace limit has not been reached. Once this method returns true, a new span is assumed to have been created and will
* be counted as such towards the limits, so make sure there's no case afterwards where a Span is not created.
*/
override fun validateAndUpdateContext(parent: EmbraceSpan?, internal: Boolean): Boolean {
if (!currentSessionSpan.get().isRecording || (parent != null && parent.spanId == null)) {
return false
}

if (!internal) {
if (parent == null) {
if (currentSessionTraceCount.get() < SpansServiceImpl.MAX_TRACE_COUNT_PER_SESSION) {
synchronized(currentSessionTraceCount) {
if (currentSessionTraceCount.get() < SpansServiceImpl.MAX_TRACE_COUNT_PER_SESSION) {
currentSessionTraceCount.incrementAndGet()
} else {
return false
}
}
} else {
return false
}
} else {
val rootSpanId = getRootSpanId(parent)
val currentSpanCount = currentSessionChildSpansCount[rootSpanId]
if (currentSpanCount == null) {
updateChildrenCount(rootSpanId)
} else if (currentSpanCount < SpansServiceImpl.MAX_SPAN_COUNT_PER_TRACE) {
synchronized(currentSessionChildSpansCount) {
val currentSpanCountAgain = currentSessionChildSpansCount[rootSpanId]
if (currentSpanCountAgain == null || currentSpanCountAgain < SpansServiceImpl.MAX_SPAN_COUNT_PER_TRACE) {
updateChildrenCount(rootSpanId)
} else {
return false
}
}
} else {
return false
}
}
}

return true
}

override fun endSession(appTerminationCause: EmbraceAttributes.AppTerminationCause?): List<EmbraceSpanData> {
synchronized(this) {
// Right now, session spans don't survive native crashes and sudden process terminations,
// so telemetry will not be recorded in those cases, for now.
val telemetryAttributes = telemetryService.getAndClearTelemetryAttributes()

currentSessionSpan.get().setAllAttributes(Attributes.builder().fromMap(telemetryAttributes).build())

if (appTerminationCause == null) {
currentSessionSpan.get().endSpan()
spansSink.getSpansRepository()?.clearCompletedSpans()
currentSessionSpan.set(startSessionSpan(clock.now()))
} else {
currentSessionSpan.get()?.let {
it.setAttribute(appTerminationCause.keyName(), appTerminationCause.name)
it.endSpan()
}
}

return spansSink.flushSpans()
}
}

override fun completedSpans(): List<EmbraceSpanData> = spansSink.completedSpans()

private fun getRootSpanId(span: EmbraceSpan): String {
var currentSpan: EmbraceSpan = span
while (currentSpan.parent != null) {
currentSpan.parent?.let { currentSpan = it }
}

return currentSpan.spanId ?: ""
}

private fun updateChildrenCount(rootSpanId: String) {
val currentCount = currentSessionChildSpansCount[rootSpanId]
if (currentCount == null) {
// The first time we'll know a root span ID is when a child is being added to it. Prior to that, when adding a prospective
// root span, the ID is not known yet. So the first time a root span is encountered add both it and the new child to the count.
//
// NOTE: Because we don't know whether the root span is internal or not at this point, it is assumed that it isn't.
// Therefore, it will count towards the limit if a non-internal span is added to the trace.
currentSessionChildSpansCount[rootSpanId] = 2
} else {
currentSessionChildSpansCount[rootSpanId] = currentCount + 1
}
}

/**
* This method should always be used when starting a new session span
*/
private fun startSessionSpan(startTimeNanos: Long): Span {
currentSessionTraceCount.set(0)
return createEmbraceSpanBuilder(tracer = tracer, name = "session-span", type = EmbraceAttributes.Type.SESSION)
.setNoParent()
.setStartTimestamp(startTimeNanos, TimeUnit.NANOSECONDS)
.startSpan()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ internal fun Tracer.embraceSpanBuilder(name: String, internal: Boolean): SpanBui
}
}

/**
* Create a [SpanBuilder] that will create a [Span] with the appropriate custom Embrace attributes
*/
internal fun createEmbraceSpanBuilder(
tracer: Tracer,
name: String,
type: EmbraceAttributes.Type,
internal: Boolean = true
): SpanBuilder = tracer.embraceSpanBuilder(name, internal).setType(type)

/**
* Create a [SpanBuilder] that will create a root [Span] with the appropriate custom Embrace attributes
*/
internal fun createRootSpanBuilder(
tracer: Tracer,
name: String,
type: EmbraceAttributes.Type,
internal: Boolean
): SpanBuilder = createEmbraceSpanBuilder(tracer = tracer, name = name, type = type, internal = internal).setNoParent()

/**
* Sets and returns the [EmbraceAttributes.Type] attribute for the given [SpanBuilder]
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import io.opentelemetry.sdk.trace.export.SpanExporter
* Note: no explicit tests exist for this as its functionality is tested via the tests for [SpansServiceImpl]
*/
@InternalApi
internal class EmbraceSpanExporter(private val spansService: SpansService) : SpanExporter {
internal class EmbraceSpanExporter(private val spansSink: SpansSink) : SpanExporter {
@Synchronized
override fun export(spans: MutableCollection<SpanData>): CompletableResultCode =
spansService.storeCompletedSpans(spans.toList())
spansSink.storeCompletedSpans(spans.toList())

override fun flush(): CompletableResultCode = CompletableResultCode.ofSuccess()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package io.embrace.android.embracesdk.internal.spans
import io.embrace.android.embracesdk.spans.EmbraceSpan
import io.embrace.android.embracesdk.spans.EmbraceSpanEvent
import io.embrace.android.embracesdk.spans.ErrorCode
import io.embrace.android.embracesdk.telemetry.TelemetryService
import io.opentelemetry.sdk.common.Clock
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.sdk.common.CompletableResultCode
import io.opentelemetry.sdk.trace.data.SpanData
import java.util.concurrent.atomic.AtomicBoolean
Expand All @@ -17,8 +16,9 @@ import java.util.concurrent.atomic.AtomicBoolean
* it may not be fast and doing it in the background doesn't affect how it works.
*/
internal class EmbraceSpansService(
private val clock: Clock,
private val telemetryService: TelemetryService
private val spansSink: SpansSink,
private val currentSessionSpan: CurrentSessionSpan,
private val tracer: Tracer,
) : Initializable, SpansService {
/**
* When this instance has been initialized with an instance of [SpansService] that does the proper spans logging
Expand All @@ -27,21 +27,18 @@ internal class EmbraceSpansService(

private val uninitializedSdkSpansService: UninitializedSdkSpansService = UninitializedSdkSpansService()

@Volatile
private var sdkInitStartTime: Long? = null

@Volatile
private var currentDelegate: SpansService = uninitializedSdkSpansService

override fun initializeService(sdkInitStartTimeNanos: Long) {
if (!initialized.get()) {
sdkInitStartTime = sdkInitStartTimeNanos
synchronized(initialized) {
if (!initialized.get()) {
currentSessionSpan.startInitialSession(sdkInitStartTimeNanos)
currentDelegate = SpansServiceImpl(
sdkInitStartTimeNanos = sdkInitStartTimeNanos,
clock = clock,
telemetryService = telemetryService
spansSink = spansSink,
currentSessionSpan = currentSessionSpan,
tracer = tracer,
)
initialized.set(true)
uninitializedSdkSpansService.recordBufferedCalls(this)
Expand Down Expand Up @@ -89,10 +86,11 @@ internal class EmbraceSpansService(
override fun storeCompletedSpans(spans: List<SpanData>): CompletableResultCode =
currentDelegate.storeCompletedSpans(spans = spans)

override fun completedSpans(): List<EmbraceSpanData>? = currentDelegate.completedSpans()
override fun completedSpans(): List<EmbraceSpanData> = currentDelegate.completedSpans()

override fun flushSpans(appTerminationCause: EmbraceAttributes.AppTerminationCause?): List<EmbraceSpanData>? =
currentDelegate.flushSpans(appTerminationCause = appTerminationCause)
override fun flushSpans(): List<EmbraceSpanData> = currentDelegate.flushSpans()

override fun getSpan(spanId: String): EmbraceSpan? = currentDelegate.getSpan(spanId)

override fun getSpansRepository(): SpansRepository? = currentDelegate.getSpansRepository()
}
Loading

0 comments on commit a6cb018

Please sign in to comment.