From 9a3743dcb97b529290e0ea1e5f8770ae6c2d85fd Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 17 Oct 2024 16:36:39 -0700 Subject: [PATCH 01/20] Add OpenTelemetry base classes --- .../services/telemetry/otel/OTelService.kt | 143 ++++++++++++++++ .../services/telemetry/otel/OtelBase.kt | 159 ++++++++++++++++++ .../jetbrains/utils/ThreadingUtils.kt | 4 +- 3 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt create mode 100644 plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt new file mode 100644 index 00000000000..696e73acb86 --- /dev/null +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt @@ -0,0 +1,143 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.telemetry.otel + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.platform.diagnostic.telemetry.impl.OpenTelemetryConfigurator +import com.intellij.platform.util.http.ContentType +import com.intellij.platform.util.http.httpPost +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator +import io.opentelemetry.context.Context +import io.opentelemetry.context.propagation.ContextPropagators +import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.ReadWriteSpan +import io.opentelemetry.sdk.trace.ReadableSpan +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.SpanProcessor +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider +import software.amazon.awssdk.http.ContentStreamProvider +import software.amazon.awssdk.http.HttpExecuteRequest +import software.amazon.awssdk.http.SdkHttpMethod +import software.amazon.awssdk.http.SdkHttpRequest +import software.amazon.awssdk.http.apache.ApacheHttpClient +import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner +import java.io.ByteArrayOutputStream +import java.net.ConnectException + +private class BasicOtlpSpanProcessor(private val coroutineScope: CoroutineScope, private val traceUrl: String = "http://127.0.0.1:4318/v1/traces") : SpanProcessor { + override fun onStart(parentContext: Context, span: ReadWriteSpan) {} + override fun isStartRequired() = false + override fun isEndRequired() = true + + override fun onEnd(span: ReadableSpan) { + val data = span.toSpanData() + coroutineScope.launch { + try { + val item = TraceRequestMarshaler.create(listOf(data)) + + httpPost(traceUrl, contentLength = item.binarySerializedSize.toLong(), contentType = ContentType.XProtobuf) { + item.writeBinaryTo(this) + } + } catch (e: CancellationException) { + throw e + } catch (e: ConnectException) { + thisLogger().warn("Cannot export (url=$traceUrl): ${e.message}") + } catch (e: Throwable) { + thisLogger().error("Cannot export (url=$traceUrl)", e) + } + } + } +} + +private class SigV4OtlpSpanProcessor(private val coroutineScope: CoroutineScope, private val traceUrl: String, private val creds: AwsCredentialsProvider) : SpanProcessor { + override fun onStart(parentContext: Context, span: ReadWriteSpan) {} + override fun isStartRequired() = false + override fun isEndRequired() = true + + private val client = ApacheHttpClient.create() + + override fun onEnd(span: ReadableSpan) { + coroutineScope.launch { + val data = span.toSpanData() + try { + val item = TraceRequestMarshaler.create(listOf(data)) + // calculate the sigv4 header + val signer = AwsV4HttpSigner.create() + val httpRequest = + SdkHttpRequest.builder() + .uri(traceUrl) + .method(SdkHttpMethod.POST) + .putHeader("Content-Type", "application/x-protobuf") + .build() + + val baos = ByteArrayOutputStream() + item.writeBinaryTo(baos) + val payload = ContentStreamProvider.fromByteArray(baos.toByteArray()) + val signedRequest = signer.sign { + it.identity(creds.resolveIdentity().get()) + it.request(httpRequest) + it.payload(payload) + it.putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, "osis") + it.putProperty(AwsV4HttpSigner.REGION_NAME, "us-west-2") + } + + // Create and HTTP client and send the request. ApacheHttpClient requires the 'apache-client' module. + client.prepareRequest( + HttpExecuteRequest.builder() + .request(signedRequest.request()) + .contentStreamProvider(signedRequest.payload().orElse(null)) + .build() + ).call() + } catch (e: CancellationException) { + throw e + } catch (e: ConnectException) { + thisLogger().warn("Cannot export (url=$traceUrl): ${e.message}") + } catch (e: Throwable) { + thisLogger().error("Cannot export (url=$traceUrl)", e) + } + } + } +} + +private object StdoutSpanProcessor : SpanProcessor { + override fun onStart(parentContext: Context, span: ReadWriteSpan) {} + override fun isStartRequired() = false + override fun isEndRequired() = true + + override fun onEnd(span: ReadableSpan) { + println(span.toSpanData()) + } +} + +@Service +class OTelService(private val cs: CoroutineScope) : Disposable { + private val sdkDelegate = lazy { + val configurator = OpenTelemetryConfigurator( + sdkBuilder = OpenTelemetrySdk.builder(), + ) + + configurator.getConfiguredSdkBuilder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(StdoutSpanProcessor) + .setResource(configurator.resource) + .build() + ) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build() + } + val sdk: OpenTelemetrySdk by sdkDelegate + + override fun dispose() { + if (sdkDelegate.isInitialized()) { + sdk.close() + } + } +} diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt new file mode 100644 index 00000000000..a72300c3f50 --- /dev/null +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -0,0 +1,159 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.telemetry.otel + +import com.intellij.platform.diagnostic.telemetry.helpers.useWithoutActiveScope +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanBuilder +import io.opentelemetry.api.trace.SpanContext +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.context.Context +import io.opentelemetry.context.ContextKey +import io.opentelemetry.context.Scope +import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct +import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver +import java.time.Instant +import java.util.concurrent.TimeUnit +import kotlin.use + +val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named("pluginDescriptor") +private val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") + +class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder(delegate) { + override fun doStartSpan() = BaseSpan(parent!!, delegate.startSpan()) +} + +abstract class AbstractSpanBuilder, Span : AbstractBaseSpan>(protected val delegate: SpanBuilder) : SpanBuilder { + /** + * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] + * + * @inheritdoc + */ + inline fun use(operation: (Span) -> T): T { + return startSpan().useWithoutActiveScope { span -> + (span as Span).makeCurrent().use { + operation(span) + } + } + } + + protected var parent: Context? = null + override fun setParent(context: Context): Builder { + parent = context + delegate.setParent(context) + return this as Builder + } + + override fun setNoParent(): Builder { + parent = null + delegate.setNoParent() + return this as Builder + } + + override fun addLink(spanContext: SpanContext): Builder { + delegate.addLink(spanContext) + return this as Builder + } + + override fun addLink( + spanContext: SpanContext, + attributes: Attributes, + ): Builder { + delegate.addLink(spanContext, attributes) + return this as Builder + } + + override fun setAttribute(key: String, value: String): Builder { + delegate.setAttribute(key, value) + return this as Builder + } + + override fun setAttribute(key: String, value: Long): Builder { + delegate.setAttribute(key, value) + return this as Builder + } + + override fun setAttribute(key: String, value: Double): Builder { + delegate.setAttribute(key, value) + return this as Builder + } + + override fun setAttribute(key: String, value: Boolean): Builder { + delegate.setAttribute(key, value) + return this as Builder + } + + override fun setAttribute( + key: AttributeKey, + value: V & Any, + ): Builder { + delegate.setAttribute(key, value) + return this as Builder + } + + override fun setAllAttributes(attributes: Attributes): Builder { + delegate.setAllAttributes(attributes) + return this as Builder + } + + override fun setSpanKind(spanKind: SpanKind): Builder { + delegate.setSpanKind(spanKind) + return this as Builder + } + + override fun setStartTimestamp(startTimestamp: Long, unit: TimeUnit): Builder { + delegate.setStartTimestamp(startTimestamp, unit) + return this as Builder + } + + override fun setStartTimestamp(startTimestamp: Instant): Builder { + delegate.setStartTimestamp(startTimestamp) + return this as Builder + } + + protected abstract fun doStartSpan(): Span + + override fun startSpan(): Span { + var parent = parent + if (parent == null) { + parent = Context.current() + } + requireNotNull(parent) + + val contextValue = parent.get(AWS_PRODUCT_CONTEXT_KEY) + if (contextValue == null) { + val s = Span.fromContextOrNull(parent) + if (s is AbstractBaseSpan) { + setParent(s.context.with(Span.fromContext(parent))) + } else { + setParent(parent.with(AWS_PRODUCT_CONTEXT_KEY, resolvePluginName())) + } + } + + setAttribute( + PLUGIN_ATTRIBUTE_KEY, + (parent.get(AWS_PRODUCT_CONTEXT_KEY) ?: resolvePluginName()).name + ) + + return doStartSpan() + } + + private fun resolvePluginName() = PluginResolver.Companion.fromStackTrace(Thread.currentThread().stackTrace).product +} + +abstract class AbstractBaseSpan(internal val context: Context, private val delegate: Span) : Span by delegate { + fun metadata(key: String, value: String) = setAttribute(key, value) + + override fun makeCurrent(): Scope = + context.with(this).makeCurrent() +} + +/** + * Placeholder; will be generated + */ +class BaseSpan(context: Context, delegate: Span) : AbstractBaseSpan(context, delegate) { + fun reason(reason: String) = metadata("reason", reason) +} diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/ThreadingUtils.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/ThreadingUtils.kt index 06df30c72f5..2fdca6b8eec 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/ThreadingUtils.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/ThreadingUtils.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.util.ThrowableComputable import com.intellij.util.ExceptionUtil import com.intellij.util.concurrency.AppExecutorUtil import com.intellij.util.concurrency.Semaphore +import io.opentelemetry.context.Context import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver import java.time.Duration import java.util.concurrent.Future @@ -81,8 +82,9 @@ fun pluginAwareExecuteOnPooledThread(action: () -> T): Future { * worker thread will not contain original call stack. Necessary for telemetry. */ val pluginResolver = PluginResolver.fromCurrentThread() + val context = Context.current() return ApplicationManager.getApplication().executeOnPooledThread { PluginResolver.setThreadLocal(pluginResolver) - action() + context.wrap(action).call() } } From 0769a77ed51c1dbf7eec48db06b0ee71c9f6f082 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Mon, 21 Oct 2024 13:52:14 -0700 Subject: [PATCH 02/20] lint --- .../services/telemetry/otel/OTelService.kt | 14 +++++++++++--- .../jetbrains/services/telemetry/otel/OtelBase.kt | 5 ++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt index 696e73acb86..9bae4e75c81 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt @@ -1,5 +1,6 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +@file:Suppress("UnusedPrivateClass") package software.aws.toolkits.jetbrains.services.telemetry.otel @@ -31,7 +32,10 @@ import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner import java.io.ByteArrayOutputStream import java.net.ConnectException -private class BasicOtlpSpanProcessor(private val coroutineScope: CoroutineScope, private val traceUrl: String = "http://127.0.0.1:4318/v1/traces") : SpanProcessor { +private class BasicOtlpSpanProcessor( + private val coroutineScope: CoroutineScope, + private val traceUrl: String = "http://127.0.0.1:4318/v1/traces", +) : SpanProcessor { override fun onStart(parentContext: Context, span: ReadWriteSpan) {} override fun isStartRequired() = false override fun isEndRequired() = true @@ -56,7 +60,11 @@ private class BasicOtlpSpanProcessor(private val coroutineScope: CoroutineScope, } } -private class SigV4OtlpSpanProcessor(private val coroutineScope: CoroutineScope, private val traceUrl: String, private val creds: AwsCredentialsProvider) : SpanProcessor { +private class SigV4OtlpSpanProcessor( + private val coroutineScope: CoroutineScope, + private val traceUrl: String, + private val creds: AwsCredentialsProvider, +) : SpanProcessor { override fun onStart(parentContext: Context, span: ReadWriteSpan) {} override fun isStartRequired() = false override fun isEndRequired() = true @@ -117,7 +125,7 @@ private object StdoutSpanProcessor : SpanProcessor { } @Service -class OTelService(private val cs: CoroutineScope) : Disposable { +class OTelService : Disposable { private val sdkDelegate = lazy { val configurator = OpenTelemetryConfigurator( sdkBuilder = OpenTelemetrySdk.builder(), diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index a72300c3f50..c5d8e02294f 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -32,13 +32,12 @@ abstract class AbstractSpanBuilder, * * @inheritdoc */ - inline fun use(operation: (Span) -> T): T { - return startSpan().useWithoutActiveScope { span -> + inline fun use(operation: (Span) -> T): T = + startSpan().useWithoutActiveScope { span -> (span as Span).makeCurrent().use { operation(span) } } - } protected var parent: Context? = null override fun setParent(context: Context): Builder { From b1ef95f6d578b1496a54410c6d66b92f4e906149 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Tue, 22 Oct 2024 11:39:17 -0700 Subject: [PATCH 03/20] build --- .../services/telemetry/otel/OTelService.kt | 28 +++++++++++++------ .../services/telemetry/otel/OtelBase.kt | 24 ++++++++++------ .../sso/SsoCredentialProviderTest.kt | 8 ++++++ 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt index 9bae4e75c81..052e3ae60f5 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt @@ -6,15 +6,19 @@ package software.aws.toolkits.jetbrains.services.telemetry.otel import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.platform.diagnostic.telemetry.impl.OpenTelemetryConfigurator +import com.intellij.openapi.util.SystemInfoRt import com.intellij.platform.util.http.ContentType import com.intellij.platform.util.http.httpPost +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator import io.opentelemetry.context.Context import io.opentelemetry.context.propagation.ContextPropagators import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.resources.Resource import io.opentelemetry.sdk.trace.ReadWriteSpan import io.opentelemetry.sdk.trace.ReadableSpan import io.opentelemetry.sdk.trace.SdkTracerProvider @@ -127,25 +131,33 @@ private object StdoutSpanProcessor : SpanProcessor { @Service class OTelService : Disposable { private val sdkDelegate = lazy { - val configurator = OpenTelemetryConfigurator( - sdkBuilder = OpenTelemetrySdk.builder(), - ) - - configurator.getConfiguredSdkBuilder() + OpenTelemetrySdk.builder() .setTracerProvider( SdkTracerProvider.builder() .addSpanProcessor(StdoutSpanProcessor) - .setResource(configurator.resource) + .setResource( + Resource.create( + Attributes.builder() + .put(AttributeKey.stringKey("os.type"), SystemInfoRt.OS_NAME) + .put(AttributeKey.stringKey("os.version"), SystemInfoRt.OS_VERSION) + .put(AttributeKey.stringKey("host.arch"), System.getProperty("os.arch")) + .build() + ) + ) .build() ) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .build() } - val sdk: OpenTelemetrySdk by sdkDelegate + internal val sdk: OpenTelemetrySdk by sdkDelegate override fun dispose() { if (sdkDelegate.isInitialized()) { sdk.close() } } + + companion object { + fun getSdk() = service().sdk + } } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index c5d8e02294f..3baa44a8fc5 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -3,7 +3,7 @@ package software.aws.toolkits.jetbrains.services.telemetry.otel -import com.intellij.platform.diagnostic.telemetry.helpers.useWithoutActiveScope +import com.intellij.platform.diagnostic.telemetry.helpers.use as ijUse import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.Span @@ -17,7 +17,6 @@ import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver import java.time.Instant import java.util.concurrent.TimeUnit -import kotlin.use val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named("pluginDescriptor") private val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") @@ -33,10 +32,8 @@ abstract class AbstractSpanBuilder, * @inheritdoc */ inline fun use(operation: (Span) -> T): T = - startSpan().useWithoutActiveScope { span -> - (span as Span).makeCurrent().use { - operation(span) - } + startSpan().ijUse { span -> + operation(span as Span) } protected var parent: Context? = null @@ -124,9 +121,10 @@ abstract class AbstractSpanBuilder, val contextValue = parent.get(AWS_PRODUCT_CONTEXT_KEY) if (contextValue == null) { - val s = Span.fromContextOrNull(parent) + // FIX_WHEN_MIN_IS_243: Kotlin compiler can't figure out difference between class/type parameter until 2.x + val s = io.opentelemetry.api.trace.Span.fromContextOrNull(parent) if (s is AbstractBaseSpan) { - setParent(s.context.with(Span.fromContext(parent))) + setParent(s.context.with(io.opentelemetry.api.trace.Span.fromContext(parent))) } else { setParent(parent.with(AWS_PRODUCT_CONTEXT_KEY, resolvePluginName())) } @@ -144,6 +142,16 @@ abstract class AbstractSpanBuilder, } abstract class AbstractBaseSpan(internal val context: Context, private val delegate: Span) : Span by delegate { + /** + * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] + * + * @inheritdoc + */ + inline fun use(operation: (Span) -> T): T = + ijUse { span -> + operation(span as Span) + } + fun metadata(key: String, value: String) = setAttribute(key, value) override fun makeCurrent(): Scope = diff --git a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt index 5463fd33663..6e2b5e124ba 100644 --- a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt +++ b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.core.credentials.sso +import com.intellij.openapi.application.ApplicationManager import com.intellij.testFramework.ApplicationRule import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -95,6 +96,13 @@ class SsoCredentialProviderTest { verify(ssoAccessTokenProvider).invalidate() } + @Test + fun aaaaa() { + ApplicationManager.getApplication().executeOnPooledThread { + println(Thread.currentThread().getContextClassLoader()) + }.get() + } + private fun createSsoResponse(expirationTime: Instant) { ssoClient.stub { on( From 0a4e9cab73fd89533e82ee48a44cfa42f9e5b040 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Tue, 22 Oct 2024 15:02:38 -0700 Subject: [PATCH 04/20] tst --- .../services/telemetry/otel/OTelService.kt | 12 +- .../services/telemetry/otel/OtelBase.kt | 51 ++- .../services/telemetry/otel/OtelBaseTest.kt | 323 ++++++++++++++++++ 3 files changed, 371 insertions(+), 15 deletions(-) create mode 100644 plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt index 052e3ae60f5..33dc4fa6e02 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt @@ -11,6 +11,7 @@ import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.util.SystemInfoRt import com.intellij.platform.util.http.ContentType import com.intellij.platform.util.http.httpPost +import com.intellij.serviceContainer.NonInjectable import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator @@ -129,12 +130,19 @@ private object StdoutSpanProcessor : SpanProcessor { } @Service -class OTelService : Disposable { +class OTelService @NonInjectable internal constructor(spanProcessors: List): Disposable { + @Suppress("unused") + constructor() : this(listOf(StdoutSpanProcessor)) + private val sdkDelegate = lazy { OpenTelemetrySdk.builder() .setTracerProvider( SdkTracerProvider.builder() - .addSpanProcessor(StdoutSpanProcessor) + .apply { + spanProcessors.forEach { + addSpanProcessor(it) + } + } .setResource( Resource.create( Attributes.builder() diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 3baa44a8fc5..9e5431dceb3 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -4,6 +4,7 @@ package software.aws.toolkits.jetbrains.services.telemetry.otel import com.intellij.platform.diagnostic.telemetry.helpers.use as ijUse +import com.intellij.platform.diagnostic.telemetry.helpers.useWithScope as ijUseWithScope import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.Span @@ -13,16 +14,21 @@ import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.context.Context import io.opentelemetry.context.ContextKey import io.opentelemetry.context.Scope +import kotlinx.coroutines.CoroutineScope import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver import java.time.Instant import java.util.concurrent.TimeUnit +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named("pluginDescriptor") -private val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") +internal val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder(delegate) { - override fun doStartSpan() = BaseSpan(parent!!, delegate.startSpan()) + override fun doStartSpan() = BaseSpan(parent, delegate.startSpan()) } abstract class AbstractSpanBuilder, Span : AbstractBaseSpan>(protected val delegate: SpanBuilder) : SpanBuilder { @@ -36,6 +42,19 @@ abstract class AbstractSpanBuilder, operation(span as Span) } + /** + * Same as [com.intellij.platform.diagnostic.telemetry.helpers.useWithScope] except downcasts to specific subclass of [BaseSpan] + * + * @inheritdoc + */ + suspend inline fun useWithScope( + context: CoroutineContext = EmptyCoroutineContext, + crossinline operation: suspend CoroutineScope.(Span) -> T + ): T = + ijUseWithScope(context) { span -> + operation(span as Span) + } + protected var parent: Context? = null override fun setParent(context: Context): Builder { parent = context @@ -123,25 +142,31 @@ abstract class AbstractSpanBuilder, if (contextValue == null) { // FIX_WHEN_MIN_IS_243: Kotlin compiler can't figure out difference between class/type parameter until 2.x val s = io.opentelemetry.api.trace.Span.fromContextOrNull(parent) - if (s is AbstractBaseSpan) { - setParent(s.context.with(io.opentelemetry.api.trace.Span.fromContext(parent))) + parent = if (s is AbstractBaseSpan && s.context != null) { + s.context.with(io.opentelemetry.api.trace.Span.fromContext(parent)) } else { - setParent(parent.with(AWS_PRODUCT_CONTEXT_KEY, resolvePluginName())) + parent.with(AWS_PRODUCT_CONTEXT_KEY, resolvePluginName()) } + setParent(parent) } + requireNotNull(parent) - setAttribute( - PLUGIN_ATTRIBUTE_KEY, - (parent.get(AWS_PRODUCT_CONTEXT_KEY) ?: resolvePluginName()).name - ) + parent.get(AWS_PRODUCT_CONTEXT_KEY)?.toString()?.let { + setAttribute(PLUGIN_ATTRIBUTE_KEY, it) + } ?: run { + LOG.warn { "Reached setAttribute with null AWS_PRODUCT_CONTEXT_KEY, but should not be possible" } + } return doStartSpan() } - private fun resolvePluginName() = PluginResolver.Companion.fromStackTrace(Thread.currentThread().stackTrace).product + private companion object { + val LOG = getLogger>() + fun resolvePluginName() = PluginResolver.Companion.fromStackTrace(Thread.currentThread().stackTrace).product + } } -abstract class AbstractBaseSpan(internal val context: Context, private val delegate: Span) : Span by delegate { +abstract class AbstractBaseSpan(internal val context: Context?, private val delegate: Span) : Span by delegate { /** * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] * @@ -155,12 +180,12 @@ abstract class AbstractBaseSpan(internal val context: Context, private val deleg fun metadata(key: String, value: String) = setAttribute(key, value) override fun makeCurrent(): Scope = - context.with(this).makeCurrent() + context?.with(this)?.makeCurrent() ?: super.makeCurrent() } /** * Placeholder; will be generated */ -class BaseSpan(context: Context, delegate: Span) : AbstractBaseSpan(context, delegate) { +class BaseSpan(context: Context?, delegate: Span) : AbstractBaseSpan(context, delegate) { fun reason(reason: String) = metadata("reason", reason) } diff --git a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt new file mode 100644 index 00000000000..cf178807a6d --- /dev/null +++ b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt @@ -0,0 +1,323 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.telemetry.otel + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.testFramework.ApplicationExtension +import io.opentelemetry.api.trace.TraceId +import io.opentelemetry.context.Context +import io.opentelemetry.extension.kotlin.asContextElement +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.trace.ReadWriteSpan +import io.opentelemetry.sdk.trace.ReadableSpan +import io.opentelemetry.sdk.trace.SpanProcessor +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.RegisterExtension +import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.warn +import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext +import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread +import software.aws.toolkits.jetbrains.utils.satisfiesKt +import software.aws.toolkits.jetbrains.utils.spinUntil +import java.util.concurrent.TimeUnit + +@ExtendWith(ApplicationExtension::class) +class OtelBaseTest { + private companion object { + @RegisterExtension + val otelExtension = OtelExtension() + } + + @Test + fun `context propagates from parent to child - happy case`() { + spanBuilder("tracer", "parentSpan").use { + spanBuilder("anotherTracer", "childSpan").use {} + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context propagates from parent to child - happy case coroutines`() = runTest { + spanBuilder("tracer", "parentSpan").useWithScope { + spanBuilder("anotherTracer", "childSpan").useWithScope {} + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context propagates from parent to child - with context override`() { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { + spanBuilder("anotherTracer", "childSpan").use {} + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context propagates from parent to child when child overrides context`() { + spanBuilder("tracer", "parentSpan").use { + // parent->child relationship is still maintained because Context.current() will return parent context + spanBuilder("anotherTracer", "childSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use {} + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isNotEqualTo(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + } + } + + @Test + fun `context override does not propagate from parent to child when switching threads`() { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { + ApplicationManager.getApplication().executeOnPooledThread { + spanBuilder("anotherTracer", "childSpan").use {} + }.get(10, TimeUnit.SECONDS) + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isNotEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context override propagates from parent to child when switching threads while preserving thread-local`() { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { + pluginAwareExecuteOnPooledThread { + spanBuilder("anotherTracer", "childSpan").use {} + }.get(10, TimeUnit.SECONDS) + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context override propagates from parent to child when only child is coroutine`() { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { + runTest { + spanBuilder("anotherTracer", "childSpan").useWithScope { + delay(10000) + } + } + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context override propagates from parent to child when only parent is coroutine`() = runTest { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).useWithScope { + spanBuilder("anotherTracer", "childSpan").use {} + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context override propagates from parent to child when both are coroutines`() = runTest { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).useWithScope { + spanBuilder("anotherTracer", "childSpan").useWithScope {} + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context override does not propagate from parent to child coroutines if context is not preserved`() { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { + runBlocking(getCoroutineBgContext()) { + spanBuilder("anotherTracer", "childSpan").use {} + } + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isNotEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context override propagates from parent to child coroutines with manual coroutine context propagation`() { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { + runBlocking(getCoroutineBgContext() + Context.current().asContextElement()) { + spanBuilder("anotherTracer", "childSpan").use {} + } + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.first() + val child = spans.last() + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + @Test + fun `context propagates from parent to child when child#end is after parent#end`() { + spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { + pluginAwareExecuteOnPooledThread { + spanBuilder("anotherTracer", "childSpan").use { + Thread.sleep(100) + } + } + } + spinUntil(java.time.Duration.ofSeconds(10)) { + otelExtension.completedSpans.size == 2 + } + + assertThat(otelExtension.completedSpans).hasSize(2).satisfiesKt { spans -> + val parent = spans.last() + val child = spans.first() + + assertThat(parent.hasEnded()) + assertThat(child.hasEnded()) + + // child started after parent + assertThat(child.toSpanData().startEpochNanos).isGreaterThanOrEqualTo(parent.toSpanData().startEpochNanos) + // and called end after parent + assertThat(child.toSpanData().endEpochNanos).isGreaterThanOrEqualTo(parent.toSpanData().endEpochNanos) + + assertThat(parent.parentSpanContext.traceId).isEqualTo(TraceId.getInvalid()) + assertThat(child.parentSpanContext.traceId).isEqualTo(parent.spanContext.traceId) + assertThat(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo("Amazon Q For VS Code") + assertThat(child.getAttribute(PLUGIN_ATTRIBUTE_KEY)).isEqualTo(parent.getAttribute(PLUGIN_ATTRIBUTE_KEY)) + } + } + + private fun spanBuilder(tracer: String, spanName: String) = DefaultSpanBuilder(otelExtension.sdk.sdk.getTracer(tracer).spanBuilder(spanName)) +} + +class OtelExtension : AfterEachCallback, AfterAllCallback { + private val openSpans = mutableSetOf() + private val _completedSpans = mutableListOf() + val completedSpans + get() = _completedSpans.reversed().toList() + + val sdk = OTelService(listOf( + // should probably be a service loader + object : SpanProcessor { + override fun isStartRequired() = true + override fun isEndRequired() = true + + override fun onStart(parentContext: Context, span: ReadWriteSpan) { + openSpans.add(span) + } + + override fun onEnd(span: ReadableSpan) { + println(span) + _completedSpans.add(span) + + if (!openSpans.contains(span)) { + LOG.warn(RuntimeException("Span ended without corresponding start")) { span.toString() } + } + openSpans.remove(span) + } + + override fun forceFlush(): CompletableResultCode { + assert(openSpans.isEmpty()) { "Not all open spans were closed: ${openSpans.joinToString(", ")}" } + return CompletableResultCode.ofSuccess() + } + } + )) + + override fun afterEach(context: ExtensionContext?) { + reset() + } + + override fun afterAll(context: ExtensionContext?) { + sdk.sdk.shutdown() + } + + fun reset() { + openSpans.clear() + _completedSpans.clear() + } + + companion object { + private val LOG = getLogger() + } +} From c34c51c10d5c45a2631f5d65c89cc52f7778a1fc Mon Sep 17 00:00:00 2001 From: Richard Li Date: Tue, 22 Oct 2024 15:03:18 -0700 Subject: [PATCH 05/20] lint --- .../services/telemetry/otel/OTelService.kt | 2 +- .../services/telemetry/otel/OtelBase.kt | 6 +-- .../services/telemetry/otel/OtelBaseTest.kt | 48 ++++++++++--------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt index 33dc4fa6e02..db3e66c976a 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OTelService.kt @@ -130,7 +130,7 @@ private object StdoutSpanProcessor : SpanProcessor { } @Service -class OTelService @NonInjectable internal constructor(spanProcessors: List): Disposable { +class OTelService @NonInjectable internal constructor(spanProcessors: List) : Disposable { @Suppress("unused") constructor() : this(listOf(StdoutSpanProcessor)) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 9e5431dceb3..0df639490ee 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -3,8 +3,6 @@ package software.aws.toolkits.jetbrains.services.telemetry.otel -import com.intellij.platform.diagnostic.telemetry.helpers.use as ijUse -import com.intellij.platform.diagnostic.telemetry.helpers.useWithScope as ijUseWithScope import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.Span @@ -23,6 +21,8 @@ import java.time.Instant import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import com.intellij.platform.diagnostic.telemetry.helpers.use as ijUse +import com.intellij.platform.diagnostic.telemetry.helpers.useWithScope as ijUseWithScope val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named("pluginDescriptor") internal val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") @@ -49,7 +49,7 @@ abstract class AbstractSpanBuilder, */ suspend inline fun useWithScope( context: CoroutineContext = EmptyCoroutineContext, - crossinline operation: suspend CoroutineScope.(Span) -> T + crossinline operation: suspend CoroutineScope.(Span) -> T, ): T = ijUseWithScope(context) { span -> operation(span as Span) diff --git a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt index cf178807a6d..4c0d3e827a8 100644 --- a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt +++ b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt @@ -144,7 +144,7 @@ class OtelBaseTest { } @Test - fun `context override propagates from parent to child when only child is coroutine`() { + fun `context override propagates from parent to child when only child is coroutine`() { spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { runTest { spanBuilder("anotherTracer", "childSpan").useWithScope { @@ -199,7 +199,7 @@ class OtelBaseTest { } @Test - fun `context override does not propagate from parent to child coroutines if context is not preserved`() { + fun `context override does not propagate from parent to child coroutines if context is not preserved`() { spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { runBlocking(getCoroutineBgContext()) { spanBuilder("anotherTracer", "childSpan").use {} @@ -218,7 +218,7 @@ class OtelBaseTest { } @Test - fun `context override propagates from parent to child coroutines with manual coroutine context propagation`() { + fun `context override propagates from parent to child coroutines with manual coroutine context propagation`() { spanBuilder("tracer", "parentSpan").setParent(Context.current().with(AWS_PRODUCT_CONTEXT_KEY, AWSProduct.AMAZON_Q_FOR_VS_CODE)).use { runBlocking(getCoroutineBgContext() + Context.current().asContextElement()) { spanBuilder("anotherTracer", "childSpan").use {} @@ -277,32 +277,34 @@ class OtelExtension : AfterEachCallback, AfterAllCallback { val completedSpans get() = _completedSpans.reversed().toList() - val sdk = OTelService(listOf( - // should probably be a service loader - object : SpanProcessor { - override fun isStartRequired() = true - override fun isEndRequired() = true + val sdk = OTelService( + listOf( + // should probably be a service loader + object : SpanProcessor { + override fun isStartRequired() = true + override fun isEndRequired() = true - override fun onStart(parentContext: Context, span: ReadWriteSpan) { - openSpans.add(span) - } + override fun onStart(parentContext: Context, span: ReadWriteSpan) { + openSpans.add(span) + } - override fun onEnd(span: ReadableSpan) { - println(span) - _completedSpans.add(span) + override fun onEnd(span: ReadableSpan) { + println(span) + _completedSpans.add(span) - if (!openSpans.contains(span)) { - LOG.warn(RuntimeException("Span ended without corresponding start")) { span.toString() } + if (!openSpans.contains(span)) { + LOG.warn(RuntimeException("Span ended without corresponding start")) { span.toString() } + } + openSpans.remove(span) } - openSpans.remove(span) - } - override fun forceFlush(): CompletableResultCode { - assert(openSpans.isEmpty()) { "Not all open spans were closed: ${openSpans.joinToString(", ")}" } - return CompletableResultCode.ofSuccess() + override fun forceFlush(): CompletableResultCode { + assert(openSpans.isEmpty()) { "Not all open spans were closed: ${openSpans.joinToString(", ")}" } + return CompletableResultCode.ofSuccess() + } } - } - )) + ) + ) override fun afterEach(context: ExtensionContext?) { reset() From ddf6db919231eed11fbf28f917da0e30258cff35 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Wed, 23 Oct 2024 17:11:08 -0700 Subject: [PATCH 06/20] hack in 233 --- .../helpers/useWithoutActiveScope.kt | 30 +++++++ .../services/telemetry/otel/OtelBase.kt | 82 +++++++++++-------- 2 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/useWithoutActiveScope.kt diff --git a/plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/useWithoutActiveScope.kt b/plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/useWithoutActiveScope.kt new file mode 100644 index 00000000000..074c08a1428 --- /dev/null +++ b/plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/useWithoutActiveScope.kt @@ -0,0 +1,30 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.intellij.platform.diagnostic.telemetry.helpers + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.StatusCode +import kotlin.coroutines.cancellation.CancellationException + +val EXCEPTION_ESCAPED = AttributeKey.booleanKey("exception.escaped") + +inline fun Span.useWithoutActiveScope(operation: (Span) -> T): T { + try { + return operation(this) + } + catch (e: CancellationException) { + recordException(e, Attributes.of(EXCEPTION_ESCAPED, true)) + throw e + } + catch (e: Throwable) { + recordException(e, Attributes.of(EXCEPTION_ESCAPED, true)) + setStatus(StatusCode.ERROR) + throw e + } + finally { + end() + } +} diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 0df639490ee..3978b6dc1a3 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -3,6 +3,8 @@ package software.aws.toolkits.jetbrains.services.telemetry.otel +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.platform.diagnostic.telemetry.helpers.useWithoutActiveScope import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.trace.Span @@ -31,15 +33,24 @@ class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder, Span : AbstractBaseSpan>(protected val delegate: SpanBuilder) : SpanBuilder { +abstract class AbstractSpanBuilder, SpanType : AbstractBaseSpan>(protected val delegate: SpanBuilder) : SpanBuilder { /** * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] * * @inheritdoc */ inline fun use(operation: (Span) -> T): T = - startSpan().ijUse { span -> - operation(span as Span) + // FIX_WHEN_MIN_IS_241: not worth fixing for 233 + if (ApplicationInfo.getInstance().build.baselineVersion == 233) { + startSpan().useWithoutActiveScope { span -> + span.makeCurrent().use { + operation(span as SpanType) + } + } + } else { + startSpan().ijUse { span -> + operation(span as SpanType) + } } /** @@ -49,89 +60,89 @@ abstract class AbstractSpanBuilder, */ suspend inline fun useWithScope( context: CoroutineContext = EmptyCoroutineContext, - crossinline operation: suspend CoroutineScope.(Span) -> T, + crossinline operation: suspend CoroutineScope.(SpanType) -> T, ): T = ijUseWithScope(context) { span -> - operation(span as Span) + operation(span as SpanType) } protected var parent: Context? = null - override fun setParent(context: Context): Builder { + override fun setParent(context: Context): BuilderType { parent = context delegate.setParent(context) - return this as Builder + return this as BuilderType } - override fun setNoParent(): Builder { + override fun setNoParent(): BuilderType { parent = null delegate.setNoParent() - return this as Builder + return this as BuilderType } - override fun addLink(spanContext: SpanContext): Builder { + override fun addLink(spanContext: SpanContext): BuilderType { delegate.addLink(spanContext) - return this as Builder + return this as BuilderType } override fun addLink( spanContext: SpanContext, attributes: Attributes, - ): Builder { + ): BuilderType { delegate.addLink(spanContext, attributes) - return this as Builder + return this as BuilderType } - override fun setAttribute(key: String, value: String): Builder { + override fun setAttribute(key: String, value: String): BuilderType { delegate.setAttribute(key, value) - return this as Builder + return this as BuilderType } - override fun setAttribute(key: String, value: Long): Builder { + override fun setAttribute(key: String, value: Long): BuilderType { delegate.setAttribute(key, value) - return this as Builder + return this as BuilderType } - override fun setAttribute(key: String, value: Double): Builder { + override fun setAttribute(key: String, value: Double): BuilderType { delegate.setAttribute(key, value) - return this as Builder + return this as BuilderType } - override fun setAttribute(key: String, value: Boolean): Builder { + override fun setAttribute(key: String, value: Boolean): BuilderType { delegate.setAttribute(key, value) - return this as Builder + return this as BuilderType } override fun setAttribute( key: AttributeKey, value: V & Any, - ): Builder { + ): BuilderType { delegate.setAttribute(key, value) - return this as Builder + return this as BuilderType } - override fun setAllAttributes(attributes: Attributes): Builder { + override fun setAllAttributes(attributes: Attributes): BuilderType { delegate.setAllAttributes(attributes) - return this as Builder + return this as BuilderType } - override fun setSpanKind(spanKind: SpanKind): Builder { + override fun setSpanKind(spanKind: SpanKind): BuilderType { delegate.setSpanKind(spanKind) - return this as Builder + return this as BuilderType } - override fun setStartTimestamp(startTimestamp: Long, unit: TimeUnit): Builder { + override fun setStartTimestamp(startTimestamp: Long, unit: TimeUnit): BuilderType { delegate.setStartTimestamp(startTimestamp, unit) - return this as Builder + return this as BuilderType } - override fun setStartTimestamp(startTimestamp: Instant): Builder { + override fun setStartTimestamp(startTimestamp: Instant): BuilderType { delegate.setStartTimestamp(startTimestamp) - return this as Builder + return this as BuilderType } - protected abstract fun doStartSpan(): Span + protected abstract fun doStartSpan(): SpanType - override fun startSpan(): Span { + override fun startSpan(): SpanType { var parent = parent if (parent == null) { parent = Context.current() @@ -140,10 +151,9 @@ abstract class AbstractSpanBuilder, val contextValue = parent.get(AWS_PRODUCT_CONTEXT_KEY) if (contextValue == null) { - // FIX_WHEN_MIN_IS_243: Kotlin compiler can't figure out difference between class/type parameter until 2.x - val s = io.opentelemetry.api.trace.Span.fromContextOrNull(parent) + val s = Span.fromContextOrNull(parent) parent = if (s is AbstractBaseSpan && s.context != null) { - s.context.with(io.opentelemetry.api.trace.Span.fromContext(parent)) + s.context.with(Span.fromContext(parent)) } else { parent.with(AWS_PRODUCT_CONTEXT_KEY, resolvePluginName()) } From 7d9079630194e6596d20946f6526f467eef1a89a Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 24 Oct 2024 13:32:45 -0700 Subject: [PATCH 07/20] lint --- ...useWithoutActiveScope.kt => UseWithoutActiveScope.kt} | 9 +++------ .../jetbrains/services/telemetry/otel/OtelBase.kt | 7 ++++++- 2 files changed, 9 insertions(+), 7 deletions(-) rename plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/{useWithoutActiveScope.kt => UseWithoutActiveScope.kt} (89%) diff --git a/plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/useWithoutActiveScope.kt b/plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/UseWithoutActiveScope.kt similarity index 89% rename from plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/useWithoutActiveScope.kt rename to plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/UseWithoutActiveScope.kt index 074c08a1428..688e054bfa2 100644 --- a/plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/useWithoutActiveScope.kt +++ b/plugins/core/jetbrains-community/src-233/com/intellij/platform/diagnostic/telemetry/helpers/UseWithoutActiveScope.kt @@ -14,17 +14,14 @@ val EXCEPTION_ESCAPED = AttributeKey.booleanKey("exception.escaped") inline fun Span.useWithoutActiveScope(operation: (Span) -> T): T { try { return operation(this) - } - catch (e: CancellationException) { + } catch (e: CancellationException) { recordException(e, Attributes.of(EXCEPTION_ESCAPED, true)) throw e - } - catch (e: Throwable) { + } catch (e: Throwable) { recordException(e, Attributes.of(EXCEPTION_ESCAPED, true)) setStatus(StatusCode.ERROR) throw e - } - finally { + } finally { end() } } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 3978b6dc1a3..5e19a5a9d3d 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -33,7 +33,12 @@ class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder, SpanType : AbstractBaseSpan>(protected val delegate: SpanBuilder) : SpanBuilder { +abstract class AbstractSpanBuilder< + BuilderType : AbstractSpanBuilder, + SpanType : AbstractBaseSpan, + >( + protected val delegate: SpanBuilder, +) : SpanBuilder { /** * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] * From f5e4c3149b4db7638b5ca58681789811877e31f2 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 25 Oct 2024 10:30:10 -0700 Subject: [PATCH 08/20] wip --- .../services/telemetry/otel/OtelBase.kt | 29 +++++++++---------- settings.gradle.kts | 2 ++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 5e19a5a9d3d..e79a49d7999 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -19,6 +19,7 @@ import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver +//import software.aws.toolkits.telemetry.BaseSpan import java.time.Instant import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -29,13 +30,16 @@ import com.intellij.platform.diagnostic.telemetry.helpers.useWithScope as ijUseW val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named("pluginDescriptor") internal val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") -class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder(delegate) { - override fun doStartSpan() = BaseSpan(parent, delegate.startSpan()) -} +//class DefaultSpan(context: Context?, delegate: Span) : BaseSpan(context, delegate) { +// override val metricName = "?????????" +//} +//class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder(delegate) { +// override fun doStartSpan() = DefaultSpan(parent, delegate.startSpan()) +//} abstract class AbstractSpanBuilder< BuilderType : AbstractSpanBuilder, - SpanType : AbstractBaseSpan, + SpanType : AbstractBaseSpan, >( protected val delegate: SpanBuilder, ) : SpanBuilder { @@ -157,7 +161,7 @@ abstract class AbstractSpanBuilder< val contextValue = parent.get(AWS_PRODUCT_CONTEXT_KEY) if (contextValue == null) { val s = Span.fromContextOrNull(parent) - parent = if (s is AbstractBaseSpan && s.context != null) { + parent = if (s is AbstractBaseSpan<*> && s.context != null) { s.context.with(Span.fromContext(parent)) } else { parent.with(AWS_PRODUCT_CONTEXT_KEY, resolvePluginName()) @@ -181,7 +185,9 @@ abstract class AbstractSpanBuilder< } } -abstract class AbstractBaseSpan(internal val context: Context?, private val delegate: Span) : Span by delegate { +abstract class AbstractBaseSpan>(internal val context: Context?, private val delegate: Span) : Span by delegate { + protected abstract val metricName: String + /** * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] * @@ -189,18 +195,11 @@ abstract class AbstractBaseSpan(internal val context: Context?, private val dele */ inline fun use(operation: (Span) -> T): T = ijUse { span -> - operation(span as Span) + operation(span as SpanType) } - fun metadata(key: String, value: String) = setAttribute(key, value) + fun metadata(key: String, value: String) = setAttribute(key, value) as SpanType override fun makeCurrent(): Scope = context?.with(this)?.makeCurrent() ?: super.makeCurrent() } - -/** - * Placeholder; will be generated - */ -class BaseSpan(context: Context?, delegate: Span) : AbstractBaseSpan(context, delegate) { - fun reason(reason: String) = metadata("reason", reason) -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 21396263a70..ef6cf582224 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -164,3 +164,5 @@ file("plugins").listFiles()?.forEach root@ { project(projectName).projectDir = it } } + +includeBuild("../aws-toolkit-common/telemetry/jetbrains") From eeb5b783a1d7de6be10c3d431c72bf8274dc52b8 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Mon, 28 Oct 2024 13:39:07 -0700 Subject: [PATCH 09/20] cleanup --- .../core/jetbrains-community/build.gradle.kts | 11 +++- .../services/telemetry/otel/OtelBase.kt | 53 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/plugins/core/jetbrains-community/build.gradle.kts b/plugins/core/jetbrains-community/build.gradle.kts index 491a82f4166..c6a3128e8e2 100644 --- a/plugins/core/jetbrains-community/build.gradle.kts +++ b/plugins/core/jetbrains-community/build.gradle.kts @@ -18,15 +18,22 @@ buildscript { } } +private val generatedSrcDir = project.layout.buildDirectory.dir("generated-src") sourceSets { main { - java.srcDir(project.layout.buildDirectory.dir("generated-src")) + java.srcDir(generatedSrcDir) + } +} + +idea { + module { + generatedSourceDirs = generatedSourceDirs.toMutableSet() + generatedSrcDir.get().asFile } } val generateTelemetry = tasks.register("generateTelemetry") { inputFiles = listOf(file("${project.projectDir}/resources/telemetryOverride.json")) - outputDirectory = project.layout.buildDirectory.dir("generated-src").get().asFile + outputDirectory = generatedSrcDir.get().asFile doFirst { outputDirectory.deleteRecursively() diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index e79a49d7999..1c92eadb1ec 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -4,6 +4,7 @@ package software.aws.toolkits.jetbrains.services.telemetry.otel import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.application.ApplicationManager import com.intellij.platform.diagnostic.telemetry.helpers.useWithoutActiveScope import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes @@ -14,12 +15,14 @@ import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.context.Context import io.opentelemetry.context.ContextKey import io.opentelemetry.context.Scope +import io.opentelemetry.sdk.trace.ReadWriteSpan import kotlinx.coroutines.CoroutineScope import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct +import software.aws.toolkits.core.utils.error import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn +import software.aws.toolkits.jetbrains.isDeveloperMode import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver -//import software.aws.toolkits.telemetry.BaseSpan import java.time.Instant import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -30,13 +33,6 @@ import com.intellij.platform.diagnostic.telemetry.helpers.useWithScope as ijUseW val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named("pluginDescriptor") internal val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") -//class DefaultSpan(context: Context?, delegate: Span) : BaseSpan(context, delegate) { -// override val metricName = "?????????" -//} -//class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder(delegate) { -// override fun doStartSpan() = DefaultSpan(parent, delegate.startSpan()) -//} - abstract class AbstractSpanBuilder< BuilderType : AbstractSpanBuilder, SpanType : AbstractBaseSpan, @@ -48,7 +44,7 @@ abstract class AbstractSpanBuilder< * * @inheritdoc */ - inline fun use(operation: (Span) -> T): T = + inline fun use(operation: (SpanType) -> T): T = // FIX_WHEN_MIN_IS_241: not worth fixing for 233 if (ApplicationInfo.getInstance().build.baselineVersion == 233) { startSpan().useWithoutActiveScope { span -> @@ -185,21 +181,50 @@ abstract class AbstractSpanBuilder< } } -abstract class AbstractBaseSpan>(internal val context: Context?, private val delegate: Span) : Span by delegate { - protected abstract val metricName: String - +abstract class AbstractBaseSpan>(internal val context: Context?, private val delegate: ReadWriteSpan) : Span by delegate { + protected open val requiredFields: Collection = emptySet() /** * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] * * @inheritdoc */ - inline fun use(operation: (Span) -> T): T = + inline fun use(operation: (SpanType) -> T): T = ijUse { span -> operation(span as SpanType) } - fun metadata(key: String, value: String) = setAttribute(key, value) as SpanType + fun metadata(key: String, value: String?): SpanType { + delegate.setAttribute(key, value) + return this as SpanType + } + + override fun end() { + validateRequiredAttributes() + delegate.end() + } + + override fun end(timestamp: Long, unit: TimeUnit) { + validateRequiredAttributes() + delegate.end() + } + + private fun validateRequiredAttributes() { + val missingFields = requiredFields.filter { delegate.getAttribute(AttributeKey.stringKey(it)) == null } + val message = { "${delegate.name} is missing required fields: ${missingFields.joinToString(", ")}" } + + if (missingFields.isNotEmpty()) { + when { + ApplicationManager.getApplication().isUnitTestMode -> error(message()) + isDeveloperMode() -> LOG.error(block = message) + else -> LOG.error(block = message) + } + } + } override fun makeCurrent(): Scope = context?.with(this)?.makeCurrent() ?: super.makeCurrent() + + private companion object { + val LOG = getLogger>() + } } From 68ac68f2cad26a48b5d421f6b2a6fb2044bb5173 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Mon, 28 Oct 2024 13:52:05 -0700 Subject: [PATCH 10/20] revert --- settings.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index ef6cf582224..21396263a70 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -164,5 +164,3 @@ file("plugins").listFiles()?.forEach root@ { project(projectName).projectDir = it } } - -includeBuild("../aws-toolkit-common/telemetry/jetbrains") From 37d92e37ed07e7db0f1547083864f918c7fdc12e Mon Sep 17 00:00:00 2001 From: Richard Li Date: Mon, 28 Oct 2024 13:52:27 -0700 Subject: [PATCH 11/20] revert --- .../core/credentials/sso/SsoCredentialProviderTest.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt index 6e2b5e124ba..5463fd33663 100644 --- a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt +++ b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/credentials/sso/SsoCredentialProviderTest.kt @@ -3,7 +3,6 @@ package software.aws.toolkits.jetbrains.core.credentials.sso -import com.intellij.openapi.application.ApplicationManager import com.intellij.testFramework.ApplicationRule import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -96,13 +95,6 @@ class SsoCredentialProviderTest { verify(ssoAccessTokenProvider).invalidate() } - @Test - fun aaaaa() { - ApplicationManager.getApplication().executeOnPooledThread { - println(Thread.currentThread().getContextClassLoader()) - }.get() - } - private fun createSsoResponse(expirationTime: Instant) { ssoClient.stub { on( From 8389744f84b5298fd02565ded30ed47f4402fa9d Mon Sep 17 00:00:00 2001 From: Richard Li Date: Mon, 28 Oct 2024 13:53:31 -0700 Subject: [PATCH 12/20] redundant --- .../aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 1c92eadb1ec..e720534268e 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -177,7 +177,7 @@ abstract class AbstractSpanBuilder< private companion object { val LOG = getLogger>() - fun resolvePluginName() = PluginResolver.Companion.fromStackTrace(Thread.currentThread().stackTrace).product + fun resolvePluginName() = PluginResolver.fromStackTrace(Thread.currentThread().stackTrace).product } } From 332ecb21b74c0d04364330d1a723cf2ce63ba598 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Mon, 28 Oct 2024 16:01:53 -0700 Subject: [PATCH 13/20] build --- .../jetbrains/services/telemetry/otel/OtelBase.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index e720534268e..4ba0030f17b 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -33,6 +33,15 @@ import com.intellij.platform.diagnostic.telemetry.helpers.useWithScope as ijUseW val AWS_PRODUCT_CONTEXT_KEY = ContextKey.named("pluginDescriptor") internal val PLUGIN_ATTRIBUTE_KEY = AttributeKey.stringKey("plugin") +class DefaultSpan(context: Context?, delegate: Span) : BaseSpan(context, delegate) + +class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder(delegate) { + override fun doStartSpan() = DefaultSpan(parent, delegate.startSpan()) +} + +// temporary; will be generated +abstract class BaseSpan>(context: Context?, delegate: Span) : AbstractBaseSpan(context, delegate as ReadWriteSpan) + abstract class AbstractSpanBuilder< BuilderType : AbstractSpanBuilder, SpanType : AbstractBaseSpan, @@ -183,6 +192,7 @@ abstract class AbstractSpanBuilder< abstract class AbstractBaseSpan>(internal val context: Context?, private val delegate: ReadWriteSpan) : Span by delegate { protected open val requiredFields: Collection = emptySet() + /** * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] * From 11a950676f2f82431cea503fc7813bf62c89b16f Mon Sep 17 00:00:00 2001 From: Richard Li Date: Tue, 29 Oct 2024 17:02:37 -0700 Subject: [PATCH 14/20] Tweak otelbase classes slightly to meet requirements from generator --- .../toolkits/core/telemetry/MetricEvent.kt | 2 +- .../core/jetbrains-community/build.gradle.kts | 6 +-- .../core/credentials/ToolkitAuthManager.kt | 5 ++- .../services/telemetry/otel/OtelBase.kt | 8 ++-- .../services/telemetry/otel/OtelBaseTest.kt | 39 +++++++++++++++++++ .../telemetry/customization.config | 6 +++ 6 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 plugins/core/sdk-codegen/codegen-resources/telemetry/customization.config diff --git a/plugins/core/core/src/software/aws/toolkits/core/telemetry/MetricEvent.kt b/plugins/core/core/src/software/aws/toolkits/core/telemetry/MetricEvent.kt index cbe4b50a100..4df716d3e14 100644 --- a/plugins/core/core/src/software/aws/toolkits/core/telemetry/MetricEvent.kt +++ b/plugins/core/core/src/software/aws/toolkits/core/telemetry/MetricEvent.kt @@ -4,11 +4,11 @@ package software.aws.toolkits.core.telemetry import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct +import software.amazon.awssdk.services.toolkittelemetry.model.MetricUnit import software.aws.toolkits.core.telemetry.MetricEvent.Companion.illegalCharsRegex import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn import java.time.Instant -import software.amazon.awssdk.services.toolkittelemetry.model.Unit as MetricUnit interface MetricEvent { val createTime: Instant diff --git a/plugins/core/jetbrains-community/build.gradle.kts b/plugins/core/jetbrains-community/build.gradle.kts index c6a3128e8e2..f2868180063 100644 --- a/plugins/core/jetbrains-community/build.gradle.kts +++ b/plugins/core/jetbrains-community/build.gradle.kts @@ -32,11 +32,11 @@ idea { } val generateTelemetry = tasks.register("generateTelemetry") { - inputFiles = listOf(file("${project.projectDir}/resources/telemetryOverride.json")) - outputDirectory = generatedSrcDir.get().asFile + inputFiles.setFrom(file("${project.projectDir}/resources/telemetryOverride.json")) + outputDirectory.set(generatedSrcDir) doFirst { - outputDirectory.deleteRecursively() + outputDirectory.get().asFile.deleteRecursively() } } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitAuthManager.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitAuthManager.kt index ffaffe6f5a8..4dcdf299d79 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitAuthManager.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitAuthManager.kt @@ -10,6 +10,7 @@ import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.Project import migration.software.aws.toolkits.jetbrains.services.telemetry.TelemetryService import software.amazon.awssdk.services.ssooidc.model.SsoOidcException +import software.amazon.awssdk.services.toolkittelemetry.model.MetricUnit import software.aws.toolkits.core.ClientConnectionSettings import software.aws.toolkits.core.ConnectionSettings import software.aws.toolkits.core.TokenConnectionSettings @@ -374,7 +375,7 @@ private fun recordLoginWithBrowser( TelemetryService.getInstance().record(null as Project?) { datum("aws_loginWithBrowser") { createTime(Instant.now()) - unit(software.amazon.awssdk.services.toolkittelemetry.model.Unit.NONE) + unit(MetricUnit.NONE) value(1.0) passive(false) credentialSourceId?.let { metadata("credentialSourceId", it.toString()) } @@ -398,7 +399,7 @@ private fun recordAddConnection( TelemetryService.getInstance().record(null as Project?) { datum("auth_addConnection") { createTime(Instant.now()) - unit(software.amazon.awssdk.services.toolkittelemetry.model.Unit.NONE) + unit(MetricUnit.NONE) value(1.0) passive(false) credentialSourceId?.let { metadata("credentialSourceId", it.toString()) } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 4ba0030f17b..b1151d34a2d 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -18,11 +18,13 @@ import io.opentelemetry.context.Scope import io.opentelemetry.sdk.trace.ReadWriteSpan import kotlinx.coroutines.CoroutineScope import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct +import software.amazon.awssdk.services.toolkittelemetry.model.MetricUnit import software.aws.toolkits.core.utils.error import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.isDeveloperMode import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver +import software.aws.toolkits.telemetry.impl.BaseSpan import java.time.Instant import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -39,9 +41,6 @@ class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder>(context: Context?, delegate: Span) : AbstractBaseSpan(context, delegate as ReadWriteSpan) - abstract class AbstractSpanBuilder< BuilderType : AbstractSpanBuilder, SpanType : AbstractBaseSpan, @@ -192,6 +191,9 @@ abstract class AbstractSpanBuilder< abstract class AbstractBaseSpan>(internal val context: Context?, private val delegate: ReadWriteSpan) : Span by delegate { protected open val requiredFields: Collection = emptySet() + protected var _passive: Boolean = false + protected var _unit: MetricUnit = MetricUnit.NONE + protected var _value: Double = 0.0 /** * Same as [com.intellij.platform.diagnostic.telemetry.helpers.use] except downcasts to specific subclass of [BaseSpan] diff --git a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt index 4c0d3e827a8..add01b95571 100644 --- a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt +++ b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBaseTest.kt @@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.telemetry.otel import com.intellij.openapi.application.ApplicationManager import com.intellij.testFramework.ApplicationExtension +import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.TraceId import io.opentelemetry.context.Context import io.opentelemetry.extension.kotlin.asContextElement @@ -17,11 +18,16 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.AfterAllCallback import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.RegisterExtension +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn @@ -29,13 +35,29 @@ import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread import software.aws.toolkits.jetbrains.utils.satisfiesKt import software.aws.toolkits.jetbrains.utils.spinUntil +import software.aws.toolkits.telemetry.MetricResult +import software.aws.toolkits.telemetry.Telemetry +import java.time.Instant import java.util.concurrent.TimeUnit +import java.util.stream.Stream @ExtendWith(ApplicationExtension::class) class OtelBaseTest { private companion object { @RegisterExtension val otelExtension = OtelExtension() + + private fun spanEndArgs() = Stream.of( + Arguments.of("end()", { it: Span -> it.end() }), + Arguments.of("end(long, TimeUnit)", { it: Span -> it.end(1, TimeUnit.SECONDS) }), + Arguments.of("end(Instant)", { it: Span -> it.end(Instant.now()) }), + ) + + @JvmStatic + fun `AbstractBaseSpan#end() throws if attributes are missing`() = spanEndArgs() + + @JvmStatic + fun `AbstractBaseSpan#end() does not throw if all required attributes are present`() = spanEndArgs() } @Test @@ -268,6 +290,23 @@ class OtelBaseTest { } } + @ParameterizedTest(name = "{0}") + @MethodSource + fun `AbstractBaseSpan#end() throws if attributes are missing`(_name: String, block: Span.() -> Unit) { + val span = Telemetry.aws.openUrl.startSpan() + val e = assertThrows { block(span) } + assertThat(e.message).contains("aws_openUrl is missing required fields: result") + } + + + @ParameterizedTest(name = "{0}") + @MethodSource + fun `AbstractBaseSpan#end() does not throw if all required attributes are present`(_name: String, block: Span.() -> Unit) { + val span = Telemetry.aws.openUrl.startSpan() + span.result(MetricResult.Succeeded) + assertDoesNotThrow { block(span) } + } + private fun spanBuilder(tracer: String, spanName: String) = DefaultSpanBuilder(otelExtension.sdk.sdk.getTracer(tracer).spanBuilder(spanName)) } diff --git a/plugins/core/sdk-codegen/codegen-resources/telemetry/customization.config b/plugins/core/sdk-codegen/codegen-resources/telemetry/customization.config new file mode 100644 index 00000000000..18fb4f1423d --- /dev/null +++ b/plugins/core/sdk-codegen/codegen-resources/telemetry/customization.config @@ -0,0 +1,6 @@ +{ + "renameShapes": { + // "Unit" conflicts with Kotlin stdlib [kotlin.Unit] + "Unit" : "MetricUnit" + } +} From 551b1530bc43fabb1c51ebe258f96022db834323 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 31 Oct 2024 13:46:05 -0700 Subject: [PATCH 15/20] lint --- .../toolkits/jetbrains/services/telemetry/otel/OtelBase.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 4ba0030f17b..b5cf95d4fe3 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -40,7 +40,10 @@ class DefaultSpanBuilder(delegate: SpanBuilder) : AbstractSpanBuilder>(context: Context?, delegate: Span) : AbstractBaseSpan(context, delegate as ReadWriteSpan) +abstract class BaseSpan>( + context: Context?, + delegate: Span, +) : AbstractBaseSpan(context, delegate as ReadWriteSpan) abstract class AbstractSpanBuilder< BuilderType : AbstractSpanBuilder, From 8b307551ee78f491a7436534f0b1d81454a1e4cc Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 31 Oct 2024 14:48:27 -0700 Subject: [PATCH 16/20] Move Q Chat 'TelemetryHelper' metrics to OTel --- .../chat/telemetry/TelemetryHelper.kt | 221 +++++++++--------- .../services/cwc/messages/CwcMessage.kt | 84 ++++--- 2 files changed, 152 insertions(+), 153 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index 635da478306..f9a90a1672e 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -29,13 +29,12 @@ import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.utils.notifyError import software.aws.toolkits.resources.message -import software.aws.toolkits.telemetry.AmazonqTelemetry import software.aws.toolkits.telemetry.CwsprChatCommandType import software.aws.toolkits.telemetry.CwsprChatConversationType import software.aws.toolkits.telemetry.CwsprChatInteractionType import software.aws.toolkits.telemetry.CwsprChatTriggerInteraction import software.aws.toolkits.telemetry.CwsprChatUserIntent -import software.aws.toolkits.telemetry.FeedbackTelemetry +import software.aws.toolkits.telemetry.Telemetry import java.time.Duration import java.time.Instant import software.amazon.awssdk.services.codewhispererruntime.model.UserIntent as CWClientUserIntent @@ -72,55 +71,57 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: // When chat panel is focused fun recordEnterFocusChat() { - AmazonqTelemetry.enterFocusChat(passive = true) + Telemetry.amazonq.enterFocusChat.use { it.passive(true) } + } // When chat panel is unfocused fun recordExitFocusChat() { - AmazonqTelemetry.exitFocusChat(passive = true) + Telemetry.amazonq.exitFocusChat.use { it.passive(true) } } fun recordStartConversation(tabId: String, data: ChatRequestData) { val sessionHistory = sessionStorage.getSession(tabId)?.history ?: return if (sessionHistory.size > 1) return - AmazonqTelemetry.startConversation( - cwsprChatConversationId = getConversationId(tabId).orEmpty(), - cwsprChatTriggerInteraction = getTelemetryTriggerType(data.triggerType), - cwsprChatConversationType = CwsprChatConversationType.Chat, - cwsprChatUserIntent = data.userIntent?.let { getTelemetryUserIntent(it) }, - cwsprChatHasCodeSnippet = data.activeFileContext.focusAreaContext?.codeSelection?.isNotEmpty() ?: false, - cwsprChatProgrammingLanguage = data.activeFileContext.fileContext?.fileLanguage, - credentialStartUrl = getStartUrl(project), - cwsprChatHasProjectContext = getIsProjectContextEnabled() && data.useRelevantDocuments && data.relevantTextDocuments.isNotEmpty() - ) + + Telemetry.amazonq.startConversation.use { span -> + span.cwsprChatConversationId(getConversationId(tabId).orEmpty()) + .cwsprChatTriggerInteraction(getTelemetryTriggerType(data.triggerType)) + .cwsprChatConversationType(CwsprChatConversationType.Chat) + .cwsprChatUserIntent(data.userIntent?.let { getTelemetryUserIntent(it) }) + .cwsprChatHasCodeSnippet(data.activeFileContext.focusAreaContext?.codeSelection?.isNotEmpty() == true) + .cwsprChatProgrammingLanguage(data.activeFileContext.fileContext?.fileLanguage) + .credentialStartUrl(getStartUrl(project)) + .cwsprChatHasProjectContext(getIsProjectContextEnabled() && data.useRelevantDocuments && data.relevantTextDocuments.isNotEmpty()) + } } // When Chat API responds to a user message (full response streamed) fun recordAddMessage(data: ChatRequestData, response: ChatMessage, responseLength: Int, statusCode: Int, numberOfCodeBlocks: Int) { - AmazonqTelemetry.addMessage( - cwsprChatConversationId = getConversationId(response.tabId).orEmpty(), - cwsprChatMessageId = response.messageId, - cwsprChatTriggerInteraction = getTelemetryTriggerType(data.triggerType), - cwsprChatUserIntent = data.userIntent?.let { getTelemetryUserIntent(it) }, - cwsprChatHasCodeSnippet = data.activeFileContext.focusAreaContext?.codeSelection?.isNotEmpty() ?: false, - cwsprChatProgrammingLanguage = data.activeFileContext.fileContext?.fileLanguage, - cwsprChatActiveEditorTotalCharacters = data.activeFileContext.focusAreaContext?.codeSelection?.length?.toLong(), - cwsprChatActiveEditorImportCount = data.activeFileContext.focusAreaContext?.codeNames?.fullyQualifiedNames?.used?.size?.toLong(), - cwsprChatResponseCodeSnippetCount = numberOfCodeBlocks.toLong(), - cwsprChatResponseCode = statusCode.toLong(), - cwsprChatSourceLinkCount = response.relatedSuggestions?.size?.toLong(), - cwsprChatReferencesCount = 0, // TODO - cwsprChatFollowUpCount = response.followUps?.size?.toLong(), - cwsprChatTimeToFirstChunk = getResponseStreamTimeToFirstChunk(response.tabId).toLong(), - cwsprChatTimeBetweenChunks = "[${getResponseStreamTimeBetweenChunks(response.tabId).joinToString(",")}]", - cwsprChatFullResponseLatency = responseStreamTotalTime[response.tabId]?.toLong() ?: 0, - cwsprChatRequestLength = data.message.length.toLong(), - cwsprChatResponseLength = responseLength.toLong(), - cwsprChatConversationType = CwsprChatConversationType.Chat, - credentialStartUrl = getStartUrl(project), - codewhispererCustomizationArn = data.customization?.arn, - cwsprChatHasProjectContext = getMessageHasProjectContext(response.messageId) - ) + Telemetry.amazonq.addMessage.use { span -> + span.cwsprChatConversationId(getConversationId(response.tabId).orEmpty()) + .cwsprChatMessageId(response.messageId) + .cwsprChatTriggerInteraction(getTelemetryTriggerType(data.triggerType)) + .cwsprChatUserIntent(data.userIntent?.let { getTelemetryUserIntent(it) }) + .cwsprChatHasCodeSnippet(data.activeFileContext.focusAreaContext?.codeSelection?.isNotEmpty() ?: false) + .cwsprChatProgrammingLanguage(data.activeFileContext.fileContext?.fileLanguage) + .cwsprChatActiveEditorTotalCharacters(data.activeFileContext.focusAreaContext?.codeSelection?.length?.toLong()) + .cwsprChatActiveEditorImportCount(data.activeFileContext.focusAreaContext?.codeNames?.fullyQualifiedNames?.used?.size?.toLong()) + .cwsprChatResponseCodeSnippetCount(numberOfCodeBlocks.toLong()) + .cwsprChatResponseCode(statusCode.toLong()) + .cwsprChatSourceLinkCount(response.relatedSuggestions?.size?.toLong()) + .cwsprChatReferencesCount(0L) // TODO + .cwsprChatFollowUpCount(response.followUps?.size?.toLong()) + .cwsprChatTimeToFirstChunk(getResponseStreamTimeToFirstChunk(response.tabId).toLong()) + .cwsprChatTimeBetweenChunks("[${getResponseStreamTimeBetweenChunks(response.tabId).joinToString(")")}]") + .cwsprChatFullResponseLatency(responseStreamTotalTime[response.tabId]?.toLong() ?: 0) + .cwsprChatRequestLength(data.message.length.toLong()) + .cwsprChatResponseLength(responseLength.toLong()) + .cwsprChatConversationType(CwsprChatConversationType.Chat) + .credentialStartUrl(getStartUrl(project)) + .codewhispererCustomizationArn(data.customization?.arn) + .cwsprChatHasProjectContext(getMessageHasProjectContext(response.messageId)) + } val programmingLanguage = data.activeFileContext.fileContext?.fileLanguage val validProgrammingLanguage = if (ChatSessionV1.validLanguages.contains(programmingLanguage)) programmingLanguage else null @@ -165,35 +166,43 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } fun recordMessageResponseError(data: ChatRequestData, tabId: String, responseCode: Int) { - AmazonqTelemetry.messageResponseError( - cwsprChatConversationId = getConversationId(tabId).orEmpty(), - cwsprChatTriggerInteraction = getTelemetryTriggerType(data.triggerType), - cwsprChatUserIntent = data.userIntent?.let { getTelemetryUserIntent(it) }, - cwsprChatHasCodeSnippet = data.activeFileContext.focusAreaContext?.codeSelection?.isNotEmpty() ?: false, - cwsprChatProgrammingLanguage = data.activeFileContext.fileContext?.fileLanguage, - cwsprChatActiveEditorTotalCharacters = data.activeFileContext.focusAreaContext?.codeSelection?.length?.toLong(), - cwsprChatActiveEditorImportCount = data.activeFileContext.focusAreaContext?.codeNames?.fullyQualifiedNames?.used?.size?.toLong(), - cwsprChatResponseCode = responseCode.toLong(), - cwsprChatRequestLength = data.message.length.toLong(), - cwsprChatConversationType = CwsprChatConversationType.Chat, - credentialStartUrl = getStartUrl(project) - ) + Telemetry.amazonq.messageResponseError.use { span -> + span.cwsprChatConversationId(getConversationId(tabId).orEmpty()) + .cwsprChatTriggerInteraction(getTelemetryTriggerType(data.triggerType)) + .cwsprChatUserIntent(data.userIntent?.let { getTelemetryUserIntent(it) }) + .cwsprChatHasCodeSnippet(data.activeFileContext.focusAreaContext?.codeSelection?.isNotEmpty() ?: false) + .cwsprChatProgrammingLanguage(data.activeFileContext.fileContext?.fileLanguage) + .cwsprChatActiveEditorTotalCharacters(data.activeFileContext.focusAreaContext?.codeSelection?.length?.toLong()) + .cwsprChatActiveEditorImportCount(data.activeFileContext.focusAreaContext?.codeNames?.fullyQualifiedNames?.used?.size?.toLong()) + .cwsprChatResponseCode(responseCode.toLong()) + .cwsprChatRequestLength(data.message.length.toLong()) + .cwsprChatConversationType(CwsprChatConversationType.Chat) + .credentialStartUrl(getStartUrl(project)) + + } } // When user interacts with a message (e.g. copy code, insert code, vote) - suspend fun recordInteractWithMessage(message: IncomingCwcMessage) { + suspend fun recordInteractWithMessage(message: IncomingCwcMessage) = Telemetry.amazonq.interactWithMessage.use { span -> + if (message is IncomingCwcMessage.TabId) { + span.cwsprChatConversationId(message.tabId?.let { getConversationId(it) }.orEmpty()) + } + + if (message is IncomingCwcMessage.MessageId) { + span.cwsprChatMessageId(message.messageId.orEmpty()) + .cwsprChatHasProjectContext(message.messageId?.let { getMessageHasProjectContext(it) }) + } + + span.credentialStartUrl(getStartUrl(project)) + val event: ChatInteractWithMessageEvent? = when (message) { is IncomingCwcMessage.ChatItemVoted -> { - AmazonqTelemetry.interactWithMessage( - cwsprChatConversationId = getConversationId(message.tabId).orEmpty(), - cwsprChatMessageId = message.messageId, - cwsprChatInteractionType = when (message.vote) { + span.cwsprChatInteractionType( + when (message.vote) { "upvote" -> CwsprChatInteractionType.Upvote "downvote" -> CwsprChatInteractionType.Downvote else -> CwsprChatInteractionType.Unknown - }, - credentialStartUrl = getStartUrl(project), - cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId) + } ) ChatInteractWithMessageEvent.builder().apply { conversationId(getConversationId(message.tabId).orEmpty()) @@ -210,13 +219,8 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } is IncomingCwcMessage.FollowupClicked -> { - AmazonqTelemetry.interactWithMessage( - cwsprChatConversationId = getConversationId(message.tabId).orEmpty(), - cwsprChatMessageId = message.messageId.orEmpty(), - cwsprChatInteractionType = CwsprChatInteractionType.ClickFollowUp, - credentialStartUrl = getStartUrl(project), - cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId.orEmpty()) - ) + span.cwsprChatInteractionType(CwsprChatInteractionType.ClickFollowUp) + ChatInteractWithMessageEvent.builder().apply { conversationId(getConversationId(message.tabId).orEmpty()) messageId(message.messageId.orEmpty()) @@ -226,20 +230,14 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } is IncomingCwcMessage.CopyCodeToClipboard -> { - AmazonqTelemetry.interactWithMessage( - cwsprChatConversationId = getConversationId(message.tabId).orEmpty(), - cwsprChatMessageId = message.messageId, - cwsprChatUserIntent = message.userIntent?.let { getTelemetryUserIntent(it) }, - cwsprChatInteractionType = CwsprChatInteractionType.CopySnippet, - cwsprChatAcceptedCharactersLength = message.code.length.toLong(), - cwsprChatInteractionTarget = message.insertionTargetType, - cwsprChatHasReference = null, - credentialStartUrl = getStartUrl(project), - cwsprChatCodeBlockIndex = message.codeBlockIndex?.toLong(), - cwsprChatTotalCodeBlocks = message.totalCodeBlocks?.toLong(), - cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId), - cwsprChatProgrammingLanguage = message.codeBlockLanguage, - ) + span.cwsprChatUserIntent(message.userIntent?.let { getTelemetryUserIntent(it) }) + .cwsprChatInteractionType(CwsprChatInteractionType.CopySnippet) + .cwsprChatAcceptedCharactersLength(message.code.length) + .cwsprChatInteractionTarget(message.insertionTargetType) + .cwsprChatCodeBlockIndex(message.codeBlockIndex) + .cwsprChatTotalCodeBlocks(message.totalCodeBlocks) + .cwsprChatProgrammingLanguage(message.codeBlockLanguage) + ChatInteractWithMessageEvent.builder().apply { conversationId(getConversationId(message.tabId).orEmpty()) messageId(message.messageId) @@ -251,21 +249,15 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } is IncomingCwcMessage.InsertCodeAtCursorPosition -> { - AmazonqTelemetry.interactWithMessage( - cwsprChatConversationId = getConversationId(message.tabId).orEmpty(), - cwsprChatMessageId = message.messageId, - cwsprChatUserIntent = message.userIntent?.let { getTelemetryUserIntent(it) }, - cwsprChatInteractionType = CwsprChatInteractionType.InsertAtCursor, - cwsprChatAcceptedCharactersLength = message.code.length.toLong(), - cwsprChatAcceptedNumberOfLines = message.code.lines().size.toLong(), - cwsprChatInteractionTarget = message.insertionTargetType, - cwsprChatHasReference = null, - credentialStartUrl = getStartUrl(project), - cwsprChatCodeBlockIndex = message.codeBlockIndex?.toLong(), - cwsprChatTotalCodeBlocks = message.totalCodeBlocks?.toLong(), - cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId), - cwsprChatProgrammingLanguage = message.codeBlockLanguage, - ) + span.cwsprChatUserIntent(message.userIntent?.let { getTelemetryUserIntent(it) }) + .cwsprChatInteractionType(CwsprChatInteractionType.InsertAtCursor) + .cwsprChatAcceptedCharactersLength(message.code.length) + .cwsprChatAcceptedNumberOfLines(message.code.lines().size) + .cwsprChatInteractionTarget(message.insertionTargetType) + .cwsprChatCodeBlockIndex(message.codeBlockIndex) + .cwsprChatTotalCodeBlocks(message.totalCodeBlocks) + .cwsprChatProgrammingLanguage(message.codeBlockLanguage) + ChatInteractWithMessageEvent.builder().apply { conversationId(getConversationId(message.tabId).orEmpty()) messageId(message.messageId) @@ -279,22 +271,16 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: is IncomingCwcMessage.ClickedLink -> { // Null when internal Amazon link is clicked - if (message.messageId == null) return + if (message.messageId == null) return null val linkInteractionType = when (message.type) { LinkType.SourceLink -> CwsprChatInteractionType.ClickLink LinkType.BodyLink -> CwsprChatInteractionType.ClickBodyLink else -> CwsprChatInteractionType.Unknown } - AmazonqTelemetry.interactWithMessage( - cwsprChatConversationId = getConversationId(message.tabId).orEmpty(), - cwsprChatMessageId = message.messageId, - cwsprChatInteractionType = linkInteractionType, - cwsprChatInteractionTarget = message.link, - cwsprChatHasReference = null, - credentialStartUrl = getStartUrl(project), - cwsprChatHasProjectContext = getMessageHasProjectContext(message.messageId) - ) + span.cwsprChatInteractionType(linkInteractionType) + span.cwsprChatInteractionTarget(message.link) + ChatInteractWithMessageEvent.builder().apply { conversationId(getConversationId(message.tabId).orEmpty()) messageId(message.messageId) @@ -357,24 +343,24 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } private fun recordFeedbackResult(success: Boolean) { - FeedbackTelemetry.result(project = null, success = success) + Telemetry.feedback.result.use { it.success(success) } } // When a conversation(tab) is focused fun recordEnterFocusConversation(tabId: String) { getConversationId(tabId)?.let { - AmazonqTelemetry.enterFocusConversation( - cwsprChatConversationId = it, - ) + Telemetry.amazonq.enterFocusConversation.use { span -> + span.cwsprChatConversationId(it) + } } } // When a conversation(tab) is unfocused fun recordExitFocusConversation(tabId: String) { getConversationId(tabId)?.let { - AmazonqTelemetry.exitFocusConversation( - cwsprChatConversationId = it, - ) + Telemetry.amazonq.exitFocusConversation.use { span -> + span.cwsprChatConversationId(it) + } } } @@ -422,15 +408,20 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: private val logger = getLogger() fun recordOpenChat() { - AmazonqTelemetry.openChat(passive = true) + Telemetry.amazonq.openChat.use { it.passive(true) } + } fun recordCloseChat() { - AmazonqTelemetry.closeChat(passive = true) + Telemetry.amazonq.closeChat.use { it.passive(true) } } fun recordTelemetryChatRunCommand(type: CwsprChatCommandType, name: String? = null, startUrl: String? = null) { - AmazonqTelemetry.runCommand(cwsprChatCommandType = type, cwsprChatCommandName = name, credentialStartUrl = startUrl) + Telemetry.amazonq.runCommand.use { + it.cwsprChatCommandType(type) + .cwsprChatCommandName(name) + .credentialStartUrl(startUrl) + } } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt index 09b7f8ab2b7..958db5076bf 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt @@ -24,48 +24,56 @@ sealed interface CwcMessage : AmazonQMessage // === UI -> App Messages === sealed interface IncomingCwcMessage : CwcMessage { + interface TabId { + val tabId: String? + } + + interface MessageId { + val messageId: String? + } + data class ClearChat( - @JsonProperty("tabID") val tabId: String, - ) : IncomingCwcMessage + @JsonProperty("tabID") override val tabId: String, + ) : IncomingCwcMessage, TabId data class Help( - @JsonProperty("tabID") val tabId: String, - ) : IncomingCwcMessage + @JsonProperty("tabID") override val tabId: String, + ) : IncomingCwcMessage, TabId data class ChatPrompt( val chatMessage: String, val command: String, - @JsonProperty("tabID") val tabId: String, + @JsonProperty("tabID") override val tabId: String, val userIntent: String?, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId data class TabAdded( - @JsonProperty("tabID") val tabId: String, + @JsonProperty("tabID") override val tabId: String, val tabType: String, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId data class TabRemoved( - @JsonProperty("tabID") val tabId: String, + @JsonProperty("tabID") override val tabId: String, val tabType: String, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId data class TabChanged( - @JsonProperty("tabID") val tabId: String, + @JsonProperty("tabID") override val tabId: String, @JsonProperty("prevTabID") val prevTabId: String?, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId data class FollowupClicked( val followUp: FollowUp, - @JsonProperty("tabID") val tabId: String, - val messageId: String?, + @JsonProperty("tabID") override val tabId: String, + override val messageId: String?, val command: String, val tabType: String, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId, MessageId data class CopyCodeToClipboard( val command: String?, - @JsonProperty("tabID") val tabId: String, - val messageId: String, + @JsonProperty("tabID") override val tabId: String, + override val messageId: String, val userIntent: UserIntent?, val code: String, val insertionTargetType: String?, @@ -73,11 +81,11 @@ sealed interface IncomingCwcMessage : CwcMessage { val codeBlockIndex: Int?, val totalCodeBlocks: Int?, val codeBlockLanguage: String?, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId, MessageId data class InsertCodeAtCursorPosition( - @JsonProperty("tabID") val tabId: String, - val messageId: String, + @JsonProperty("tabID") override val tabId: String, + override val messageId: String, val userIntent: UserIntent?, val code: String, val insertionTargetType: String?, @@ -86,29 +94,29 @@ sealed interface IncomingCwcMessage : CwcMessage { val codeBlockIndex: Int?, val totalCodeBlocks: Int?, val codeBlockLanguage: String?, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId, MessageId data class TriggerTabIdReceived( @JsonProperty("triggerID") val triggerId: String, - @JsonProperty("tabID") val tabId: String, - ) : IncomingCwcMessage + @JsonProperty("tabID") override val tabId: String, + ) : IncomingCwcMessage, TabId data class StopResponse( - @JsonProperty("tabID") val tabId: String, - ) : IncomingCwcMessage + @JsonProperty("tabID") override val tabId: String, + ) : IncomingCwcMessage, TabId data class ChatItemVoted( - @JsonProperty("tabID") val tabId: String, - val messageId: String, + @JsonProperty("tabID") override val tabId: String, + override val messageId: String, val vote: String, // upvote / downvote - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId, MessageId data class ChatItemFeedback( - @JsonProperty("tabID") val tabId: String, + @JsonProperty("tabID") override val tabId: String, val selectedOption: String, val comment: String?, - val messageId: String, - ) : IncomingCwcMessage + override val messageId: String, + ) : IncomingCwcMessage, TabId, MessageId data class UIFocus( val command: String, @@ -119,19 +127,19 @@ sealed interface IncomingCwcMessage : CwcMessage { data class ClickedLink( @JsonProperty("command") val type: LinkType, - @JsonProperty("tabID") val tabId: String, - val messageId: String?, + @JsonProperty("tabID") override val tabId: String, + override val messageId: String?, val link: String, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId, MessageId data class AuthFollowUpWasClicked( - @JsonProperty("tabID") val tabId: String, + @JsonProperty("tabID") override val tabId: String, val authType: AuthFollowUpType, - ) : IncomingCwcMessage + ) : IncomingCwcMessage, TabId data class OpenSettings( - @JsonProperty("tabID") val tabId: String? = null, - ) : IncomingCwcMessage + @JsonProperty("tabID") override val tabId: String? = null, + ) : IncomingCwcMessage, TabId } enum class FocusType { From 4b66203b93398c1f655bb8cee411bb47e73aae83 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 1 Nov 2024 15:11:31 -0700 Subject: [PATCH 17/20] Move CodeWhispererTelemetryService metrics to OTel --- .../service/CodeWhispererService.kt | 2 +- .../service/CodeWhispererServiceNew.kt | 2 +- .../CodeWhispererTelemetryService.kt | 364 ++++++++++-------- .../CodeWhispererTelemetryServiceNew.kt | 291 +++++--------- 4 files changed, 293 insertions(+), 366 deletions(-) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt index d6189566d1a..d75fd90329c 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt @@ -166,7 +166,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { getRequestContext(triggerTypeInfo, editor, project, psiFile, latencyContext) } catch (e: Exception) { LOG.debug { e.message.toString() } - CodeWhispererTelemetryService.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName) + CodeWhispererTelemetryService.getInstance().sendFailedServiceInvocationEvent(e) return } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt index 27f1a302f91..0a72e1cad36 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt @@ -174,7 +174,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { getRequestContext(triggerTypeInfo, editor, project, psiFile) } catch (e: Exception) { LOG.debug { e.message.toString() } - CodeWhispererTelemetryServiceNew.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName) + CodeWhispererTelemetryServiceNew.getInstance().sendFailedServiceInvocationEvent(e) return } val caretContext = requestContext.fileContextInfo.caretContext diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt index 77f62b65b9a..a6d72c6eead 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt @@ -44,10 +44,10 @@ import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask import software.aws.toolkits.telemetry.CodewhispererLanguage import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState import software.aws.toolkits.telemetry.CodewhispererSuggestionState -import software.aws.toolkits.telemetry.CodewhispererTelemetry import software.aws.toolkits.telemetry.CodewhispererTriggerType import software.aws.toolkits.telemetry.Component -import software.aws.toolkits.telemetry.Result +import software.aws.toolkits.telemetry.MetricResult +import software.aws.toolkits.telemetry.Telemetry import java.time.Duration import java.time.Instant import java.util.Queue @@ -72,23 +72,27 @@ class CodeWhispererTelemetryService { companion object { fun getInstance(): CodeWhispererTelemetryService = service() val LOG = getLogger() - const val NO_ACCEPTED_INDEX = -1 } - fun sendFailedServiceInvocationEvent(project: Project, exceptionType: String?) { - CodewhispererTelemetry.serviceInvocation( - project = project, - codewhispererCursorOffset = 0, - codewhispererLanguage = CodewhispererLanguage.Unknown, - codewhispererLastSuggestionIndex = -1, - codewhispererLineNumber = 0, - codewhispererTriggerType = CodewhispererTriggerType.Unknown, - duration = 0.0, - reason = exceptionType, - success = false, - ) + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.serviceInvocation", "software.aws.toolkits.telemetry.Telemetry") + ) + fun sendFailedServiceInvocationEvent(e: Exception): Unit = Telemetry.codewhisperer.serviceInvocation.use { + it.codewhispererCursorOffset(0) + .codewhispererLanguage(CodewhispererLanguage.Unknown) + .codewhispererLastSuggestionIndex(-1L) + .codewhispererLineNumber(0) + .codewhispererTriggerType(CodewhispererTriggerType.Unknown) + .duration(0.0) + .recordException(e) + .success(false) } + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.serviceInvocation", "software.aws.toolkits.telemetry.Telemetry") + ) fun sendServiceInvocationEvent( requestId: String, requestContext: RequestContext, @@ -97,7 +101,7 @@ class CodeWhispererTelemetryService { invocationSuccess: Boolean, latency: Double, exceptionType: String?, - ) { + ): Unit = Telemetry.codewhisperer.serviceInvocation.use { val (triggerType, automatedTriggerType) = requestContext.triggerTypeInfo val (offset, line) = requestContext.caretPosition @@ -114,33 +118,32 @@ class CodeWhispererTelemetryService { null } - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.serviceInvocation( - project = requestContext.project, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererCursorOffset = offset.toLong(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererLastSuggestionIndex = lastRecommendationIndex.toLong(), - codewhispererLineNumber = line.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererTriggerType = triggerType, - duration = latency, - reason = exceptionType, - success = invocationSuccess, - credentialStartUrl = startUrl, - codewhispererImportRecommendationEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled(), - codewhispererSupplementalContextTimeout = supContext?.isProcessTimeout, - codewhispererSupplementalContextIsUtg = supContext?.isUtg, - codewhispererSupplementalContextLatency = supContext?.latency?.toDouble(), - codewhispererSupplementalContextLength = supContext?.contentLength?.toLong(), - codewhispererCustomizationArn = requestContext.customizationArn, - ) + it.codewhispererAutomatedTriggerType(automatedTriggerType.telemetryType) + .codewhispererCompletionType(CodewhispererCompletionType.Line) + .codewhispererCursorOffset(offset) + .codewhispererGettingStartedTask(getGettingStartedTaskType(requestContext.editor)) + .codewhispererLanguage(requestContext.fileContextInfo.programmingLanguage.toTelemetryType()) + .codewhispererLastSuggestionIndex(lastRecommendationIndex) + .codewhispererLineNumber(line) + .codewhispererRequestId(requestId) + .codewhispererSessionId(responseContext.sessionId) + .codewhispererTriggerType(triggerType) + .duration(latency) + .reason(exceptionType) + .success(invocationSuccess) + .credentialStartUrl(getConnectionStartUrl(requestContext.connection)) + .codewhispererImportRecommendationEnabled(CodeWhispererSettings.getInstance().isImportAdderEnabled()) + .codewhispererSupplementalContextTimeout(supContext?.isProcessTimeout) + .codewhispererSupplementalContextIsUtg(supContext?.isUtg) + .codewhispererSupplementalContextLatency(supContext?.latency?.toDouble()) + .codewhispererSupplementalContextLength(supContext?.contentLength?.toLong()) + .codewhispererCustomizationArn(requestContext.customizationArn) } + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.userDecision", "software.aws.toolkits.telemetry.Telemetry") + ) fun sendUserDecisionEvent( requestContext: RequestContext, responseContext: ResponseContext, @@ -148,10 +151,9 @@ class CodeWhispererTelemetryService { index: Int, suggestionState: CodewhispererSuggestionState, numOfRecommendations: Int, - ) { + ): Unit = Telemetry.codewhisperer.userDecision.use { val requestId = detailContext.requestId val recommendation = detailContext.recommendation - val (project, _, triggerTypeInfo) = requestContext val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() val supplementalContext = requestContext.supplementalContext @@ -164,27 +166,28 @@ class CodeWhispererTelemetryService { } val startUrl = getConnectionStartUrl(requestContext.connection) val importEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled() - CodewhispererTelemetry.userDecision( - project = project, - codewhispererCompletionType = detailContext.completionType, - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererPaginationProgress = numOfRecommendations.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererSuggestionIndex = index.toLong(), - codewhispererSuggestionReferenceCount = recommendation.references().size.toLong(), - codewhispererSuggestionReferences = jacksonObjectMapper().writeValueAsString(recommendation.references().map { it.licenseName() }.toSet().toList()), - codewhispererSuggestionImportCount = if (importEnabled) recommendation.mostRelevantMissingImports().size.toLong() else null, - codewhispererSuggestionState = suggestionState, - codewhispererTriggerType = triggerTypeInfo.triggerType, - credentialStartUrl = startUrl, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - ) + it.codewhispererCompletionType(detailContext.completionType) + .codewhispererGettingStartedTask(getGettingStartedTaskType(requestContext.editor)) + .codewhispererLanguage(codewhispererLanguage) + .codewhispererPaginationProgress(numOfRecommendations) + .codewhispererRequestId(requestId) + .codewhispererSessionId(responseContext.sessionId) + .codewhispererSuggestionIndex(index) + .codewhispererSuggestionReferenceCount(recommendation.references().size) + .codewhispererSuggestionReferences(jacksonObjectMapper().writeValueAsString(recommendation.references().map { it.licenseName() }.toSet().toList())) + .codewhispererSuggestionImportCount(if (importEnabled) recommendation.mostRelevantMissingImports().size else null) + .codewhispererSuggestionState(suggestionState) + .codewhispererTriggerType(requestContext.triggerTypeInfo.triggerType) + .credentialStartUrl(startUrl) + .codewhispererSupplementalContextIsUtg(supplementalContext?.isUtg) + .codewhispererSupplementalContextLength(supplementalContext?.contentLength?.toLong()) + .codewhispererSupplementalContextTimeout(supplementalContext?.isProcessTimeout) } + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.userTriggerDecision", "software.aws.toolkits.telemetry.Telemetry") + ) fun sendUserTriggerDecisionEvent( requestContext: RequestContext, responseContext: ResponseContext, @@ -243,44 +246,47 @@ class CodeWhispererTelemetryService { } } - CodewhispererTelemetry.userTriggerDecision( - project = project, - codewhispererSessionId = responseContext.sessionId, - codewhispererFirstRequestId = requestContext.latencyContext.firstRequestId, - credentialStartUrl = getConnectionStartUrl(requestContext.connection), - codewhispererIsPartialAcceptance = null, - codewhispererPartialAcceptanceCount = null, - codewhispererCharactersAccepted = acceptedCharCount.toLong(), - codewhispererCharactersRecommended = null, - codewhispererCompletionType = completionType, - codewhispererLanguage = language.toTelemetryType(), - codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererLineNumber = requestContext.caretPosition.line.toLong(), - codewhispererCursorOffset = requestContext.caretPosition.offset.toLong(), - codewhispererSuggestionCount = recommendationContext.details.size.toLong(), - codewhispererSuggestionImportCount = totalImportCount.toLong(), - codewhispererTotalShownTime = popupShownTime?.toMillis()?.toDouble(), - codewhispererTriggerCharacter = triggerChar, - codewhispererTypeaheadLength = recommendationContext.userInputSinceInvocation.length.toLong(), - codewhispererTimeSinceLastDocumentChange = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged(), - codewhispererTimeSinceLastUserDecision = codewhispererTimeSinceLastUserDecision, - codewhispererTimeToFirstRecommendation = requestContext.latencyContext.paginationFirstCompletionTime, - codewhispererPreviousSuggestionState = previousUserTriggerDecision, - codewhispererSuggestionState = suggestionState, - codewhispererClassifierResult = classifierResult, - codewhispererClassifierThreshold = classifierThreshold, - codewhispererCustomizationArn = requestContext.customizationArn, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - codewhispererSupplementalContextStrategyId = supplementalContext?.strategy.toString(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererFeatureEvaluations = CodeWhispererFeatureConfigService.getInstance().getFeatureConfigsTelemetry() - ) + Telemetry.codewhisperer.userTriggerDecision.use { + it.codewhispererSessionId(responseContext.sessionId) + .codewhispererFirstRequestId(requestContext.latencyContext.firstRequestId) + .credentialStartUrl(getConnectionStartUrl(requestContext.connection)) + .codewhispererIsPartialAcceptance(null) + .codewhispererPartialAcceptanceCount(null as Long?) + .codewhispererCharactersAccepted(acceptedCharCount) + .codewhispererCharactersRecommended(null as Long?) + .codewhispererCompletionType(completionType) + .codewhispererLanguage(language.toTelemetryType()) + .codewhispererTriggerType(requestContext.triggerTypeInfo.triggerType) + .codewhispererAutomatedTriggerType(automatedTriggerType.telemetryType) + .codewhispererLineNumber(requestContext.caretPosition.line) + .codewhispererCursorOffset(requestContext.caretPosition.offset) + .codewhispererSuggestionCount(recommendationContext.details.size) + .codewhispererSuggestionImportCount(totalImportCount) + .codewhispererTotalShownTime(popupShownTime?.toMillis()?.toDouble()) + .codewhispererTriggerCharacter(triggerChar) + .codewhispererTypeaheadLength(recommendationContext.userInputSinceInvocation.length) + .codewhispererTimeSinceLastDocumentChange(CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged()) + .codewhispererTimeSinceLastUserDecision(codewhispererTimeSinceLastUserDecision) + .codewhispererTimeToFirstRecommendation(requestContext.latencyContext.paginationFirstCompletionTime) + .codewhispererPreviousSuggestionState(previousUserTriggerDecision) + .codewhispererSuggestionState(suggestionState) + .codewhispererClassifierResult(classifierResult) + .codewhispererClassifierThreshold(classifierThreshold) + .codewhispererCustomizationArn(requestContext.customizationArn) + .codewhispererSupplementalContextIsUtg(supplementalContext?.isUtg) + .codewhispererSupplementalContextLength(supplementalContext?.contentLength?.toLong()) + .codewhispererSupplementalContextTimeout(supplementalContext?.isProcessTimeout) + .codewhispererSupplementalContextStrategyId(supplementalContext?.strategy.toString()) + .codewhispererGettingStartedTask(getGettingStartedTaskType(requestContext.editor)) + .codewhispererFeatureEvaluations(CodeWhispererFeatureConfigService.getInstance().getFeatureConfigsTelemetry()) + } } - fun sendSecurityScanEvent(codeScanEvent: CodeScanTelemetryEvent, project: Project? = null) { + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.securityScan", "software.aws.toolkits.telemetry.Telemetry") + ) + fun sendSecurityScanEvent(codeScanEvent: CodeScanTelemetryEvent): Unit = Telemetry.codewhisperer.securityScan.use { val payloadContext = codeScanEvent.codeScanResponseContext.payloadContext val serviceInvocationContext = codeScanEvent.codeScanResponseContext.serviceInvocationContext val codeScanJobId = codeScanEvent.codeScanResponseContext.codeScanJobId @@ -309,49 +315,55 @@ class CodeWhispererTelemetryService { "Scope: ${codeAnalysisScope.value} \n" + "Passive: $passive \n" } - CodewhispererTelemetry.securityScan( - project = project, - codewhispererCodeScanLines = payloadContext.totalLines, - codewhispererCodeScanJobId = codeScanJobId, - codewhispererCodeScanProjectBytes = codeScanEvent.totalProjectSizeInBytes, - codewhispererCodeScanSrcPayloadBytes = payloadContext.srcPayloadSize, - codewhispererCodeScanBuildPayloadBytes = payloadContext.buildPayloadSize, - codewhispererCodeScanSrcZipFileBytes = payloadContext.srcZipFileSize, - codewhispererCodeScanTotalIssues = totalIssues.toLong(), - codewhispererCodeScanIssuesWithFixes = issuesWithFixes.toLong(), - codewhispererLanguage = payloadContext.language, - duration = codeScanEvent.duration, - contextTruncationDuration = payloadContext.totalTimeInMilliseconds, - artifactsUploadDuration = serviceInvocationContext.artifactsUploadDuration, - codeScanServiceInvocationsDuration = serviceInvocationContext.serviceInvocationDuration, - reason = reason, - result = codeScanEvent.result, - credentialStartUrl = startUrl, - codewhispererCodeScanScope = CodewhispererCodeScanScope.from(codeAnalysisScope.value), - passive = passive - ) + + it.codewhispererCodeScanLines(payloadContext.totalLines) + .codewhispererCodeScanJobId(codeScanJobId) + .codewhispererCodeScanProjectBytes(codeScanEvent.totalProjectSizeInBytes) + .codewhispererCodeScanSrcPayloadBytes(payloadContext.srcPayloadSize) + .codewhispererCodeScanBuildPayloadBytes(payloadContext.buildPayloadSize) + .codewhispererCodeScanSrcZipFileBytes(payloadContext.srcZipFileSize) + .codewhispererCodeScanTotalIssues(totalIssues) + .codewhispererCodeScanIssuesWithFixes(issuesWithFixes) + .codewhispererLanguage(payloadContext.language) + .duration(codeScanEvent.duration) + .contextTruncationDuration(payloadContext.totalTimeInMilliseconds) + .artifactsUploadDuration(serviceInvocationContext.artifactsUploadDuration) + .codeScanServiceInvocationsDuration(serviceInvocationContext.serviceInvocationDuration) + .reason(reason) + .result(codeScanEvent.result) + .credentialStartUrl(startUrl) + .codewhispererCodeScanScope(CodewhispererCodeScanScope.from(codeAnalysisScope.value)) + .passive(passive) } - fun sendCodeScanIssueHoverEvent(issue: CodeWhispererCodeScanIssue) { - CodewhispererTelemetry.codeScanIssueHover( - findingId = issue.findingId, - detectorId = issue.detectorId, - ruleId = issue.ruleId, - includesFix = issue.suggestedFixes.isNotEmpty(), - credentialStartUrl = getCodeWhispererStartUrl(issue.project) - ) + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.codeScanIssueHover", "software.aws.toolkits.telemetry.Telemetry") + ) + fun sendCodeScanIssueHoverEvent(issue: CodeWhispererCodeScanIssue): Unit = Telemetry.codewhisperer.codeScanIssueHover.use { + it.findingId(issue.findingId) + .detectorId(issue.detectorId) + .ruleId(issue.ruleId) + .includesFix(issue.suggestedFixes.isNotEmpty()) + .credentialStartUrl(getCodeWhispererStartUrl(issue.project)) } - fun sendCodeScanIssueApplyFixEvent(issue: CodeWhispererCodeScanIssue, result: Result, reason: String? = null) { - CodewhispererTelemetry.codeScanIssueApplyFix( - findingId = issue.findingId, - detectorId = issue.detectorId, - ruleId = issue.ruleId, - component = Component.Hover, - result = result, - reason = reason, - credentialStartUrl = getCodeWhispererStartUrl(issue.project) - ) + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.codeScanIssueApplyFix", "software.aws.toolkits.telemetry.Telemetry") + ) + fun sendCodeScanIssueApplyFixEvent( + issue: CodeWhispererCodeScanIssue, + result: MetricResult, + reason: String? = null, + ): Unit = Telemetry.codewhisperer.codeScanIssueApplyFix.use { + it.findingId(issue.findingId) + .detectorId(issue.detectorId) + .ruleId(issue.ruleId) + .component(Component.Hover) + .result(result) + .reason(reason) + .credentialStartUrl(getCodeWhispererStartUrl(issue.project)) } fun enqueueAcceptedSuggestionEntry( @@ -470,55 +482,65 @@ class CodeWhispererTelemetryService { } } + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.perceivedLatency", "software.aws.toolkits.telemetry.Telemetry") + ) fun sendPerceivedLatencyEvent( requestId: String, requestContext: RequestContext, responseContext: ResponseContext, latency: Double, - ) { - val (project, _, triggerTypeInfo) = requestContext + ): Unit = Telemetry.codewhisperer.perceivedLatency.use { val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.perceivedLatency( - project = project, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererLanguage = codewhispererLanguage, - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererTriggerType = triggerTypeInfo.triggerType, - duration = latency, - passive = true, - credentialStartUrl = startUrl, - codewhispererCustomizationArn = requestContext.customizationArn, - ) + + it.codewhispererCompletionType(CodewhispererCompletionType.Line) + .codewhispererLanguage(codewhispererLanguage) + .codewhispererRequestId(requestId) + .codewhispererSessionId(responseContext.sessionId) + .codewhispererTriggerType(requestContext.triggerTypeInfo.triggerType) + .duration(latency) + .passive(true) + .credentialStartUrl(startUrl) + .codewhispererCustomizationArn(requestContext.customizationArn) } - fun sendClientComponentLatencyEvent(states: InvocationContext) { + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.clientComponentLatency", "software.aws.toolkits.telemetry.Telemetry") + ) + fun sendClientComponentLatencyEvent(states: InvocationContext): Unit = Telemetry.codewhisperer.clientComponentLatency.use { val requestContext = states.requestContext val responseContext = states.responseContext val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.clientComponentLatency( - project = requestContext.project, - codewhispererSessionId = responseContext.sessionId, - codewhispererRequestId = requestContext.latencyContext.firstRequestId, - codewhispererFirstCompletionLatency = requestContext.latencyContext.paginationFirstCompletionTime, - codewhispererPreprocessingLatency = requestContext.latencyContext.getCodeWhispererPreprocessingLatency(), - codewhispererEndToEndLatency = requestContext.latencyContext.getCodeWhispererEndToEndLatency(), - codewhispererAllCompletionsLatency = requestContext.latencyContext.getCodeWhispererAllCompletionsLatency(), - codewhispererPostprocessingLatency = requestContext.latencyContext.getCodeWhispererPostprocessingLatency(), - codewhispererCredentialFetchingLatency = requestContext.latencyContext.getCodeWhispererCredentialFetchingLatency(), - codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererLanguage = codewhispererLanguage, - credentialStartUrl = startUrl, - codewhispererCustomizationArn = requestContext.customizationArn, - ) + + it.codewhispererSessionId(responseContext.sessionId) + .codewhispererRequestId(requestContext.latencyContext.firstRequestId) + .codewhispererFirstCompletionLatency(requestContext.latencyContext.paginationFirstCompletionTime) + .codewhispererPreprocessingLatency(requestContext.latencyContext.getCodeWhispererPreprocessingLatency()) + .codewhispererEndToEndLatency(requestContext.latencyContext.getCodeWhispererEndToEndLatency()) + .codewhispererAllCompletionsLatency(requestContext.latencyContext.getCodeWhispererAllCompletionsLatency()) + .codewhispererPostprocessingLatency(requestContext.latencyContext.getCodeWhispererPostprocessingLatency()) + .codewhispererCredentialFetchingLatency(requestContext.latencyContext.getCodeWhispererCredentialFetchingLatency()) + .codewhispererTriggerType(requestContext.triggerTypeInfo.triggerType) + .codewhispererCompletionType(CodewhispererCompletionType.Line) + .codewhispererLanguage(codewhispererLanguage) + .credentialStartUrl(startUrl) + .codewhispererCustomizationArn(requestContext.customizationArn) } - fun sendOnboardingClickEvent(language: CodeWhispererProgrammingLanguage, taskType: CodewhispererGettingStartedTask) { - // Project instance is not needed. We look at these metrics for each clientId. - CodewhispererTelemetry.onboardingClick(project = null, codewhispererLanguage = language.toTelemetryType(), codewhispererGettingStartedTask = taskType) + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.onboardingClick", "software.aws.toolkits.telemetry.Telemetry") + ) + fun sendOnboardingClickEvent( + language: CodeWhispererProgrammingLanguage, + taskType: CodewhispererGettingStartedTask, + ): Unit = Telemetry.codewhisperer.onboardingClick.use { + it.codewhispererLanguage(language.toTelemetryType()) + .codewhispererGettingStartedTask(taskType) } fun recordSuggestionState( diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt index 7ed73527d57..f5e4627fc6a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryServiceNew.kt @@ -4,25 +4,19 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.telemetry import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import kotlinx.coroutines.launch import org.apache.commons.collections4.queue.CircularFifoQueue -import org.jetbrains.annotations.TestOnly import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException import software.amazon.awssdk.services.codewhispererruntime.model.Completion import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService -import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor -import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage -import software.aws.toolkits.jetbrains.services.codewhisperer.model.CodeScanTelemetryEvent import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew @@ -32,25 +26,18 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispe import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererServiceNew import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContextNew import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getCodeWhispererStartUrl import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getConnectionStartUrl import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getGettingStartedTaskType import software.aws.toolkits.jetbrains.services.codewhisperer.util.runIfIdcConnectionOrTelemetryEnabled import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings -import software.aws.toolkits.telemetry.CodewhispererCodeScanScope import software.aws.toolkits.telemetry.CodewhispererCompletionType -import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask import software.aws.toolkits.telemetry.CodewhispererLanguage import software.aws.toolkits.telemetry.CodewhispererPreviousSuggestionState import software.aws.toolkits.telemetry.CodewhispererSuggestionState -import software.aws.toolkits.telemetry.CodewhispererTelemetry import software.aws.toolkits.telemetry.CodewhispererTriggerType -import software.aws.toolkits.telemetry.Component -import software.aws.toolkits.telemetry.Result +import software.aws.toolkits.telemetry.Telemetry import java.time.Duration import java.time.Instant -import java.util.Queue @Service class CodeWhispererTelemetryServiceNew { @@ -70,23 +57,27 @@ class CodeWhispererTelemetryServiceNew { companion object { fun getInstance(): CodeWhispererTelemetryServiceNew = service() val LOG = getLogger() - const val NO_ACCEPTED_INDEX = -1 } - fun sendFailedServiceInvocationEvent(project: Project, exceptionType: String?) { - CodewhispererTelemetry.serviceInvocation( - project = project, - codewhispererCursorOffset = 0, - codewhispererLanguage = CodewhispererLanguage.Unknown, - codewhispererLastSuggestionIndex = -1, - codewhispererLineNumber = 0, - codewhispererTriggerType = CodewhispererTriggerType.Unknown, - duration = 0.0, - reason = exceptionType, - success = false, - ) + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.serviceInvocation", "software.aws.toolkits.telemetry.Telemetry") + ) + fun sendFailedServiceInvocationEvent(e: Exception): Unit = Telemetry.codewhisperer.serviceInvocation.use { + it.codewhispererCursorOffset(0) + .codewhispererLanguage(CodewhispererLanguage.Unknown) + .codewhispererLastSuggestionIndex(-1L) + .codewhispererLineNumber(0) + .codewhispererTriggerType(CodewhispererTriggerType.Unknown) + .duration(0.0) + .recordException(e) + .success(false) } + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.serviceInvocation", "software.aws.toolkits.telemetry.Telemetry") + ) fun sendServiceInvocationEvent( jobId: Int, requestId: String, @@ -96,7 +87,7 @@ class CodeWhispererTelemetryServiceNew { invocationSuccess: Boolean, latency: Double, exceptionType: String?, - ) { + ): Unit = Telemetry.codewhisperer.serviceInvocation.use { LOG.debug { "Sending serviceInvocation for $requestId, jobId: $jobId" } val (triggerType, automatedTriggerType) = requestContext.triggerTypeInfo val (offset, line) = requestContext.caretPosition @@ -114,33 +105,32 @@ class CodeWhispererTelemetryServiceNew { null } - val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() - val startUrl = getConnectionStartUrl(requestContext.connection) - CodewhispererTelemetry.serviceInvocation( - project = requestContext.project, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererCompletionType = CodewhispererCompletionType.Line, - codewhispererCursorOffset = offset.toLong(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererLastSuggestionIndex = lastRecommendationIndex.toLong(), - codewhispererLineNumber = line.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererTriggerType = triggerType, - duration = latency, - reason = exceptionType, - success = invocationSuccess, - credentialStartUrl = startUrl, - codewhispererImportRecommendationEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled(), - codewhispererSupplementalContextTimeout = supContext?.isProcessTimeout, - codewhispererSupplementalContextIsUtg = supContext?.isUtg, - codewhispererSupplementalContextLatency = supContext?.latency?.toDouble(), - codewhispererSupplementalContextLength = supContext?.contentLength?.toLong(), - codewhispererCustomizationArn = requestContext.customizationArn, - ) + it.codewhispererAutomatedTriggerType(automatedTriggerType.telemetryType) + .codewhispererCompletionType(CodewhispererCompletionType.Line) + .codewhispererCursorOffset(offset) + .codewhispererGettingStartedTask(getGettingStartedTaskType(requestContext.editor)) + .codewhispererLanguage(requestContext.fileContextInfo.programmingLanguage.toTelemetryType()) + .codewhispererLastSuggestionIndex(lastRecommendationIndex) + .codewhispererLineNumber(line) + .codewhispererRequestId(requestId) + .codewhispererSessionId(responseContext.sessionId) + .codewhispererTriggerType(triggerType) + .duration(latency) + .reason(exceptionType) + .success(invocationSuccess) + .credentialStartUrl(getConnectionStartUrl(requestContext.connection)) + .codewhispererImportRecommendationEnabled(CodeWhispererSettings.getInstance().isImportAdderEnabled()) + .codewhispererSupplementalContextTimeout(supContext?.isProcessTimeout) + .codewhispererSupplementalContextIsUtg(supContext?.isUtg) + .codewhispererSupplementalContextLatency(supContext?.latency?.toDouble()) + .codewhispererSupplementalContextLength(supContext?.contentLength?.toLong()) + .codewhispererCustomizationArn(requestContext.customizationArn) } + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.userDecision", "software.aws.toolkits.telemetry.Telemetry") + ) fun sendUserDecisionEvent( requestContext: RequestContextNew, responseContext: ResponseContext, @@ -148,10 +138,9 @@ class CodeWhispererTelemetryServiceNew { index: Int, suggestionState: CodewhispererSuggestionState, numOfRecommendations: Int, - ) { + ): Unit = Telemetry.codewhisperer.userDecision.use { val requestId = detailContext.requestId val recommendation = detailContext.recommendation - val (project, _, triggerTypeInfo) = requestContext val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() val supplementalContext = requestContext.supplementalContext @@ -164,27 +153,29 @@ class CodeWhispererTelemetryServiceNew { } val startUrl = getConnectionStartUrl(requestContext.connection) val importEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled() - CodewhispererTelemetry.userDecision( - project = project, - codewhispererCompletionType = detailContext.completionType, - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererLanguage = codewhispererLanguage, - codewhispererPaginationProgress = numOfRecommendations.toLong(), - codewhispererRequestId = requestId, - codewhispererSessionId = responseContext.sessionId, - codewhispererSuggestionIndex = index.toLong(), - codewhispererSuggestionReferenceCount = recommendation.references().size.toLong(), - codewhispererSuggestionReferences = jacksonObjectMapper().writeValueAsString(recommendation.references().map { it.licenseName() }.toSet().toList()), - codewhispererSuggestionImportCount = if (importEnabled) recommendation.mostRelevantMissingImports().size.toLong() else null, - codewhispererSuggestionState = suggestionState, - codewhispererTriggerType = triggerTypeInfo.triggerType, - credentialStartUrl = startUrl, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - ) + + it.codewhispererCompletionType(detailContext.completionType) + .codewhispererGettingStartedTask(getGettingStartedTaskType(requestContext.editor)) + .codewhispererLanguage(codewhispererLanguage) + .codewhispererPaginationProgress(numOfRecommendations) + .codewhispererRequestId(requestId) + .codewhispererSessionId(responseContext.sessionId) + .codewhispererSuggestionIndex(index) + .codewhispererSuggestionReferenceCount(recommendation.references().size) + .codewhispererSuggestionReferences(jacksonObjectMapper().writeValueAsString(recommendation.references().map { it.licenseName() }.toSet().toList())) + .codewhispererSuggestionImportCount(if (importEnabled) recommendation.mostRelevantMissingImports().size else null) + .codewhispererSuggestionState(suggestionState) + .codewhispererTriggerType(requestContext.triggerTypeInfo.triggerType) + .credentialStartUrl(startUrl) + .codewhispererSupplementalContextIsUtg(supplementalContext?.isUtg) + .codewhispererSupplementalContextLength(supplementalContext?.contentLength?.toLong()) + .codewhispererSupplementalContextTimeout(supplementalContext?.isProcessTimeout) } + @Deprecated( + "Does not capture entire context of method call", + ReplaceWith("Telemetry.codewhisperer.userTriggerDecision", "software.aws.toolkits.telemetry.Telemetry") + ) fun sendUserTriggerDecisionEvent( sessionContext: SessionContextNew, requestContext: RequestContextNew, @@ -245,115 +236,40 @@ class CodeWhispererTelemetryServiceNew { } } - CodewhispererTelemetry.userTriggerDecision( - project = project, - codewhispererSessionId = responseContext.sessionId, - codewhispererFirstRequestId = sessionContext.latencyContext.firstRequestId, - credentialStartUrl = getConnectionStartUrl(requestContext.connection), - codewhispererIsPartialAcceptance = null, - codewhispererPartialAcceptanceCount = null, - codewhispererCharactersAccepted = acceptedCharCount.toLong(), - codewhispererCharactersRecommended = null, - codewhispererCompletionType = completionType, - codewhispererLanguage = language.toTelemetryType(), - codewhispererTriggerType = requestContext.triggerTypeInfo.triggerType, - codewhispererAutomatedTriggerType = automatedTriggerType.telemetryType, - codewhispererLineNumber = requestContext.caretPosition.line.toLong(), - codewhispererCursorOffset = requestContext.caretPosition.offset.toLong(), - codewhispererSuggestionCount = recommendationContext.details.size.toLong(), - codewhispererSuggestionImportCount = totalImportCount.toLong(), - codewhispererTotalShownTime = popupShownTime?.toMillis()?.toDouble(), - codewhispererTriggerCharacter = triggerChar, - codewhispererTypeaheadLength = recommendationContext.userInputSinceInvocation.length.toLong(), - codewhispererTimeSinceLastDocumentChange = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged(), - codewhispererTimeSinceLastUserDecision = codewhispererTimeSinceLastUserDecision, - codewhispererTimeToFirstRecommendation = sessionContext.latencyContext.paginationFirstCompletionTime, - codewhispererPreviousSuggestionState = previousUserTriggerDecision, - codewhispererSuggestionState = suggestionState, - codewhispererClassifierResult = classifierResult, - codewhispererClassifierThreshold = classifierThreshold, - codewhispererCustomizationArn = requestContext.customizationArn, - codewhispererSupplementalContextIsUtg = supplementalContext?.isUtg, - codewhispererSupplementalContextLength = supplementalContext?.contentLength?.toLong(), - codewhispererSupplementalContextTimeout = supplementalContext?.isProcessTimeout, - codewhispererSupplementalContextStrategyId = supplementalContext?.strategy.toString(), - codewhispererGettingStartedTask = getGettingStartedTaskType(requestContext.editor), - codewhispererFeatureEvaluations = CodeWhispererFeatureConfigService.getInstance().getFeatureConfigsTelemetry() - ) - } - - fun sendSecurityScanEvent(codeScanEvent: CodeScanTelemetryEvent, project: Project? = null) { - val payloadContext = codeScanEvent.codeScanResponseContext.payloadContext - val serviceInvocationContext = codeScanEvent.codeScanResponseContext.serviceInvocationContext - val codeScanJobId = codeScanEvent.codeScanResponseContext.codeScanJobId - val totalIssues = codeScanEvent.codeScanResponseContext.codeScanTotalIssues - val issuesWithFixes = codeScanEvent.codeScanResponseContext.codeScanIssuesWithFixes - val reason = codeScanEvent.codeScanResponseContext.reason - val startUrl = getConnectionStartUrl(codeScanEvent.connection) - val codeAnalysisScope = codeScanEvent.codeAnalysisScope - val passive = codeAnalysisScope == CodeWhispererConstants.CodeAnalysisScope.FILE - - LOG.debug { - "Recording code security scan event. \n" + - "Total number of security scan issues found: $totalIssues, \n" + - "Number of security scan issues with fixes: $issuesWithFixes, \n" + - "Language: ${payloadContext.language}, \n" + - "Uncompressed source payload size in bytes: ${payloadContext.srcPayloadSize}, \n" + - "Uncompressed build payload size in bytes: ${payloadContext.buildPayloadSize}, \n" + - "Compressed source zip file size in bytes: ${payloadContext.srcZipFileSize}, \n" + - "Total project size in bytes: ${codeScanEvent.totalProjectSizeInBytes}, \n" + - "Total duration of the security scan job in milliseconds: ${codeScanEvent.duration}, \n" + - "Context truncation duration in milliseconds: ${payloadContext.totalTimeInMilliseconds}, \n" + - "Artifacts upload duration in milliseconds: ${serviceInvocationContext.artifactsUploadDuration}, \n" + - "Service invocation duration in milliseconds: ${serviceInvocationContext.serviceInvocationDuration}, \n" + - "Total number of lines scanned: ${payloadContext.totalLines}, \n" + - "Reason: $reason \n" + - "Scope: ${codeAnalysisScope.value} \n" + - "Passive: $passive \n" + Telemetry.codewhisperer.userTriggerDecision.use { + it.codewhispererSessionId(responseContext.sessionId) + .codewhispererFirstRequestId(sessionContext.latencyContext.firstRequestId) + .credentialStartUrl(getConnectionStartUrl(requestContext.connection)) + .codewhispererIsPartialAcceptance(null) + .codewhispererPartialAcceptanceCount(null as Long?) + .codewhispererCharactersAccepted(acceptedCharCount) + .codewhispererCharactersRecommended(null as Long?) + .codewhispererCompletionType(completionType) + .codewhispererLanguage(language.toTelemetryType()) + .codewhispererTriggerType(requestContext.triggerTypeInfo.triggerType) + .codewhispererAutomatedTriggerType(automatedTriggerType.telemetryType) + .codewhispererLineNumber(requestContext.caretPosition.line) + .codewhispererCursorOffset(requestContext.caretPosition.offset) + .codewhispererSuggestionCount(recommendationContext.details.size) + .codewhispererSuggestionImportCount(totalImportCount) + .codewhispererTotalShownTime(popupShownTime?.toMillis()?.toDouble()) + .codewhispererTriggerCharacter(triggerChar) + .codewhispererTypeaheadLength(recommendationContext.userInputSinceInvocation.length) + .codewhispererTimeSinceLastDocumentChange(CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged()) + .codewhispererTimeSinceLastUserDecision(codewhispererTimeSinceLastUserDecision) + .codewhispererTimeToFirstRecommendation(sessionContext.latencyContext.paginationFirstCompletionTime) + .codewhispererPreviousSuggestionState(previousUserTriggerDecision) + .codewhispererSuggestionState(suggestionState) + .codewhispererClassifierResult(classifierResult) + .codewhispererClassifierThreshold(classifierThreshold) + .codewhispererCustomizationArn(requestContext.customizationArn) + .codewhispererSupplementalContextIsUtg(supplementalContext?.isUtg) + .codewhispererSupplementalContextLength(supplementalContext?.contentLength?.toLong()) + .codewhispererSupplementalContextTimeout(supplementalContext?.isProcessTimeout) + .codewhispererSupplementalContextStrategyId(supplementalContext?.strategy.toString()) + .codewhispererGettingStartedTask(getGettingStartedTaskType(requestContext.editor)) + .codewhispererFeatureEvaluations(CodeWhispererFeatureConfigService.getInstance().getFeatureConfigsTelemetry()) } - CodewhispererTelemetry.securityScan( - project = project, - codewhispererCodeScanLines = payloadContext.totalLines, - codewhispererCodeScanJobId = codeScanJobId, - codewhispererCodeScanProjectBytes = codeScanEvent.totalProjectSizeInBytes, - codewhispererCodeScanSrcPayloadBytes = payloadContext.srcPayloadSize, - codewhispererCodeScanBuildPayloadBytes = payloadContext.buildPayloadSize, - codewhispererCodeScanSrcZipFileBytes = payloadContext.srcZipFileSize, - codewhispererCodeScanTotalIssues = totalIssues.toLong(), - codewhispererCodeScanIssuesWithFixes = issuesWithFixes.toLong(), - codewhispererLanguage = payloadContext.language, - duration = codeScanEvent.duration, - contextTruncationDuration = payloadContext.totalTimeInMilliseconds, - artifactsUploadDuration = serviceInvocationContext.artifactsUploadDuration, - codeScanServiceInvocationsDuration = serviceInvocationContext.serviceInvocationDuration, - reason = reason, - result = codeScanEvent.result, - credentialStartUrl = startUrl, - codewhispererCodeScanScope = CodewhispererCodeScanScope.from(codeAnalysisScope.value), - passive = passive - ) - } - - fun sendCodeScanIssueHoverEvent(issue: CodeWhispererCodeScanIssue) { - CodewhispererTelemetry.codeScanIssueHover( - findingId = issue.findingId, - detectorId = issue.detectorId, - ruleId = issue.ruleId, - includesFix = issue.suggestedFixes.isNotEmpty(), - credentialStartUrl = getCodeWhispererStartUrl(issue.project) - ) - } - - fun sendCodeScanIssueApplyFixEvent(issue: CodeWhispererCodeScanIssue, result: Result, reason: String? = null) { - CodewhispererTelemetry.codeScanIssueApplyFix( - findingId = issue.findingId, - detectorId = issue.detectorId, - ruleId = issue.ruleId, - component = Component.Hover, - result = result, - reason = reason, - credentialStartUrl = getCodeWhispererStartUrl(issue.project) - ) } fun enqueueAcceptedSuggestionEntry( @@ -487,11 +403,6 @@ class CodeWhispererTelemetryServiceNew { } } - fun sendOnboardingClickEvent(language: CodeWhispererProgrammingLanguage, taskType: CodewhispererGettingStartedTask) { - // Project instance is not needed. We look at these metrics for each clientId. - CodewhispererTelemetry.onboardingClick(project = null, codewhispererLanguage = language.toTelemetryType(), codewhispererGettingStartedTask = taskType) - } - fun recordSuggestionState( detail: DetailContextNew, hasUserAccepted: Boolean, @@ -511,10 +422,4 @@ class CodeWhispererTelemetryServiceNew { } else { CodewhispererSuggestionState.Reject } - - @TestOnly - fun previousDecisions(): Queue { - assert(ApplicationManager.getApplication().isUnitTestMode) - return this.previousUserTriggerDecisions - } } From c30c6aa2cdfe277b6351a9ba936d7babc0ad5fae Mon Sep 17 00:00:00 2001 From: Richard Li Date: Wed, 6 Nov 2024 12:35:28 -0800 Subject: [PATCH 18/20] lint --- .../cwc/controller/chat/telemetry/TelemetryHelper.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt index f9a90a1672e..bf7637e6cff 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt @@ -72,7 +72,6 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: // When chat panel is focused fun recordEnterFocusChat() { Telemetry.amazonq.enterFocusChat.use { it.passive(true) } - } // When chat panel is unfocused @@ -178,7 +177,6 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: .cwsprChatRequestLength(data.message.length.toLong()) .cwsprChatConversationType(CwsprChatConversationType.Chat) .credentialStartUrl(getStartUrl(project)) - } } @@ -297,11 +295,16 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: } is IncomingCwcMessage.ChatItemFeedback -> { + span.cwsprChatInteractionType(CwsprChatInteractionType.Unknown) recordFeedback(message) null } - else -> null + else -> { + span.cwsprChatInteractionType(CwsprChatInteractionType.Unknown) + + null + } }?.let { // override request and add customizationArn if it's not null, else return itself customization?.let { myCustomization -> @@ -409,7 +412,6 @@ class TelemetryHelper(private val project: Project, private val sessionStorage: fun recordOpenChat() { Telemetry.amazonq.openChat.use { it.passive(true) } - } fun recordCloseChat() { From 56fd69d197d3dc1e68cd96f68ad37c59ed0015dd Mon Sep 17 00:00:00 2001 From: Richard Li Date: Wed, 6 Nov 2024 13:04:59 -0800 Subject: [PATCH 19/20] tst --- .../toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt index a0502367d7c..267d159e906 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt @@ -22,6 +22,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.stub +import org.mockito.kotlin.times import org.mockito.kotlin.verify import software.amazon.awssdk.awscore.DefaultAwsResponseMetadata import software.amazon.awssdk.awscore.util.AwsHeader.AWS_REQUEST_ID @@ -608,7 +609,7 @@ class TelemetryHelperTest { // Toolkit telemetry argumentCaptor { - verify(mockBatcher).enqueue(capture()) + verify(mockBatcher, times(2)).enqueue(capture()) val event = firstValue.data.find { it.name == "feedback_result" } assertNotNull(event) assertThat(event).matches { it.metadata["result"] == "Succeeded" } From 5a29b2665abeb331b0c9f01e81119c5456646d40 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 8 Nov 2024 10:42:16 -0800 Subject: [PATCH 20/20] lint --- .../codewhisperer/telemetry/CodeWhispererTelemetryService.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt index a6d72c6eead..d74a436658d 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererTelemetryService.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.editor.RangeMarker -import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import kotlinx.coroutines.launch import org.apache.commons.collections4.queue.CircularFifoQueue @@ -151,7 +150,7 @@ class CodeWhispererTelemetryService { index: Int, suggestionState: CodewhispererSuggestionState, numOfRecommendations: Int, - ): Unit = Telemetry.codewhisperer.userDecision.use { + ): Unit = Telemetry.codewhisperer.userDecision.use { span -> val requestId = detailContext.requestId val recommendation = detailContext.recommendation val codewhispererLanguage = requestContext.fileContextInfo.programmingLanguage.toTelemetryType() @@ -166,7 +165,7 @@ class CodeWhispererTelemetryService { } val startUrl = getConnectionStartUrl(requestContext.connection) val importEnabled = CodeWhispererSettings.getInstance().isImportAdderEnabled() - it.codewhispererCompletionType(detailContext.completionType) + span.codewhispererCompletionType(detailContext.completionType) .codewhispererGettingStartedTask(getGettingStartedTaskType(requestContext.editor)) .codewhispererLanguage(codewhispererLanguage) .codewhispererPaginationProgress(numOfRecommendations)