Skip to content
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
125 changes: 67 additions & 58 deletions Confidence/src/main/java/com/spotify/confidence/Telemetry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,66 +32,68 @@ internal class Telemetry(
}

fun encodedHeaderValue(): String? {
val (evalSnapshot, resolveSnapshot) = synchronized(lock) {
val evals = evaluationTraces.toList()
val resolves = resolveLatencyTraces.toList()
val snapshot = synchronized(lock) {
val s = Snapshot(
evaluations = evaluationTraces.toList(),
resolveTraces = resolveLatencyTraces.toList(),
library = library
)
evaluationTraces.clear()
resolveLatencyTraces.clear()
Pair(evals, resolves)
s
}

if (evalSnapshot.isEmpty() && resolveSnapshot.isEmpty()) return null
if (snapshot.evaluations.isEmpty() && snapshot.resolveTraces.isEmpty()) return null

val bytes = encodeMonitoring(evalSnapshot, resolveSnapshot)
val bytes = encodeMonitoring(snapshot)
return Base64.encodeToString(bytes, Base64.NO_WRAP)
}

private fun encodeMonitoring(
evals: List<EvaluationTrace>,
resolves: List<ResolveLatencyTrace>
): ByteArray {
private data class Snapshot(
val evaluations: List<EvaluationTrace>,
val resolveTraces: List<ResolveLatencyTrace>,
val library: Library
)

private fun encodeMonitoring(snapshot: Snapshot): ByteArray {
val out = ByteArrayOutputStream()

// field 1: LibraryTraces (length-delimited)
val libraryTracesBytes = encodeLibraryTraces(evals, resolves)
val libraryTracesBytes = encodeLibraryTraces(snapshot)
out.writeTag(1, WIRE_TYPE_LENGTH_DELIMITED)
out.writeVarint(libraryTracesBytes.size)
out.writeVarint(libraryTracesBytes.size.toLong())
out.write(libraryTracesBytes)

// field 2: platform (varint) - KOTLIN = 2
out.writeTag(2, WIRE_TYPE_VARINT)
out.writeVarint(Platform.KOTLIN.value)
if (Platform.KOTLIN.value != 0) {
out.writeTag(2, WIRE_TYPE_VARINT)
out.writeVarint(Platform.KOTLIN.value.toLong())
}

return out.toByteArray()
}

private fun encodeLibraryTraces(
evals: List<EvaluationTrace>,
resolves: List<ResolveLatencyTrace>
): ByteArray {
private fun encodeLibraryTraces(snapshot: Snapshot): ByteArray {
val out = ByteArrayOutputStream()

// field 1: library (varint)
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(library.value)
if (snapshot.library.value != 0) {
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(snapshot.library.value.toLong())
}

// field 2: library_version (string)
out.writeTag(2, WIRE_TYPE_LENGTH_DELIMITED)
val versionBytes = sdkVersion.toByteArray(Charsets.UTF_8)
out.writeVarint(versionBytes.size)
out.writeTag(2, WIRE_TYPE_LENGTH_DELIMITED)
out.writeVarint(versionBytes.size.toLong())
out.write(versionBytes)

// field 3: traces (repeated)
for (resolve in resolves) {
for (resolve in snapshot.resolveTraces) {
val traceBytes = encodeResolveLatencyTrace(resolve)
out.writeTag(3, WIRE_TYPE_LENGTH_DELIMITED)
out.writeVarint(traceBytes.size)
out.writeVarint(traceBytes.size.toLong())
out.write(traceBytes)
}
for (eval in evals) {
for (eval in snapshot.evaluations) {
val traceBytes = encodeEvaluationTrace(eval)
out.writeTag(3, WIRE_TYPE_LENGTH_DELIMITED)
out.writeVarint(traceBytes.size)
out.writeVarint(traceBytes.size.toLong())
out.write(traceBytes)
}

Expand All @@ -101,14 +103,12 @@ internal class Telemetry(
private fun encodeResolveLatencyTrace(trace: ResolveLatencyTrace): ByteArray {
val out = ByteArrayOutputStream()

// field 1: id (varint) - RESOLVE_LATENCY = 1
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(TraceId.RESOLVE_LATENCY.value)
out.writeVarint(TraceId.RESOLVE_LATENCY.value.toLong())

// field 3: request_trace (length-delimited) - oneof field 3
val requestTraceBytes = encodeRequestTrace(trace)
out.writeTag(3, WIRE_TYPE_LENGTH_DELIMITED)
out.writeVarint(requestTraceBytes.size)
out.writeVarint(requestTraceBytes.size.toLong())
out.write(requestTraceBytes)

return out.toByteArray()
Expand All @@ -117,43 +117,47 @@ internal class Telemetry(
private fun encodeRequestTrace(trace: ResolveLatencyTrace): ByteArray {
val out = ByteArrayOutputStream()

// field 1: latency_ms (varint)
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(trace.durationMs.toInt())
if (trace.durationMs != 0L) {
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(trace.durationMs)
}

// field 2: status (varint)
out.writeTag(2, WIRE_TYPE_VARINT)
out.writeVarint(trace.status.value)
if (trace.status.value != 0) {
out.writeTag(2, WIRE_TYPE_VARINT)
out.writeVarint(trace.status.value.toLong())
}

return out.toByteArray()
}

private fun encodeEvaluationTrace(trace: EvaluationTrace): ByteArray {
val out = ByteArrayOutputStream()

// field 1: id (varint) - FLAG_EVALUATION = 3
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(TraceId.FLAG_EVALUATION.value)
out.writeVarint(TraceId.FLAG_EVALUATION.value.toLong())

// field 5: evaluation_trace (length-delimited) - oneof field 5
val evalTraceBytes = encodeEvalTraceBody(trace)
out.writeTag(5, WIRE_TYPE_LENGTH_DELIMITED)
out.writeVarint(evalTraceBytes.size)
out.write(evalTraceBytes)
if (evalTraceBytes.isNotEmpty()) {
out.writeTag(5, WIRE_TYPE_LENGTH_DELIMITED)
out.writeVarint(evalTraceBytes.size.toLong())
out.write(evalTraceBytes)
}

return out.toByteArray()
}

private fun encodeEvalTraceBody(trace: EvaluationTrace): ByteArray {
val out = ByteArrayOutputStream()

// field 1: reason (varint)
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(trace.reason.value)
if (trace.reason.value != 0) {
out.writeTag(1, WIRE_TYPE_VARINT)
out.writeVarint(trace.reason.value.toLong())
}

// field 2: error_code (varint)
out.writeTag(2, WIRE_TYPE_VARINT)
out.writeVarint(trace.errorCode.value)
if (trace.errorCode.value != 0) {
out.writeTag(2, WIRE_TYPE_VARINT)
out.writeVarint(trace.errorCode.value.toLong())
}

return out.toByteArray()
}
Expand Down Expand Up @@ -204,16 +208,16 @@ internal class Telemetry(
}

private fun ByteArrayOutputStream.writeTag(fieldNumber: Int, wireType: Int) {
writeVarint((fieldNumber shl 3) or wireType)
writeVarint(((fieldNumber shl 3) or wireType).toLong())
}

private fun ByteArrayOutputStream.writeVarint(value: Int) {
private fun ByteArrayOutputStream.writeVarint(value: Long) {
var v = value
while (v and 0x7F.inv() != 0) {
write((v and 0x7F) or 0x80)
while (v and 0x7FL.inv() != 0L) {
write(((v and 0x7F) or 0x80).toInt())
v = v ushr 7
}
write(v)
write(v.toInt())
}
}

Expand Down Expand Up @@ -244,6 +248,9 @@ internal class Telemetry(
DEFAULT(2),
STALE(3),
DISABLED(4),
CACHED(5),
STATIC(6),
SPLIT(7),
ERROR(8)
}

Expand All @@ -252,8 +259,10 @@ internal class Telemetry(
PROVIDER_NOT_READY(1),
FLAG_NOT_FOUND(2),
PARSE_ERROR(3),
TYPE_MISMATCH(4),
TARGETING_KEY_MISSING(5),
INVALID_CONTEXT(6),
PROVIDER_FATAL(7),
GENERAL(8)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,6 @@ internal class ConfidenceRemoteClientTests {
whenever(mockClock.currentTime()).thenReturn(sendDate)

val telemetry = Telemetry(SDK_ID + "_TEST", Telemetry.Library.CONFIDENCE, "1.0.0")
// No events tracked

var recordedRequest: RecordedRequest? = null
mockWebServer.dispatcher = object : Dispatcher() {
Expand Down
50 changes: 39 additions & 11 deletions Confidence/src/test/java/com/spotify/confidence/TelemetryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class TelemetryTest {
Telemetry.EvaluationErrorCode.UNSPECIFIED
)

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())

assertEquals(ProtoPlatform.PLATFORM_KOTLIN, monitoring.platform)
assertEquals(1, monitoring.libraryTracesCount)
Expand All @@ -248,7 +248,7 @@ class TelemetryTest {
val telemetry = Telemetry("test-sdk", Telemetry.Library.CONFIDENCE, "2.0.0")
telemetry.trackResolveLatency(142, Telemetry.RequestStatus.SUCCESS)

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())

assertEquals(ProtoPlatform.PLATFORM_KOTLIN, monitoring.platform)

Expand All @@ -273,7 +273,7 @@ class TelemetryTest {
Telemetry.EvaluationErrorCode.FLAG_NOT_FOUND
)

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())
val lib = monitoring.getLibraryTraces(0)

assertEquals(LibraryTraces.Library.LIBRARY_OPEN_FEATURE, lib.library)
Expand All @@ -298,7 +298,7 @@ class TelemetryTest {
Telemetry.EvaluationErrorCode.UNSPECIFIED
)

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())
val traces = monitoring.getLibraryTraces(0).tracesList
assertEquals(4, traces.size)

Expand All @@ -324,7 +324,7 @@ class TelemetryTest {
val telemetry = Telemetry("test-sdk", Telemetry.Library.CONFIDENCE, "1.0.0")
telemetry.trackResolveLatency(250, Telemetry.RequestStatus.ERROR)

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())
val req = monitoring.getLibraryTraces(0).getTraces(0).requestTrace
assertEquals(250L, req.millisecondDuration)
assertEquals(ProtoStatus.STATUS_ERROR, req.status)
Expand Down Expand Up @@ -370,6 +370,24 @@ class TelemetryTest {
ProtoReason.EVALUATION_REASON_DISABLED,
ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED
),
Case(
Telemetry.EvaluationReason.CACHED,
Telemetry.EvaluationErrorCode.UNSPECIFIED,
ProtoReason.EVALUATION_REASON_CACHED,
ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED
),
Case(
Telemetry.EvaluationReason.STATIC,
Telemetry.EvaluationErrorCode.UNSPECIFIED,
ProtoReason.EVALUATION_REASON_STATIC,
ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED
),
Case(
Telemetry.EvaluationReason.SPLIT,
Telemetry.EvaluationErrorCode.UNSPECIFIED,
ProtoReason.EVALUATION_REASON_SPLIT,
ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED
),
Case(
Telemetry.EvaluationReason.ERROR,
Telemetry.EvaluationErrorCode.GENERAL,
Expand Down Expand Up @@ -405,14 +423,26 @@ class TelemetryTest {
Telemetry.EvaluationErrorCode.TARGETING_KEY_MISSING,
ProtoReason.EVALUATION_REASON_ERROR,
ProtoErrorCode.EVALUATION_ERROR_CODE_TARGETING_KEY_MISSING
),
Case(
Telemetry.EvaluationReason.ERROR,
Telemetry.EvaluationErrorCode.TYPE_MISMATCH,
ProtoReason.EVALUATION_REASON_ERROR,
ProtoErrorCode.EVALUATION_ERROR_CODE_TYPE_MISMATCH
),
Case(
Telemetry.EvaluationReason.ERROR,
Telemetry.EvaluationErrorCode.PROVIDER_FATAL,
ProtoReason.EVALUATION_REASON_ERROR,
ProtoErrorCode.EVALUATION_ERROR_CODE_PROVIDER_FATAL
)
)

for (case in cases) {
val telemetry = Telemetry("test-sdk", Telemetry.Library.CONFIDENCE, "1.0.0")
telemetry.trackEvaluation(case.reason, case.errorCode)

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())
val eval = monitoring.getLibraryTraces(0).getTraces(0).evaluationTrace

assertEquals("reason for ${case.reason}", case.expectedReason, eval.reason)
Expand Down Expand Up @@ -449,17 +479,14 @@ class TelemetryTest {
debugLogger = null
)

// Default should be CONFIDENCE
assertEquals(Telemetry.Library.CONFIDENCE, confidence.telemetry.library)

// Call the private method via reflection (same as ConfidenceFeatureProvider.create)
val method = confidence.javaClass.getDeclaredMethod("setTelemetryLibraryOpenFeature")
method.isAccessible = true
method.invoke(confidence)

assertEquals(Telemetry.Library.OPEN_FEATURE, confidence.telemetry.library)

// Verify encoded header uses OPEN_FEATURE
confidence.telemetry.trackEvaluation(
Telemetry.EvaluationReason.DEFAULT,
Telemetry.EvaluationErrorCode.UNSPECIFIED
Expand Down Expand Up @@ -532,7 +559,7 @@ class TelemetryTest {
latch.await()
executor.shutdown()

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())
val traces = monitoring.getLibraryTraces(0).tracesList
// Each list is capped at 100, so max 200 total (100 eval + 100 latency)
assertTrue("Traces should be capped at 200", traces.size <= 200)
Expand All @@ -554,7 +581,7 @@ class TelemetryTest {
telemetry.trackResolveLatency(50, Telemetry.RequestStatus.SUCCESS)
}

val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!)
val monitoring = decodeMonitoring(telemetry.encodedHeaderValue())
val traces = monitoring.getLibraryTraces(0).tracesList
val evalTraces = traces.filter {
it.id == LibraryTraces.TraceId.TRACE_ID_FLAG_EVALUATION
Expand Down Expand Up @@ -640,6 +667,7 @@ class TelemetryTest {
assertNotNull("Expected ${Telemetry.HEADER_NAME} header", headerValue)

val monitoring = decodeMonitoring(headerValue!!)

assertEquals(ProtoPlatform.PLATFORM_KOTLIN, monitoring.platform)

val lib = monitoring.getLibraryTraces(0)
Expand Down
Loading