Skip to content

Commit bb851ec

Browse files
authored
Emit OTel spans to toolkit telemetry service (#5046)
This PR transforms OTel spans to the format expected by the current telemetry service
1 parent 49e31b2 commit bb851ec

File tree

5 files changed

+340
-27
lines changed

5 files changed

+340
-27
lines changed

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ private object StdoutSpanProcessor : SpanProcessor {
132132
@Service
133133
class OTelService @NonInjectable internal constructor(spanProcessors: List<SpanProcessor>) : Disposable {
134134
@Suppress("unused")
135-
constructor() : this(listOf(StdoutSpanProcessor))
135+
constructor() : this(listOf(ToolkitTelemetryOTelSpanProcessor()))
136136

137137
private val sdkDelegate = lazy {
138138
OpenTelemetrySdk.builder()

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import com.intellij.platform.diagnostic.telemetry.helpers.use as ijUse
3333
import com.intellij.platform.diagnostic.telemetry.helpers.useWithScope as ijUseWithScope
3434

3535
val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named<AWSProduct>("pluginDescriptor")
36-
internal val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin")
36+
internal val PLUGIN_NAME_ATTRIBUTE_KEY = AttributeKey.stringKey("pluginName")
3737

3838
class DefaultSpan(context: Context?, delegate: Span) : BaseSpan<DefaultSpan>(context, delegate)
3939

@@ -175,7 +175,7 @@ abstract class AbstractSpanBuilder<
175175
requireNotNull(parent)
176176

177177
parent.get(AWS_PRODUCT_CONTEXT_KEY)?.toString()?.let {
178-
setAttribute(PLUGIN_ATTRIBUTE_KEY, it)
178+
setAttribute(PLUGIN_NAME_ATTRIBUTE_KEY, it)
179179
} ?: run {
180180
LOG.warn { "Reached setAttribute with null AWS_PRODUCT_CONTEXT_KEY, but should not be possible" }
181181
}
@@ -193,7 +193,7 @@ abstract class AbstractBaseSpan<SpanType : AbstractBaseSpan<SpanType>>(internal
193193
protected open val requiredFields: Collection<String> = emptySet()
194194
private var passive: Boolean = false
195195
private var unit: MetricUnit = MetricUnit.NONE
196-
private var value: Double = 0.0
196+
private var value: Double = 1.0
197197

198198
/**
199199
* Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan]
@@ -210,12 +210,19 @@ abstract class AbstractBaseSpan<SpanType : AbstractBaseSpan<SpanType>>(internal
210210
return this as SpanType
211211
}
212212

213+
override fun recordException(exception: Throwable): SpanType {
214+
delegate.recordException(exception)
215+
return this as SpanType
216+
}
217+
213218
override fun end() {
219+
finalize()
214220
validateRequiredAttributes()
215221
delegate.end()
216222
}
217223

218224
override fun end(timestamp: Long, unit: TimeUnit) {
225+
finalize()
219226
validateRequiredAttributes()
220227
delegate.end()
221228
}
@@ -235,6 +242,12 @@ abstract class AbstractBaseSpan<SpanType : AbstractBaseSpan<SpanType>>(internal
235242
return this as SpanType
236243
}
237244

245+
private fun finalize() {
246+
setAttribute("passive", passive)
247+
setAttribute("unit", unit.toString())
248+
setAttribute("value", value)
249+
}
250+
238251
private fun validateRequiredAttributes() {
239252
val missingFields = requiredFields.filter { delegate.getAttribute(AttributeKey.stringKey(it)) == null }
240253
val message = { "${delegate.name} is missing required fields: ${missingFields.joinToString(", ")}" }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.telemetry.otel
5+
6+
import io.opentelemetry.context.Context
7+
import io.opentelemetry.sdk.trace.ReadWriteSpan
8+
import io.opentelemetry.sdk.trace.ReadableSpan
9+
import io.opentelemetry.sdk.trace.SpanProcessor
10+
import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct
11+
import software.amazon.awssdk.services.toolkittelemetry.model.MetricUnit
12+
import software.aws.toolkits.core.utils.tryOrNull
13+
import software.aws.toolkits.jetbrains.AwsPlugin
14+
import software.aws.toolkits.jetbrains.AwsToolkit
15+
import software.aws.toolkits.jetbrains.services.telemetry.MetricEventMetadata
16+
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
17+
import java.time.Instant
18+
import kotlin.time.Duration.Companion.nanoseconds
19+
20+
class ToolkitTelemetryOTelSpanProcessor : SpanProcessor {
21+
override fun isStartRequired() = false
22+
override fun isEndRequired() = true
23+
24+
override fun onStart(parentContext: Context, span: ReadWriteSpan) {}
25+
26+
override fun onEnd(span: ReadableSpan) {
27+
val data = span.toSpanData()
28+
val product = data.attributes.get(PLUGIN_NAME_ATTRIBUTE_KEY)?.let { AWSProduct.fromValue(it) } ?: AWSProduct.AWS_TOOLKIT_FOR_JET_BRAINS
29+
val version = tryOrNull {
30+
when (product) {
31+
AWSProduct.AWS_TOOLKIT_FOR_JET_BRAINS -> AwsToolkit.PLUGINS_INFO[AwsPlugin.TOOLKIT]?.version
32+
AWSProduct.AMAZON_Q_FOR_JET_BRAINS -> AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.version
33+
else -> null
34+
}
35+
} ?: "unknown"
36+
37+
TelemetryService.getInstance().record(
38+
MetricEventMetadata(
39+
awsProduct = product,
40+
awsVersion = version,
41+
)
42+
) {
43+
createTime(Instant.ofEpochSecond(0L, data.startEpochNanos))
44+
45+
datum(data.name) {
46+
val attributes = data.attributes.asMap().entries.associate { it.key.key to it.value }.toMutableMap()
47+
// goes on root of payload
48+
attributes.remove(PLUGIN_NAME_ATTRIBUTE_KEY.key)
49+
50+
// special handling attributes
51+
passive(attributes.remove("passive") as Boolean)
52+
unit(MetricUnit.fromValue(attributes.remove("unit") as String))
53+
value(attributes.remove("value") as Double)
54+
55+
// everything else
56+
attributes.forEach { t, u ->
57+
metadata(t, u.toString())
58+
}
59+
60+
// auto-duration
61+
if (attributes["duration"] == null && data.endEpochNanos != 0L) {
62+
metadata("duration", (data.endEpochNanos - data.startEpochNanos).nanoseconds.inWholeMilliseconds.toString())
63+
}
64+
65+
// the reason why we used opentelemetry
66+
metadata("traceId", data.traceId)
67+
metadata("metricId", data.spanId)
68+
metadata("parentId", data.parentSpanId)
69+
}
70+
}
71+
}
72+
}

plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class OtelBaseTest {
7272

7373
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
7474
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
75-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
75+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
7676
}
7777
}
7878

@@ -88,7 +88,7 @@ class OtelBaseTest {
8888

8989
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
9090
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
91-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
91+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
9292
}
9393
}
9494

@@ -104,8 +104,8 @@ class OtelBaseTest {
104104

105105
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
106106
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
107-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
108-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
107+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
108+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
109109
}
110110
}
111111

@@ -122,8 +122,8 @@ class OtelBaseTest {
122122

123123
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
124124
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
125-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isNotEqualTo(child.getAttribute(PLUGIN_ATTRIBUTE_KEY))
126-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
125+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isNotEqualTo(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
126+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
127127
}
128128
}
129129

@@ -141,8 +141,8 @@ class OtelBaseTest {
141141

142142
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
143143
assertThat(child.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
144-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
145-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isNotEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
144+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
145+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isNotEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
146146
}
147147
}
148148

@@ -160,8 +160,8 @@ class OtelBaseTest {
160160

161161
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
162162
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
163-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
164-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
163+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
164+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
165165
}
166166
}
167167

@@ -181,8 +181,8 @@ class OtelBaseTest {
181181

182182
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
183183
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
184-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
185-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
184+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
185+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
186186
}
187187
}
188188

@@ -198,8 +198,8 @@ class OtelBaseTest {
198198

199199
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
200200
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
201-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
202-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
201+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
202+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
203203
}
204204
}
205205

@@ -215,8 +215,8 @@ class OtelBaseTest {
215215

216216
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
217217
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
218-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
219-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
218+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
219+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
220220
}
221221
}
222222

@@ -234,8 +234,8 @@ class OtelBaseTest {
234234

235235
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
236236
assertThat(child.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
237-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
238-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isNotEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
237+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
238+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isNotEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
239239
}
240240
}
241241

@@ -253,8 +253,8 @@ class OtelBaseTest {
253253

254254
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
255255
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
256-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
257-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
256+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
257+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
258258
}
259259
}
260260

@@ -285,8 +285,8 @@ class OtelBaseTest {
285285

286286
assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid())
287287
assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId)
288-
assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
289-
assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY))
288+
assertThat(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code")
289+
assertThat(child.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_NAME_ATTRIBUTE_KEY))
290290
}
291291
}
292292

@@ -339,7 +339,8 @@ class OtelExtension : AfterEachCallback, AfterAllCallback {
339339
assert(openSpans.isEmpty()) { "Not all open spans were closed: ${openSpans.joinToString(", ")}" }
340340
return CompletableResultCode.ofSuccess()
341341
}
342-
}
342+
},
343+
ToolkitTelemetryOTelSpanProcessor()
343344
)
344345
)
345346

0 commit comments

Comments
 (0)