Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ public final class io/sentry/Span : io/sentry/ISpan {
public fun toSentryTrace ()Lio/sentry/SentryTraceHeader;
}

public class io/sentry/SpanContext {
public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
public static final field TYPE Ljava/lang/String;
protected field description Ljava/lang/String;
protected field op Ljava/lang/String;
Expand All @@ -1102,10 +1102,30 @@ public class io/sentry/SpanContext {
public fun getStatus ()Lio/sentry/SpanStatus;
public fun getTags ()Ljava/util/Map;
public fun getTraceId ()Lio/sentry/protocol/SentryId;
public fun getUnknown ()Ljava/util/Map;
public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V
public fun setDescription (Ljava/lang/String;)V
public fun setOperation (Ljava/lang/String;)V
public fun setStatus (Lio/sentry/SpanStatus;)V
public fun setTag (Ljava/lang/String;Ljava/lang/String;)V
public fun setUnknown (Ljava/util/Map;)V
}

public final class io/sentry/SpanContext$Deserializer : io/sentry/JsonDeserializer {
public fun <init> ()V
public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/SpanContext;
public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object;
}

public final class io/sentry/SpanContext$JsonKeys {
public static final field DESCRIPTION Ljava/lang/String;
public static final field OP Ljava/lang/String;
public static final field PARENT_SPAN_ID Ljava/lang/String;
public static final field SPAN_ID Ljava/lang/String;
public static final field STATUS Ljava/lang/String;
public static final field TAGS Ljava/lang/String;
public static final field TRACE_ID Ljava/lang/String;
public fun <init> ()V
}

public final class io/sentry/SpanId : io/sentry/JsonSerializable {
Expand All @@ -1124,7 +1144,7 @@ public final class io/sentry/SpanId$Deserializer : io/sentry/JsonDeserializer {
public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object;
}

public final class io/sentry/SpanStatus : java/lang/Enum {
public final class io/sentry/SpanStatus : java/lang/Enum, io/sentry/JsonSerializable {
public static final field ABORTED Lio/sentry/SpanStatus;
public static final field ALREADY_EXISTS Lio/sentry/SpanStatus;
public static final field CANCELLED Lio/sentry/SpanStatus;
Expand All @@ -1145,10 +1165,17 @@ public final class io/sentry/SpanStatus : java/lang/Enum {
public static final field UNKNOWN_ERROR Lio/sentry/SpanStatus;
public static fun fromHttpStatusCode (I)Lio/sentry/SpanStatus;
public static fun fromHttpStatusCode (Ljava/lang/Integer;Lio/sentry/SpanStatus;)Lio/sentry/SpanStatus;
public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V
public static fun valueOf (Ljava/lang/String;)Lio/sentry/SpanStatus;
public static fun values ()[Lio/sentry/SpanStatus;
}

public final class io/sentry/SpanStatus$Deserializer : io/sentry/JsonDeserializer {
public fun <init> ()V
public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/SpanStatus;
public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object;
}

public final class io/sentry/SystemOutLogger : io/sentry/ILogger {
public fun <init> ()V
public fun isEnabled (Lio/sentry/SentryLevel;)Z
Expand Down
145 changes: 144 additions & 1 deletion sentry/src/main/java/io/sentry/SpanContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
import io.sentry.protocol.SentryId;
import io.sentry.util.CollectionUtils;
import io.sentry.util.Objects;
import io.sentry.vendor.gson.stream.JsonToken;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

@Open
public class SpanContext {
public class SpanContext implements JsonUnknown, JsonSerializable {
public static final String TYPE = "trace";

/** Determines which trace the Span belongs to. */
Expand Down Expand Up @@ -41,6 +43,8 @@ public class SpanContext {
/** A map or list of tags for this event. Each tag must be less than 200 characters. */
protected @NotNull Map<String, @NotNull String> tags = new ConcurrentHashMap<>();

private @Nullable Map<String, Object> unknown;

public SpanContext(final @NotNull String operation, final @Nullable Boolean sampled) {
this(new SentryId(), new SpanId(), operation, null, sampled);
}
Expand Down Expand Up @@ -143,4 +147,143 @@ public SpanId getParentSpanId() {
void setSampled(final @Nullable Boolean sampled) {
this.sampled = sampled;
}

// region JsonSerializable

public static final class JsonKeys {
public static final String TRACE_ID = "trace_id";
public static final String SPAN_ID = "span_id";
public static final String PARENT_SPAN_ID = "parent_span_id";
public static final String OP = "op";
public static final String DESCRIPTION = "description";
public static final String STATUS = "status";
public static final String TAGS = "tags";
}

@Override
public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger)
throws IOException {
writer.beginObject();
writer.name(JsonKeys.TRACE_ID);
traceId.serialize(writer, logger);
writer.name(JsonKeys.SPAN_ID);
spanId.serialize(writer, logger);
if (parentSpanId != null) {
writer.name(JsonKeys.PARENT_SPAN_ID);
parentSpanId.serialize(writer, logger);
}
writer.name(JsonKeys.OP).value(op);
if (description != null) {
writer.name(JsonKeys.DESCRIPTION).value(description);
}
if (status != null) {
writer.name(JsonKeys.STATUS).value(logger, status);
}
if (!tags.isEmpty()) {
writer.name(JsonKeys.TAGS).value(logger, tags);
}
if (unknown != null) {
for (String key : unknown.keySet()) {
Object value = unknown.get(key);
writer.name(key).value(logger, value);
}
}
writer.endObject();
}

@Nullable
@Override
public Map<String, Object> getUnknown() {
return unknown;
}

@Override
public void setUnknown(@Nullable Map<String, Object> unknown) {
this.unknown = unknown;
}

public static final class Deserializer implements JsonDeserializer<SpanContext> {
@SuppressWarnings("unchecked")
@Override
public @NotNull SpanContext deserialize(
@NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception {
reader.beginObject();
SentryId traceId = null;
SpanId spanId = null;
SpanId parentSpanId = null;
String op = null;
String description = null;
SpanStatus status = null;
Map<String, String> tags = null;

Map<String, Object> unknown = null;
do {
final String nextName = reader.nextName();
switch (nextName) {
case JsonKeys.TRACE_ID:
traceId = new SentryId.Deserializer().deserialize(reader, logger);
break;
case JsonKeys.SPAN_ID:
spanId = new SpanId.Deserializer().deserialize(reader, logger);
break;
case JsonKeys.PARENT_SPAN_ID:
parentSpanId = new SpanId.Deserializer().deserialize(reader, logger);
break;
case JsonKeys.OP:
op = reader.nextString();
break;
case JsonKeys.DESCRIPTION:
description = reader.nextString();
break;
case JsonKeys.STATUS:
if (reader.peek() != JsonToken.NULL) {
status = new SpanStatus.Deserializer().deserialize(reader, logger);
}
break;
case JsonKeys.TAGS:
tags =
CollectionUtils.newConcurrentHashMap(
(Map<String, String>) reader.nextObjectOrNull());
break;
default:
if (unknown == null) {
unknown = new ConcurrentHashMap<>();
}
reader.nextUnknown(logger, unknown, nextName);
break;
}
} while (reader.hasNext());

if (traceId == null) {
String message = "Missing required field \"" + JsonKeys.TRACE_ID + "\"";
Exception exception = new IllegalStateException(message);
logger.log(SentryLevel.ERROR, message, exception);
throw exception;
}

if (spanId == null) {
String message = "Missing required field \"" + JsonKeys.SPAN_ID + "\"";
Exception exception = new IllegalStateException(message);
logger.log(SentryLevel.ERROR, message, exception);
throw exception;
}

if (op == null) {
String message = "Missing required field \"" + JsonKeys.OP + "\"";
Exception exception = new IllegalStateException(message);
logger.log(SentryLevel.ERROR, message, exception);
throw exception;
}

SpanContext spanContext = new SpanContext(traceId, spanId, op, parentSpanId, null);
spanContext.setDescription(description);
spanContext.setStatus(status);
if (tags != null) {
spanContext.tags = tags;
}
spanContext.setUnknown(unknown);
reader.endObject();
return spanContext;
}
}
}
21 changes: 20 additions & 1 deletion sentry/src/main/java/io/sentry/SpanStatus.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.sentry;

import java.io.IOException;
import java.util.Locale;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public enum SpanStatus {
public enum SpanStatus implements JsonSerializable {
/** Not an error, returned on success. */
OK(200, 299),
/** The operation was cancelled, typically by the caller. */
Expand Down Expand Up @@ -100,4 +102,21 @@ public enum SpanStatus {
private boolean matches(int httpStatusCode) {
return httpStatusCode >= minHttpStatusCode && httpStatusCode <= maxHttpStatusCode;
}

// JsonSerializable

@Override
public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger)
throws IOException {
writer.value(name().toLowerCase(Locale.ROOT));
}

public static final class Deserializer implements JsonDeserializer<SpanStatus> {

@Override
public @NotNull SpanStatus deserialize(
@NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception {
return SpanStatus.valueOf(reader.nextString().toUpperCase(Locale.ROOT));
}
}
}
31 changes: 31 additions & 0 deletions sentry/src/test/java/io/sentry/JsonUnknownSerializationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class JsonUnknownSerializationTest {
fun getGpu() = givenJsonUnknown(Gpu())
fun getOperatingSystem() = givenJsonUnknown(OperatingSystem())
fun getSentryRuntime() = givenJsonUnknown(SentryRuntime())
fun getSpanContext(): SpanContext {
val operation = "c2fb8fee2e2b49758bcb67cda0f713c7"
return givenJsonUnknown(SpanContext(operation))
}
fun getUserFeedback(): UserFeedback {
val eventId = SentryId("c2fb8fee2e2b49758bcb67cda0f713c7")
return givenJsonUnknown(UserFeedback(eventId))
Expand Down Expand Up @@ -183,6 +187,7 @@ class JsonUnknownSerializationTest {
@Test
fun `serializing unknown calls json object writer for sentry runtime`() {
val writer: JsonObjectWriter = mock()
whenever(writer.name(any())).thenReturn(writer)
val logger: ILogger = mock()
val sut = fixture.getUserFeedback()

Expand All @@ -192,6 +197,31 @@ class JsonUnknownSerializationTest {
verify(writer).value(logger, "fixture-value")
}

// SpanContext

@Test
fun `serializing and deserialize span context`() {
val sut = fixture.getSpanContext()

val serialized = serialize(sut)
val deserialized = deserialize(serialized, SpanContext.Deserializer())

assertEquals(sut.unknown, deserialized.unknown)
}

@Test
fun `serializing unknown calls json object writer for span context`() {
val writer: JsonObjectWriter = mock()
whenever(writer.name(any())).thenReturn(writer)
val logger: ILogger = mock()
val sut = fixture.getSpanContext()

sut.serialize(writer, logger)

verify(writer).name("fixture-key")
verify(writer).value(logger, "fixture-value")
}

// UserFeedback

@Test
Expand All @@ -207,6 +237,7 @@ class JsonUnknownSerializationTest {
@Test
fun `serializing unknown calls json object writer for user feedback`() {
val writer: JsonObjectWriter = mock()
whenever(writer.name(any())).thenReturn(writer)
val logger: ILogger = mock()
val sut = fixture.getUserFeedback()

Expand Down
Loading