Skip to content

Commit b8f9218

Browse files
authored
Merge pull request #4983 from getsentry/12-17-add_transport_types_for_metrics
feat(metrics): [Trace Metrics 4] Add transport types for metrics
2 parents 20376f7 + 14ea96a commit b8f9218

File tree

4 files changed

+419
-2
lines changed

4 files changed

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

0 commit comments

Comments
 (0)