Skip to content

Commit e449fa0

Browse files
committed
Add transport types for metrics
1 parent c5710d1 commit e449fa0

File tree

4 files changed

+424
-2
lines changed

4 files changed

+424
-2
lines changed

sentry/src/main/java/io/sentry/SentryEnvelopeItem.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,36 @@ public static SentryEnvelopeItem fromLogs(
546546
return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes());
547547
}
548548

549+
public static SentryEnvelopeItem fromMetrics(
550+
final @NotNull ISerializer serializer, final @NotNull SentryMetricsEvents metricsEvents) {
551+
Objects.requireNonNull(serializer, "ISerializer is required.");
552+
Objects.requireNonNull(metricsEvents, "SentryMetricsEvents is required.");
553+
554+
final CachedItem cachedItem =
555+
new CachedItem(
556+
() -> {
557+
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream();
558+
final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
559+
serializer.serialize(metricsEvents, writer);
560+
return stream.toByteArray();
561+
}
562+
});
563+
564+
SentryEnvelopeItemHeader itemHeader =
565+
new SentryEnvelopeItemHeader(
566+
SentryItemType.TraceMetric,
567+
() -> cachedItem.getBytes().length,
568+
"application/vnd.sentry.items.trace-metric+json",
569+
null,
570+
null,
571+
null,
572+
metricsEvents.getItems().size());
573+
574+
// avoid method refs on Android due to some issues with older AGP setups
575+
// noinspection Convert2MethodRef
576+
return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes());
577+
}
578+
549579
private static class CachedItem {
550580
private @Nullable byte[] bytes;
551581
private final @Nullable Callable<byte[]> dataFactory;
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
package io.sentry;
2+
3+
import static io.sentry.DateUtils.doubleToBigDecimal;
4+
5+
import org.jetbrains.annotations.NotNull;
6+
import org.jetbrains.annotations.Nullable;
7+
8+
import java.io.IOException;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
12+
import io.sentry.protocol.SentryId;
13+
import io.sentry.vendor.gson.stream.JsonToken;
14+
15+
public final class SentryMetricsEvent implements JsonUnknown, JsonSerializable {
16+
17+
private @NotNull SentryId traceId;
18+
private @Nullable SpanId spanId;
19+
/**
20+
* Timestamp in seconds (epoch time) indicating when the metric was recorded.
21+
*/
22+
private @NotNull Double timestamp;
23+
/**
24+
* The name of the metric.
25+
* This should follow a hierarchical naming convention using dots as separators
26+
* (e.g., api.response_time, db.query.duration).
27+
*/
28+
private @NotNull String name;
29+
/**
30+
* The unit of measurement for the metric value.
31+
*/
32+
private @Nullable String unit;
33+
/**
34+
* The type of metric. One of:
35+
* - counter: A metric that increments counts
36+
* - gauge: A metric that tracks a value that can go up or down
37+
* - distribution: A metric that tracks the statistical distribution of values
38+
*/
39+
private @NotNull String type;
40+
/**
41+
* The numeric value of the metric. The interpretation depends on the metric type:
42+
* - For counter metrics: the count to increment by (should default to 1)
43+
* - For gauge metrics: the current value
44+
* - For distribution metrics: a single measured value
45+
*/
46+
private @NotNull Double value;
47+
48+
private @Nullable Map<String, SentryLogEventAttributeValue> attributes;
49+
private @Nullable Map<String, Object> unknown;
50+
51+
public SentryMetricsEvent(
52+
final @NotNull SentryId traceId,
53+
final @NotNull SentryDate timestamp,
54+
final @NotNull String name,
55+
final @NotNull String type,
56+
final @NotNull Double value) {
57+
this(traceId, DateUtils.nanosToSeconds(timestamp.nanoTimestamp()), name, type, value);
58+
}
59+
60+
public SentryMetricsEvent(
61+
final @NotNull SentryId traceId,
62+
final @NotNull Double timestamp,
63+
final @NotNull String name,
64+
final @NotNull String type,
65+
final @NotNull Double value) {
66+
this.traceId = traceId;
67+
this.timestamp = timestamp;
68+
this.name = name;
69+
this.type = type;
70+
this.value = value;
71+
}
72+
73+
@NotNull
74+
public Double getTimestamp() {
75+
return timestamp;
76+
}
77+
78+
public void setTimestamp(final @NotNull Double timestamp) {
79+
this.timestamp = timestamp;
80+
}
81+
82+
public @NotNull String getName() {
83+
return name;
84+
}
85+
86+
public void setName(@NotNull String name) {
87+
this.name = name;
88+
}
89+
90+
public @NotNull String getType() {
91+
return type;
92+
}
93+
94+
public void setType(@NotNull String type) {
95+
this.type = type;
96+
}
97+
98+
public @Nullable String getUnit() {
99+
return unit;
100+
}
101+
102+
public void setUnit(@Nullable String unit) {
103+
this.unit = unit;
104+
}
105+
106+
public @Nullable SpanId getSpanId() {
107+
return spanId;
108+
}
109+
110+
public void setSpanId(@Nullable SpanId spanId) {
111+
this.spanId = spanId;
112+
}
113+
114+
public @NotNull Double getValue() {
115+
return value;
116+
}
117+
118+
public void setValue(@NotNull Double value) {
119+
this.value = value;
120+
}
121+
122+
public @Nullable Map<String, SentryLogEventAttributeValue> getAttributes() {
123+
return attributes;
124+
}
125+
126+
public void setAttributes(final @Nullable Map<String, SentryLogEventAttributeValue> attributes) {
127+
this.attributes = attributes;
128+
}
129+
130+
public void setAttribute(
131+
final @Nullable String key, final @Nullable SentryLogEventAttributeValue value) {
132+
if (key == null) {
133+
return;
134+
}
135+
if (this.attributes == null) {
136+
this.attributes = new HashMap<>();
137+
}
138+
this.attributes.put(key, value);
139+
}
140+
141+
// region json
142+
public static final class JsonKeys {
143+
public static final String TIMESTAMP = "timestamp";
144+
public static final String TRACE_ID = "trace_id";
145+
public static final String SPAN_ID = "span_id";
146+
public static final String NAME = "name";
147+
public static final String TYPE = "type";
148+
public static final String UNIT = "unit";
149+
public static final String VALUE = "value";
150+
public static final String ATTRIBUTES = "attributes";
151+
}
152+
153+
@Override
154+
@SuppressWarnings("JdkObsolete")
155+
public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger)
156+
throws IOException {
157+
writer.beginObject();
158+
writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp));
159+
writer.name(JsonKeys.TYPE).value(type);
160+
writer.name(JsonKeys.NAME).value(name);
161+
writer.name(JsonKeys.VALUE).value(value);
162+
writer.name(JsonKeys.TRACE_ID).value(logger, traceId);
163+
if (spanId != null) {
164+
writer.name(JsonKeys.SPAN_ID).value(logger, spanId);
165+
}
166+
if (unit != null) {
167+
writer.name(JsonKeys.UNIT).value(logger, unit);
168+
}
169+
if (attributes != null) {
170+
writer.name(JsonKeys.ATTRIBUTES).value(logger, attributes);
171+
}
172+
173+
if (unknown != null) {
174+
for (String key : unknown.keySet()) {
175+
Object value = unknown.get(key);
176+
writer.name(key).value(logger, value);
177+
}
178+
}
179+
writer.endObject();
180+
}
181+
182+
@Override
183+
public @Nullable Map<String, Object> getUnknown() {
184+
return unknown;
185+
}
186+
187+
@Override
188+
public void setUnknown(final @Nullable Map<String, Object> unknown) {
189+
this.unknown = unknown;
190+
}
191+
192+
public static final class Deserializer implements JsonDeserializer<SentryMetricsEvent> {
193+
194+
@SuppressWarnings("unchecked")
195+
@Override
196+
public @NotNull SentryMetricsEvent deserialize(
197+
final @NotNull ObjectReader reader, final @NotNull ILogger logger) throws Exception {
198+
@Nullable Map<String, Object> unknown = null;
199+
@Nullable SentryId traceId = null;
200+
@Nullable SpanId spanId = null;
201+
@Nullable Double timestamp = null;
202+
@Nullable String type = null;
203+
@Nullable String name = null;
204+
@Nullable String unit = null;
205+
@Nullable Double value = null;
206+
@Nullable Map<String, SentryLogEventAttributeValue> attributes = null;
207+
208+
reader.beginObject();
209+
while (reader.peek() == JsonToken.NAME) {
210+
final String nextName = reader.nextName();
211+
switch (nextName) {
212+
case JsonKeys.TRACE_ID:
213+
traceId = reader.nextOrNull(logger, new SentryId.Deserializer());
214+
break;
215+
case JsonKeys.SPAN_ID:
216+
spanId = reader.nextOrNull(logger, new SpanId.Deserializer());
217+
break;
218+
case JsonKeys.TIMESTAMP:
219+
timestamp = reader.nextDoubleOrNull();
220+
break;
221+
case JsonKeys.TYPE:
222+
type = reader.nextStringOrNull();
223+
break;
224+
case JsonKeys.NAME:
225+
name = reader.nextStringOrNull();
226+
break;
227+
case JsonKeys.UNIT:
228+
unit = reader.nextStringOrNull();
229+
break;
230+
case JsonKeys.VALUE:
231+
value = reader.nextDoubleOrNull();
232+
break;
233+
case JsonKeys.ATTRIBUTES:
234+
attributes =
235+
reader.nextMapOrNull(logger, new SentryLogEventAttributeValue.Deserializer());
236+
break;
237+
default:
238+
if (unknown == null) {
239+
unknown = new HashMap<>();
240+
}
241+
reader.nextUnknown(logger, unknown, nextName);
242+
break;
243+
}
244+
}
245+
reader.endObject();
246+
247+
if (traceId == null) {
248+
String message = "Missing required field \"" + JsonKeys.TRACE_ID + "\"";
249+
Exception exception = new IllegalStateException(message);
250+
logger.log(SentryLevel.ERROR, message, exception);
251+
throw exception;
252+
}
253+
254+
if (timestamp == null) {
255+
String message = "Missing required field \"" + JsonKeys.TIMESTAMP + "\"";
256+
Exception exception = new IllegalStateException(message);
257+
logger.log(SentryLevel.ERROR, message, exception);
258+
throw exception;
259+
}
260+
261+
if (type == null) {
262+
String message = "Missing required field \"" + JsonKeys.TYPE + "\"";
263+
Exception exception = new IllegalStateException(message);
264+
logger.log(SentryLevel.ERROR, message, exception);
265+
throw exception;
266+
}
267+
268+
if (name == null) {
269+
String message = "Missing required field \"" + JsonKeys.NAME + "\"";
270+
Exception exception = new IllegalStateException(message);
271+
logger.log(SentryLevel.ERROR, message, exception);
272+
throw exception;
273+
}
274+
275+
if (value == null) {
276+
String message = "Missing required field \"" + JsonKeys.VALUE + "\"";
277+
Exception exception = new IllegalStateException(message);
278+
logger.log(SentryLevel.ERROR, message, exception);
279+
throw exception;
280+
}
281+
282+
final SentryMetricsEvent logEvent = new SentryMetricsEvent(traceId, timestamp, name, type, value);
283+
284+
logEvent.setAttributes(attributes);
285+
logEvent.setSpanId(spanId);
286+
logEvent.setUnit(unit);
287+
logEvent.setUnknown(unknown);
288+
289+
return logEvent;
290+
}
291+
}
292+
// endregion json
293+
}

0 commit comments

Comments
 (0)