Skip to content

Commit

Permalink
Add support for span creating mode in metrics.timing API (#3248)
Browse files Browse the repository at this point in the history
* Create a span when metrics.timing is called

* Populate span tags with metric tags

* Update Changelog
  • Loading branch information
markushi authored Mar 7, 2024
1 parent e37359d commit f4f39c7
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Experimental: Add span summaries for metrics ([#3238](https://github.com/getsentry/sentry-java/pull/3238))
- Experimental: Implement span creating mode in metrics.timing API ([#3248](https://github.com/getsentry/sentry-java/pull/3248))

## 7.5.0

Expand Down
3 changes: 3 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/MetricsApi$
public fun setTransaction (Ljava/lang/String;)V
public fun setUser (Lio/sentry/protocol/User;)V
public fun startSession ()V
public fun startSpanForMetric (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction;
public fun traceHeaders ()Lio/sentry/SentryTraceHeader;
public fun withScope (Lio/sentry/ScopeCallback;)V
Expand Down Expand Up @@ -3536,6 +3537,7 @@ public abstract interface class io/sentry/metrics/MetricsApi$IMetricsInterface {
public abstract fun getDefaultTagsForMetrics ()Ljava/util/Map;
public abstract fun getLocalMetricsAggregator ()Lio/sentry/metrics/LocalMetricsAggregator;
public abstract fun getMetricsAggregator ()Lio/sentry/IMetricsAggregator;
public abstract fun startSpanForMetric (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
}

public final class io/sentry/metrics/MetricsHelper {
Expand Down Expand Up @@ -3570,6 +3572,7 @@ public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsA
public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JILio/sentry/metrics/LocalMetricsAggregator;)V
public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JILio/sentry/metrics/LocalMetricsAggregator;)V
public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JILio/sentry/metrics/LocalMetricsAggregator;)V
public fun startSpanForMetric (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;ILio/sentry/metrics/LocalMetricsAggregator;)V
}

Expand Down
9 changes: 9 additions & 0 deletions sentry/src/main/java/io/sentry/Hub.java
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,15 @@ private IScope buildLocalScope(
return Collections.unmodifiableMap(tags);
}

@Override
public @Nullable ISpan startSpanForMetric(@NotNull String op, @NotNull String description) {
final @Nullable ISpan span = getSpan();
if (span != null) {
return span.startChild(op, description);
}
return null;
}

@Override
public @Nullable LocalMetricsAggregator getLocalMetricsAggregator() {
if (!options.isEnableSpanLocalMetricAggregation()) {
Expand Down
22 changes: 19 additions & 3 deletions sentry/src/main/java/io/sentry/metrics/MetricsApi.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry.metrics;

import io.sentry.IMetricsAggregator;
import io.sentry.ISpan;
import io.sentry.MeasurementUnit;
import java.util.Map;
import org.jetbrains.annotations.ApiStatus;
Expand All @@ -19,6 +20,9 @@ public interface IMetricsInterface {

@NotNull
Map<String, String> getDefaultTagsForMetrics();

@Nullable
ISpan startSpanForMetric(final @NotNull String op, final @NotNull String description);
}

private final @NotNull MetricsApi.IMetricsInterface aggregator;
Expand Down Expand Up @@ -554,8 +558,20 @@ public void timing(
final @Nullable LocalMetricsAggregator localMetricsAggregator =
aggregator.getLocalMetricsAggregator();

aggregator
.getMetricsAggregator()
.timing(key, callback, durationUnit, enrichedTags, stackLevel, localMetricsAggregator);
final @Nullable ISpan span = aggregator.startSpanForMetric("metric.timing", key);
if (span != null && tags != null) {
for (final @NotNull Map.Entry<String, String> entry : tags.entrySet()) {
span.setTag(entry.getKey(), entry.getValue());
}
}
try {
aggregator
.getMetricsAggregator()
.timing(key, callback, durationUnit, enrichedTags, stackLevel, localMetricsAggregator);
} finally {
if (span != null) {
span.finish();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry.metrics;

import io.sentry.IMetricsAggregator;
import io.sentry.ISpan;
import io.sentry.MeasurementUnit;
import java.io.IOException;
import java.util.Collections;
Expand Down Expand Up @@ -102,4 +103,9 @@ public void close() throws IOException {}
public @NotNull Map<String, String> getDefaultTagsForMetrics() {
return Collections.emptyMap();
}

@Override
public @Nullable ISpan startSpanForMetric(@NotNull String op, @NotNull String description) {
return null;
}
}
21 changes: 21 additions & 0 deletions sentry/src/test/java/io/sentry/HubTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,27 @@ class HubTest {
assertNotNull(hub.localMetricsAggregator)
}

@Test
fun `hub startSpanForMetric starts a child span`() {
val hub = generateHub {
it.isEnableMetrics = true
it.isEnableSpanLocalMetricAggregation = true
it.sampleRate = 1.0
} as Hub

val txn = hub.startTransaction(
"name.txn",
"op.txn",
TransactionOptions().apply { isBindToScope = true }
)

val span = hub.startSpanForMetric("op", "key")!!

assertEquals("op", span.spanContext.op)
assertEquals("key", span.spanContext.description)
assertEquals(span.spanContext.parentSpanId, txn.spanContext.spanId)
}

private val dsnTest = "https://key@sentry.io/proj"

private fun generateHub(optionsConfiguration: Sentry.OptionsConfiguration<SentryOptions>? = null): IHub {
Expand Down
83 changes: 82 additions & 1 deletion sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package io.sentry.metrics

import io.sentry.IMetricsAggregator
import io.sentry.ISpan
import io.sentry.MeasurementUnit
import io.sentry.metrics.MetricsApi.IMetricsInterface
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import kotlin.test.Test
import kotlin.test.assertEquals

class MetricsApiTest {

class Fixture {
val aggregator = mock<IMetricsAggregator>()
val localMetricsAggregator = mock<LocalMetricsAggregator>()

fun getSut(defaultTags: Map<String, String> = emptyMap()): MetricsApi {
var lastSpan: ISpan? = null
var lastOp: String? = null
var lastDescription: String? = null

fun getSut(
defaultTags: Map<String, String> = emptyMap(),
spanProvider: () -> ISpan? = { mock<ISpan>() }
): MetricsApi {
val localAggregator = localMetricsAggregator

return MetricsApi(object : IMetricsInterface {
Expand All @@ -26,6 +37,13 @@ class MetricsApiTest {
override fun getLocalMetricsAggregator(): LocalMetricsAggregator? = localAggregator

override fun getDefaultTagsForMetrics(): Map<String, String> = defaultTags

override fun startSpanForMetric(op: String, description: String): ISpan? {
lastOp = op
lastDescription = description
lastSpan = spanProvider()
return lastSpan
}
})
}
}
Expand Down Expand Up @@ -288,4 +306,67 @@ class MetricsApiTest {
eq(fixture.localMetricsAggregator)
)
}

@Test
fun `timing starts and finishes a span`() {
val api = fixture.getSut()

api.timing("key") {
// no-op
}

assertEquals("metric.timing", fixture.lastOp)
assertEquals("key", fixture.lastDescription)

verify(fixture.lastSpan!!).finish()
}

@Test
fun `timing applies metric tags as span tags`() {
val span = mock<ISpan>()
val api = fixture.getSut(
spanProvider = {
span
},
defaultTags = mapOf(
"release" to "1.0"
)
)
// when timing is called
api.timing("key", {
// no-op
}, MeasurementUnit.Duration.NANOSECOND, mapOf("a" to "b"))

// the last span should have the metric tags, without the default ones
verify(fixture.lastSpan!!, never()).setTag("release", "1.0")
verify(fixture.lastSpan!!).setTag("a", "b")
}

@Test
fun `if timing throws an exception, span still finishes`() {
val api = fixture.getSut()

try {
api.timing("key") {
throw IllegalStateException()
}
} catch (e: IllegalStateException) {
// ignored
}

assertEquals("metric.timing", fixture.lastOp)
assertEquals("key", fixture.lastDescription)
verify(fixture.lastSpan!!).finish()
}

@Test
fun `if timing does retrieve a null span, it still works`() {
val api = fixture.getSut(
spanProvider = { null }
)
api.timing("key") {
// no-op
}
// no crash
}
}

0 comments on commit f4f39c7

Please sign in to comment.