Skip to content

Commit 642dc0a

Browse files
feat: Adding a new feature which allows us to drop spans matching some criterion. (#122)
* feat: Adding a new feature which allows us to drop spans matching some criterion. This comes in handy when there is a lot of unnecessary data coming into hypertrace platform that's not really useful for API observability.
1 parent 2c39de4 commit 642dc0a

4 files changed

Lines changed: 147 additions & 11 deletions

File tree

span-normalizer/helm/templates/span-normalizer-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ data:
4949
{{- if hasKey .Values.spanNormalizerConfig.processor "defaultTenantId" }}
5050
defaultTenantId = "{{ .Values.spanNormalizerConfig.processor.defaultTenantId }}"
5151
{{- end }}
52+
53+
{{- if hasKey .Values.spanNormalizerConfig.processor "spanDropCriterion" }}
54+
spanDropCriterion = {{ .Values.spanNormalizerConfig.processor.spanDropCriterion | toJson }}
55+
{{- end }}
5256
}
5357
{{- end }}
5458

span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanNormalizer.java

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
import java.io.ByteArrayOutputStream;
1414
import java.io.IOException;
1515
import java.util.ArrayList;
16+
import java.util.Arrays;
1617
import java.util.Collections;
1718
import java.util.HashMap;
1819
import java.util.HashSet;
1920
import java.util.List;
2021
import java.util.Map;
22+
import java.util.Objects;
2123
import java.util.Optional;
2224
import java.util.Set;
2325
import java.util.concurrent.Callable;
@@ -33,6 +35,7 @@
3335
import org.apache.avro.specific.SpecificDatumWriter;
3436
import org.apache.avro.specific.SpecificRecordBase;
3537
import org.apache.commons.lang3.StringUtils;
38+
import org.apache.commons.lang3.tuple.Pair;
3639
import org.hypertrace.core.datamodel.AttributeValue;
3740
import org.hypertrace.core.datamodel.Attributes;
3841
import org.hypertrace.core.datamodel.Event;
@@ -71,13 +74,27 @@ public class JaegerSpanNormalizer implements SpanNormalizer<Span, RawSpan> {
7174
private static final String DEFAULT_TENANT_ID_CONFIG = "processor.defaultTenantId";
7275
// list of tenant ids to exclude
7376
private static final String TENANT_IDS_TO_EXCLUDE_CONFIG = "processor.excludeTenantIds";
74-
private final List<String> tenantIdsToExclude;
77+
78+
/**
79+
* Config key using which a list of criterion can be specified to drop the matching spans.
80+
* Any span matching any one of the criterion is dropped. Each criteria is a comma separated
81+
* list of key:value pairs and multiple pairs in one criteria are AND'ed.
82+
*
83+
* For example: ["messaging.destination_kind:queue,messaging.operation:receive,messaging.system:jms"]
84+
* drops all spans which have all 3 attribute:value pairs.
85+
*/
86+
private static final String SPAN_DROP_CRITERION_CONFIG = "processor.spanDropCriterion";
87+
88+
private static final String COMMA = ",";
89+
private static final String COLON = ":";
7590

7691
private static final String SPAN_NORMALIZATION_TIME_METRIC = "span.normalization.time";
7792

7893
private static JaegerSpanNormalizer INSTANCE;
7994
private final FieldsGenerator fieldsGenerator;
8095
private final TenantIdProvider tenantIdProvider;
96+
private final List<String> tenantIdsToExclude;
97+
private final List<List<Pair<String, String>>> spanDropCriterion;
8198
private final ConcurrentMap<String, Timer> tenantToSpanNormalizationTimer = new ConcurrentHashMap<>();
8299

83100
public static JaegerSpanNormalizer get(Config config) {
@@ -139,6 +156,32 @@ public JaegerSpanNormalizer(Config config) {
139156
if (!this.tenantIdsToExclude.isEmpty()) {
140157
LOG.info("list of tenant ids to exclude : {}", this.tenantIdsToExclude);
141158
}
159+
160+
List<String> criterion = config.hasPath(SPAN_DROP_CRITERION_CONFIG)
161+
? config.getStringList(SPAN_DROP_CRITERION_CONFIG)
162+
: Collections.emptyList();
163+
164+
// Parse the config to see if there is any criteria to drop spans.
165+
this.spanDropCriterion = criterion.stream()
166+
// Split each criteria based on comma
167+
.map(s -> s.split(COMMA))
168+
.map(a -> Arrays.stream(a).map(this::convertToPair).filter(Objects::nonNull).collect(Collectors.toList()))
169+
.collect(Collectors.toList());
170+
171+
if (!this.spanDropCriterion.isEmpty()) {
172+
LOG.info("Span drop criterion: {}", this.spanDropCriterion);
173+
}
174+
}
175+
176+
@Nullable
177+
private Pair<String, String> convertToPair(String s) {
178+
if (s != null && s.contains(COLON)) {
179+
String[] parts = s.split(COLON);
180+
if (parts.length == 2) {
181+
return Pair.of(parts[0], parts[1]);
182+
}
183+
}
184+
return null;
142185
}
143186

144187
public Timer getSpanNormalizationTimer(String tenantId) {
@@ -166,6 +209,10 @@ public RawSpan convert(Span jaegerSpan) throws Exception {
166209
return null;
167210
}
168211

212+
if (!this.spanDropCriterion.isEmpty() && shouldDropSpan(tags)) {
213+
return null;
214+
}
215+
169216
// Record the time taken for converting the span, along with the tenant id tag.
170217
return tenantToSpanNormalizationTimer
171218
.computeIfAbsent(
@@ -176,6 +223,17 @@ public RawSpan convert(Span jaegerSpan) throws Exception {
176223
.recordCallable(getRawSpanNormalizerCallable(jaegerSpan, tags, tenantId));
177224
}
178225

226+
/**
227+
* Method to check if the given span attributes match any of the drop criterion. Returns true if the span should be
228+
* dropped, false otherwise.
229+
*/
230+
private boolean shouldDropSpan(Map<String, KeyValue> tags) {
231+
return this.spanDropCriterion.stream()
232+
.anyMatch(l -> l.stream()
233+
.allMatch(p -> tags.containsKey(p.getLeft())
234+
&& StringUtils.equals(tags.get(p.getLeft()).getVStr(), p.getRight())));
235+
}
236+
179237
@Nonnull
180238
private Callable<RawSpan> getRawSpanNormalizerCallable(Span jaegerSpan,
181239
Map<String, KeyValue> spanTags, String tenantId) {
@@ -220,7 +278,7 @@ private Event buildEvent(
220278

221279
// SPAN REFS
222280
List<JaegerSpanInternalModel.SpanRef> referencesList = jaegerSpan.getReferencesList();
223-
if (referencesList != null && referencesList.size() > 0) {
281+
if (referencesList.size() > 0) {
224282
eventBuilder.setEventRefList(new ArrayList<>());
225283
// Convert the reflist to a set to remove duplicate references. This has been observed in the
226284
// field.
@@ -272,13 +330,13 @@ private Event buildEvent(
272330

273331
// WARNINGS
274332
ProtocolStringList warningsList = jaegerSpan.getWarningsList();
275-
if (warningsList != null && warningsList.size() > 0) {
333+
if (warningsList.size() > 0) {
276334
jaegerFieldsBuilder.setWarnings(warningsList);
277335
}
278336
// LOGS - NOTE: This is modeled as a google.protobuf.Any
279337
List<JaegerSpanInternalModel.Log> logsList = jaegerSpan.getLogsList();
280338
List<String> eventLogsList = new ArrayList<>();
281-
if (logsList != null && logsList.size() > 0) {
339+
if (logsList.size() > 0) {
282340
for (Log log : logsList) {
283341
try {
284342
String json = JsonFormat.printer().omittingInsignificantWhitespace().print(log);
@@ -292,11 +350,11 @@ private Event buildEvent(
292350

293351
// Jaeger service name can come from either first class field in Span or the tag `jaeger.servicename`
294352
String serviceName =
295-
(jaegerSpan.getProcess() != null && !StringUtils.isEmpty(jaegerSpan.getProcess().getServiceName()))
353+
!StringUtils.isEmpty(jaegerSpan.getProcess().getServiceName())
296354
? jaegerSpan.getProcess().getServiceName()
297-
: (attributeFieldMap.containsKey(OLD_JAEGER_SERVICENAME_KEY)
355+
: attributeFieldMap.containsKey(OLD_JAEGER_SERVICENAME_KEY)
298356
? attributeFieldMap.get(OLD_JAEGER_SERVICENAME_KEY).getValue()
299-
: StringUtils.EMPTY);
357+
: StringUtils.EMPTY;
300358

301359
if (!StringUtils.isEmpty(serviceName)) {
302360
eventBuilder.setServiceName(serviceName);
@@ -337,7 +395,7 @@ public static <T extends SpecificRecordBase> String convertToJsonString(T object
337395
writer.write(object, encoder);
338396
encoder.flush();
339397
output.flush();
340-
return new String(output.toByteArray());
398+
return output.toString();
341399
}
342400
}
343401
}

span-normalizer/span-normalizer/src/main/java/org/hypertrace/core/spannormalizer/jaeger/JaegerSpanToAvroRawSpanTransformer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public class JaegerSpanToAvroRawSpanTransformer implements
3030
private static final String VALID_SPAN_RECEIVED_COUNT = "hypertrace.reported.spans.processed";
3131
private static final ConcurrentMap<String, Counter> tenantToSpanReceivedCount = new ConcurrentHashMap<>();
3232

33-
3433
private JaegerSpanNormalizer converter;
3534

3635
@Override

span-normalizer/span-normalizer/src/test/java/org/hypertrace/core/spannormalizer/JaegerSpanNormalizerTest.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
import com.typesafe.config.Config;
44
import com.typesafe.config.ConfigFactory;
5-
import io.jaegertracing.api_v2.JaegerSpanInternalModel;
65
import io.jaegertracing.api_v2.JaegerSpanInternalModel.KeyValue;
76
import io.jaegertracing.api_v2.JaegerSpanInternalModel.Process;
87
import io.jaegertracing.api_v2.JaegerSpanInternalModel.Span;
98
import io.micrometer.core.instrument.Timer;
10-
import java.io.File;
119
import java.io.IOException;
1210
import java.lang.reflect.Field;
1311
import java.util.HashMap;
@@ -160,6 +158,83 @@ public void testServiceNameAddedToEvent_excludeTenantIds() throws Exception {
160158
Assertions.assertNotNull(rawSpan2);
161159
}
162160

161+
@Test
162+
public void testSpanDropCriterion() throws Exception {
163+
String tenantId = "tenant-" + random.nextLong();
164+
Map<String, Object> configs = new HashMap<>(getCommonConfig());
165+
configs.putAll(
166+
Map.of(
167+
"processor",
168+
Map.of("tenantIdTagKey", "tenant-key",
169+
"spanDropCriterion", List.of("foo:bar,k1:v1"))));
170+
JaegerSpanNormalizer normalizer = JaegerSpanNormalizer.get(ConfigFactory.parseMap(configs));
171+
Process process = Process.newBuilder().setServiceName("testService").build();
172+
Span span1 =
173+
Span.newBuilder()
174+
.setProcess(process)
175+
.addTags(KeyValue.newBuilder().setKey("tenant-key").setVStr(tenantId).build())
176+
.addTags(KeyValue.newBuilder().setKey("foo").setVStr("bar").build())
177+
.build();
178+
RawSpan rawSpan1 = normalizer.convert(span1);
179+
Assertions.assertNotNull(rawSpan1);
180+
181+
Span span2 =
182+
Span.newBuilder()
183+
.setProcess(process)
184+
.addTags(KeyValue.newBuilder().setKey("tenant-key").setVStr(tenantId).build())
185+
.addTags(KeyValue.newBuilder().setKey("foo").setVStr("bar").build())
186+
.addTags(KeyValue.newBuilder().setKey("k1").setVStr("v1").build())
187+
.build();
188+
RawSpan rawSpan2 = normalizer.convert(span2);
189+
Assertions.assertNull(rawSpan2);
190+
}
191+
192+
@Test
193+
public void testDropSpanWithMultipleCriterion() throws Exception {
194+
String tenantId = "tenant-" + random.nextLong();
195+
Map<String, Object> configs = new HashMap<>(getCommonConfig());
196+
configs.putAll(
197+
Map.of(
198+
"processor",
199+
Map.of("tenantIdTagKey", "tenant-key",
200+
"spanDropCriterion", List.of("foo:bar,k1:v1", "k2:v2"))));
201+
202+
JaegerSpanNormalizer normalizer = JaegerSpanNormalizer.get(ConfigFactory.parseMap(configs));
203+
Process process = Process.newBuilder().setServiceName("testService").build();
204+
Span span3 =
205+
Span.newBuilder()
206+
.setProcess(process)
207+
.addTags(KeyValue.newBuilder().setKey("tenant-key").setVStr(tenantId).build())
208+
.addTags(KeyValue.newBuilder().setKey("foo").setVStr("bar").build())
209+
.addTags(KeyValue.newBuilder().setKey("k2").setVStr("v2").build())
210+
.build();
211+
RawSpan rawSpan3 = normalizer.convert(span3);
212+
Assertions.assertNull(rawSpan3);
213+
}
214+
215+
@Test
216+
public void testDropSpanWithEmptyCriterion() throws Exception {
217+
String tenantId = "tenant-" + random.nextLong();
218+
Map<String, Object> configs = new HashMap<>(getCommonConfig());
219+
configs.putAll(
220+
Map.of(
221+
"processor",
222+
Map.of("tenantIdTagKey", "tenant-key",
223+
"spanDropCriterion", List.of())));
224+
225+
JaegerSpanNormalizer normalizer = JaegerSpanNormalizer.get(ConfigFactory.parseMap(configs));
226+
Process process = Process.newBuilder().setServiceName("testService").build();
227+
Span span3 =
228+
Span.newBuilder()
229+
.setProcess(process)
230+
.addTags(KeyValue.newBuilder().setKey("tenant-key").setVStr(tenantId).build())
231+
.addTags(KeyValue.newBuilder().setKey("foo").setVStr("bar").build())
232+
.addTags(KeyValue.newBuilder().setKey("k2").setVStr("v2").build())
233+
.build();
234+
RawSpan rawSpan3 = normalizer.convert(span3);
235+
Assertions.assertNotNull(rawSpan3);
236+
}
237+
163238
@Test
164239
public void testServiceNameAddedToEvent() throws Exception {
165240
String tenantId = "tenant-" + random.nextLong();

0 commit comments

Comments
 (0)