Skip to content

Commit b735210

Browse files
committed
feat: add PersistedTelemetryRecord
1 parent 8bb0556 commit b735210

File tree

10 files changed

+213
-0
lines changed

10 files changed

+213
-0
lines changed

exporters-persistence/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# exporters-core
2+
3+
This module contains exporters that persist telemetry before attempting to send it, thus avoiding
4+
data loss in a process termination scenario.

exporters-persistence/api/android/exporters-persistence.api

Whitespace-only changes.

exporters-persistence/api/jvm/exporters-persistence.api

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
plugins {
2+
kotlin("multiplatform")
3+
id("com.android.kotlin.multiplatform.library")
4+
id("io.opentelemetry.kotlin.build-logic")
5+
id("signing")
6+
id("com.vanniktech.maven.publish")
7+
id("org.jetbrains.kotlinx.kover")
8+
}
9+
10+
kotlin {
11+
sourceSets {
12+
val commonMain by getting {
13+
dependencies {
14+
implementation(project(":api"))
15+
implementation(project(":exporters-otlp"))
16+
implementation(libs.kotlinx.coroutines)
17+
}
18+
}
19+
val commonTest by getting {
20+
dependencies {
21+
implementation(project(":test-fakes"))
22+
implementation(project(":integration-test"))
23+
implementation(libs.kotlin.test)
24+
implementation(libs.kotlinx.coroutines.test)
25+
}
26+
}
27+
val jvmTest by getting {
28+
dependencies {
29+
implementation(libs.kotlin.test)
30+
}
31+
}
32+
}
33+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.opentelemetry.kotlin.export
2+
3+
internal class PersistedTelemetryConfig(
4+
5+
/**
6+
* Maximum number of batches that should be stored for each telemetry signal. 100 by default.
7+
* For example, 100 batches of X logs may be stored before old telemetry is deleted.
8+
*/
9+
val maxBatchedItemsPerSignal: Int = 100,
10+
11+
/**
12+
* Maximum age of telemetry before it should be deleted. Old telemetry is not considered useful
13+
* so it can be deleted after 30 days by default.
14+
*/
15+
val maxTelemetryAgeInDays: Long = 30,
16+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.opentelemetry.kotlin.export
2+
3+
/**
4+
* Creates a filename that can be used to persist a telemetry record.
5+
*/
6+
internal data class PersistedTelemetryRecord(
7+
val timestamp: Long,
8+
val type: PersistedTelemetryType,
9+
val uid: String,
10+
) {
11+
12+
/**
13+
* String filename that encodes information about the telemetry record.
14+
*/
15+
val filename: String = "${type}_${timestamp}_$uid.gz"
16+
17+
companion object {
18+
19+
private const val EXTENSION = ".gz"
20+
private const val DELIMITER = "_"
21+
22+
/**
23+
* Comparator that orders records by timestamp (oldest first), then type, then uid.
24+
*/
25+
val comparator: Comparator<PersistedTelemetryRecord> = compareBy(
26+
{ it.timestamp },
27+
{ it.type },
28+
{ it.uid }
29+
)
30+
31+
/**
32+
* Decodes a filename to gain information about the telemetry recorded within.
33+
*
34+
* Returns null if the filename does not match the expected format.
35+
*/
36+
fun fromFilename(filename: String): PersistedTelemetryRecord? {
37+
if (!filename.endsWith(EXTENSION)) {
38+
return null
39+
}
40+
val parts = filename.removeSuffix(EXTENSION).split(DELIMITER, limit = 3)
41+
if (parts.size != 3) {
42+
return null
43+
}
44+
val type = try {
45+
PersistedTelemetryType.valueOf(parts[0])
46+
} catch (e: IllegalArgumentException) {
47+
return null
48+
}
49+
val timestamp = parts[1].toLongOrNull() ?: return null
50+
return PersistedTelemetryRecord(timestamp, type, parts[2])
51+
}
52+
}
53+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.opentelemetry.kotlin.export
2+
3+
/**
4+
* The type of telemetry that is persisted.
5+
*/
6+
internal enum class PersistedTelemetryType {
7+
LOGS, SPANS,
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.opentelemetry.kotlin.export
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
6+
internal class PersistedTelemetryConfigTest {
7+
8+
@Test
9+
fun testDefaults() {
10+
val cfg = PersistedTelemetryConfig()
11+
assertEquals(30, cfg.maxTelemetryAgeInDays)
12+
assertEquals(100, cfg.maxBatchedItemsPerSignal)
13+
}
14+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package io.opentelemetry.kotlin.export
2+
3+
import io.opentelemetry.kotlin.export.PersistedTelemetryRecord.Companion.comparator
4+
import io.opentelemetry.kotlin.export.PersistedTelemetryRecord.Companion.fromFilename
5+
import io.opentelemetry.kotlin.export.PersistedTelemetryType.LOGS
6+
import io.opentelemetry.kotlin.export.PersistedTelemetryType.SPANS
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertNull
10+
11+
internal class PersistedTelemetryRecordTest {
12+
13+
@Test
14+
fun testFilenameEncodesLogType() {
15+
val record = PersistedTelemetryRecord(
16+
timestamp = 1234567890L,
17+
type = LOGS,
18+
uid = "abc-123"
19+
)
20+
assertEquals("LOGS_1234567890_abc-123.gz", record.filename)
21+
val decoded = fromFilename(record.filename)
22+
assertEquals(record, decoded)
23+
}
24+
25+
@Test
26+
fun testFilenameEncodesSpansType() {
27+
val record = PersistedTelemetryRecord(
28+
timestamp = 9876543210L,
29+
type = SPANS,
30+
uid = "xyz-789"
31+
)
32+
assertEquals("SPANS_9876543210_xyz-789.gz", record.filename)
33+
val decoded = fromFilename(record.filename)
34+
assertEquals(record, decoded)
35+
}
36+
37+
@Test
38+
fun testFromFilenameReturnsNullForInvalid() {
39+
val invalid = listOf(
40+
"LOGS_1234567890_abc-123",
41+
"INVALID_1234567890_abc-123.gz",
42+
"LOGS_notanumber_abc-123.gz",
43+
"LOGS_1234567890.gz",
44+
"",
45+
)
46+
invalid.forEach {
47+
val record = fromFilename(it)
48+
assertNull(record)
49+
}
50+
}
51+
52+
@Test
53+
fun testFromFilenamePreservesuidWithUnderscores() {
54+
val record = fromFilename("LOGS_1234567890_uid_with_underscores.gz")
55+
assertEquals("uid_with_underscores", record?.uid)
56+
}
57+
58+
@Test
59+
fun testComparatorSorting() {
60+
val a = PersistedTelemetryRecord(200L, SPANS, "a")
61+
val b = PersistedTelemetryRecord(100L, LOGS, "b")
62+
val c = PersistedTelemetryRecord(100L, LOGS, "a")
63+
val d = PersistedTelemetryRecord(100L, SPANS, "a")
64+
val e = PersistedTelemetryRecord(200L, LOGS, "a")
65+
66+
val records = listOf(a, b, c, d, e)
67+
val sorted = records.sortedWith(comparator)
68+
// Sorted by timestamp (oldest first), then type, then uid
69+
assertEquals(listOf(c, b, d, e, a), sorted)
70+
}
71+
72+
@Test
73+
fun testSortEmptyList() {
74+
val sorted = emptyList<PersistedTelemetryRecord>().sortedWith(comparator)
75+
assertEquals(emptyList(), sorted)
76+
}
77+
78+
@Test
79+
fun testSortSingleElement() {
80+
val record = PersistedTelemetryRecord(100L, LOGS, "a")
81+
val sorted = listOf(record).sortedWith(comparator)
82+
assertEquals(listOf(record), sorted)
83+
}
84+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ include(
3434
":exporters-core",
3535
":exporters-in-memory",
3636
":exporters-otlp",
37+
":exporters-persistence",
3738
":exporters-protobuf",
3839
":java-typealiases",
3940
"examples:jvm-app",

0 commit comments

Comments
 (0)