diff --git a/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/tracing/TracingSettings.java b/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/tracing/TracingSettings.java index e1529f7797..f174b99dc9 100644 --- a/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/tracing/TracingSettings.java +++ b/inspectit-ocelot-config/src/main/java/rocks/inspectit/ocelot/config/model/tracing/TracingSettings.java @@ -54,6 +54,12 @@ public enum AddCommonTags { @NotNull private AddCommonTags addCommonTags; + /** + * If enabled, metric tags will be added as attributes to tracing within the same rule + */ + @NotNull + private boolean addMetricTags; + /** * Settings for automatic tracing (stack trace sampling) */ @@ -79,7 +85,7 @@ public enum AddCommonTags { private long scheduleDelayMillis = 5000; /** - * I enabled 64 Bit Trace Ids are used instead of the default 128 Bit. + * If enabled, 64 Bit Trace Ids are used instead of the default 128 Bit. */ private boolean use64BitTraceIds = false; } diff --git a/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/basics.yml b/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/basics.yml index 1390100720..986fdfe67b 100644 --- a/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/basics.yml +++ b/inspectit-ocelot-config/src/main/resources/rocks/inspectit/ocelot/config/default/basics.yml @@ -36,6 +36,8 @@ inspectit: # defines when to add common tags as attributes to spans # options are: NEVER, ON_GLOBAL_ROOT, ON_LOCAL_ROOT, ALWAYS add-common-tags: ON_LOCAL_ROOT + # if enabled, metric tags will be added as attributes to tracing within the same rule + add-metric-tags: true # settings regarding automatic tracing (stack-trace-sampling) auto-tracing: frequency: 50ms diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGenerator.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGenerator.java index fcab375117..140613dbd0 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGenerator.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGenerator.java @@ -109,7 +109,7 @@ public MethodHook buildHook(Class declaringClass, MethodDescription method, M builder.exitActions(buildActionCalls(config.getPreExitActions(), methodInfo)); builder.exitActions(buildActionCalls(config.getExitActions(), methodInfo)); if (tracingSettings != null) { - List actions = buildTracingExitActions(tracingSettings); + List actions = buildTracingExitActions(config); if (isTracingInternalActions() && config.isTraceExitHook()) { actions = wrapActionsWithTracing(actions); } @@ -190,7 +190,8 @@ private void configureSampling(RuleTracingSettings tracing, ContinueOrStartSpanA } @VisibleForTesting - List buildTracingExitActions(RuleTracingSettings tracing) { + List buildTracingExitActions(MethodHookConfiguration config) { + RuleTracingSettings tracing = config.getTracing(); val result = new ArrayList(); boolean isSpanStartedOrContinued = tracing.getStartSpan() || StringUtils.isNotBlank(tracing.getContinueSpan()); @@ -201,10 +202,24 @@ List buildTracingExitActions(RuleTracingSettings tracing) { result.add(new SetSpanStatusAction(accessor)); } - val attributes = tracing.getAttributes(); - if (!attributes.isEmpty()) { + Map tracingAttributes = tracing.getAttributes(); + Map attributes = tracingAttributes; + Map constantAttributes = new HashMap<>(); + + if(addMetricsToTracing()) { + Collection metrics = config.getMetrics(); + constantAttributes = collectMetricConstantTags(metrics); + attributes = collectMetricDataTags(metrics); + // write tracing attributes after metric tags, to allow overwriting of metric tags + attributes.putAll(tracingAttributes); + } + + if (!attributes.isEmpty() || !constantAttributes.isEmpty()) { Map attributeAccessors = new HashMap<>(); + constantAttributes.forEach((attribute, constant) -> attributeAccessors.put(attribute, variableAccessorFactory.getConstantAccessor(constant))); + // if necessary, overwrite constant attributes attributes.forEach((attribute, variable) -> attributeAccessors.put(attribute, variableAccessorFactory.getVariableAccessor(variable))); + IHookAction endTraceAction = new WriteSpanAttributesAction(attributeAccessors, obfuscationManager.obfuscatorySupplier()); IHookAction actionWithConditions = ConditionalHookAction.wrapWithConditionChecks(tracing.getAttributeConditions(), endTraceAction, variableAccessorFactory); result.add(actionWithConditions); @@ -219,13 +234,24 @@ List buildTracingExitActions(RuleTracingSettings tracing) { return result; } + private Map collectMetricDataTags(Collection metrics) { + Map dataTags = new HashMap<>(); + metrics.forEach(metric -> dataTags.putAll(metric.getDataTags())); + return dataTags; + } + + private Map collectMetricConstantTags(Collection metrics) { + Map constantTags = new HashMap<>(); + metrics.forEach(metric -> constantTags.putAll(metric.getConstantTags())); + return constantTags; + } + private Optional buildMetricsRecorder(MethodHookConfiguration config) { Collection metricRecordingSettings = config.getMetrics(); if (!metricRecordingSettings.isEmpty()) { List metricAccessors = metricRecordingSettings.stream() .map(this::buildMetricAccessor) .collect(Collectors.toList()); - IHookAction recorder = new MetricsRecorder(metricAccessors, commonTagsManager, metricsManager, tagValueGuard); if (isTracingInternalActions() && config.isTraceExitHook()) { @@ -238,6 +264,13 @@ private Optional buildMetricsRecorder(MethodHookConfiguration confi } } + /** + * @return Returns whether metrics tags should be added to tracing as attributes + */ + private boolean addMetricsToTracing() { + return environment.getCurrentConfig().getTracing().isAddMetricTags(); + } + /** * @return Returns whether action tracing should be enabled for internal actions (e.g. {@link MetricsRecorder}). */ diff --git a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/WriteSpanAttributesAction.java b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/WriteSpanAttributesAction.java index ee81835255..17bf86519c 100644 --- a/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/WriteSpanAttributesAction.java +++ b/inspectit-ocelot-core/src/main/java/rocks/inspectit/ocelot/core/instrumentation/hook/actions/span/WriteSpanAttributesAction.java @@ -17,6 +17,7 @@ public class WriteSpanAttributesAction implements IHookAction { @Singular + @Getter private final Map attributeAccessors; private final Supplier obfuscatorySupplier; diff --git a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGeneratorTest.java b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGeneratorTest.java index 55fc7d24b5..5f643586ca 100644 --- a/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGeneratorTest.java +++ b/inspectit-ocelot-core/src/test/java/rocks/inspectit/ocelot/core/instrumentation/hook/MethodHookGeneratorTest.java @@ -1,6 +1,8 @@ package rocks.inspectit.ocelot.core.instrumentation.hook; +import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multiset; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import org.assertj.core.api.Assertions; @@ -190,7 +192,8 @@ public void verifyNoActionsGeneratedIfNoSpanStartedOrContinued() { .endSpan(true) .build(); - List actions = generator.buildTracingExitActions(settings); + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(settings).build(); + List actions = generator.buildTracingExitActions(config); assertThat(actions).isEmpty(); } @@ -205,7 +208,8 @@ public void verifyActionsGeneratedIfSpanStarted() { .endSpan(true) .build(); - List actions = generator.buildTracingExitActions(settings); + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(settings).build(); + List actions = generator.buildTracingExitActions(config); assertThat(actions) .hasSize(3) @@ -224,7 +228,8 @@ public void verifyActionsGeneratedIfSpanContinued() { .endSpan(true) .build(); - List actions = generator.buildTracingExitActions(settings); + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(settings).build(); + List actions = generator.buildTracingExitActions(config); assertThat(actions) .hasSize(3) @@ -234,4 +239,240 @@ public void verifyActionsGeneratedIfSpanContinued() { } } + + @Nested + class collectMetricTagsInTracing { + + private RuleTracingSettings createTracingSettings(Map attributes) { + return RuleTracingSettings.builder() + .startSpan(true) + .continueSpan(null) + .attributes(attributes) + .endSpan(true) + .build(); + } + + private MetricRecordingSettings createMetricSettings(Map dataTags, Map constantTags) { + return MetricRecordingSettings.builder().metric("name").value("1.0") + .dataTags(dataTags).constantTags(constantTags).build(); + } + + @Test + public void verifyTagsHaveBeenAdded() { + when(environment.getCurrentConfig().getTracing().isAddMetricTags()).thenReturn(true); + VariableAccessor tracingAccessor = (context) -> "tracing-text"; + VariableAccessor dataAccessor = (context) -> "data-text"; + VariableAccessor constantAccessor = (context) -> "constant-text"; + when(variableAccessorFactory.getVariableAccessor("tracing-value")).thenReturn(tracingAccessor); + when(variableAccessorFactory.getVariableAccessor("data-value")).thenReturn(dataAccessor); + when(variableAccessorFactory.getConstantAccessor(any())).thenReturn(constantAccessor); + Map expectedResult = new HashMap<>(); + expectedResult.put("tracing-key", tracingAccessor); + expectedResult.put("data-tag", dataAccessor); + expectedResult.put("constant-tag", constantAccessor); + + Map attributes = ImmutableMap.of("tracing-key", "tracing-value"); + RuleTracingSettings tracing = createTracingSettings(attributes); + + Map dataTags = ImmutableMap.of("data-tag", "data-value"); + Map constantTags = ImmutableMap.of("constant-tag", "constant-value"); + MetricRecordingSettings metric = createMetricSettings(dataTags, constantTags); + Multiset metrics = HashMultiset.create(); + metrics.add(metric); + + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(tracing).metrics(metrics).build(); + List actions = generator.buildTracingExitActions(config); + + Optional maybeAction = actions.stream().filter(action -> action instanceof WriteSpanAttributesAction).findFirst(); + assertThat(maybeAction.isPresent()).isTrue(); + WriteSpanAttributesAction attributeAction = (WriteSpanAttributesAction) maybeAction.get(); + + Map accessors = attributeAction.getAttributeAccessors(); + assertThat(accessors.size()).isEqualTo(expectedResult.size()); + assertThat(accessors).containsAllEntriesOf(expectedResult); + } + + @Test + public void verifyNoTagsHaveBeenAdded() { + when(environment.getCurrentConfig().getTracing().isAddMetricTags()).thenReturn(false); + VariableAccessor tracingAccessor = (context) -> "tracing-text"; + when(variableAccessorFactory.getVariableAccessor("tracing-value")).thenReturn(tracingAccessor); + Map expectedResult = new HashMap<>(); + expectedResult.put("tracing-key", tracingAccessor); + + Map attributes = ImmutableMap.of("tracing-key", "tracing-value"); + RuleTracingSettings tracing = createTracingSettings(attributes); + + Map dataTags = ImmutableMap.of("data-tag", "data-value"); + Map constantTags = ImmutableMap.of("constant-tag", "constant-value"); + MetricRecordingSettings metric = createMetricSettings(dataTags, constantTags); + Multiset metrics = HashMultiset.create(); + metrics.add(metric); + + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(tracing).metrics(metrics).build(); + List actions = generator.buildTracingExitActions(config); + + Optional maybeAction = actions.stream().filter(action -> action instanceof WriteSpanAttributesAction).findFirst(); + assertThat(maybeAction.isPresent()).isTrue(); + WriteSpanAttributesAction attributeAction = (WriteSpanAttributesAction) maybeAction.get(); + + Map accessors = attributeAction.getAttributeAccessors(); + assertThat(accessors.size()).isEqualTo(expectedResult.size()); + assertThat(accessors).containsAllEntriesOf(expectedResult); + } + + @Test + void verifyTracingOverwritesMetrics() { + when(environment.getCurrentConfig().getTracing().isAddMetricTags()).thenReturn(true); + VariableAccessor tracingAccessor = (context) -> "tracing-text"; + when(variableAccessorFactory.getVariableAccessor("tracing-value")).thenReturn(tracingAccessor); + Map expectedResult = new HashMap<>(); + expectedResult.put("one-key", tracingAccessor); + + Map attributes = ImmutableMap.of("one-key", "tracing-value"); + RuleTracingSettings tracing = createTracingSettings(attributes); + + Map dataTags = ImmutableMap.of("one-key", "data-value"); + Map constantTags = ImmutableMap.of("one-key", "constant-value"); + MetricRecordingSettings metric = createMetricSettings(dataTags, constantTags); + Multiset metrics = HashMultiset.create(); + metrics.add(metric); + + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(tracing).metrics(metrics).build(); + List actions = generator.buildTracingExitActions(config); + + Optional maybeAction = actions.stream().filter(action -> action instanceof WriteSpanAttributesAction).findFirst(); + assertThat(maybeAction.isPresent()).isTrue(); + WriteSpanAttributesAction attributeAction = (WriteSpanAttributesAction) maybeAction.get(); + + Map accessors = attributeAction.getAttributeAccessors(); + assertThat(accessors.size()).isEqualTo(expectedResult.size()); + assertThat(accessors).containsAllEntriesOf(expectedResult); + } + + @Test + void verifyDataTagsOverwriteConstantTags() { + when(environment.getCurrentConfig().getTracing().isAddMetricTags()).thenReturn(true); + VariableAccessor dataAccessor = (context) -> "data-text"; + when(variableAccessorFactory.getVariableAccessor("data-value")).thenReturn(dataAccessor); + Map expectedResult = new HashMap<>(); + expectedResult.put("one-key", dataAccessor); + + Map attributes = Collections.emptyMap(); + RuleTracingSettings tracing = createTracingSettings(attributes); + + Map dataTags = ImmutableMap.of("one-key", "data-value"); + Map constantTags = ImmutableMap.of("one-key", "constant-value"); + MetricRecordingSettings metric = createMetricSettings(dataTags, constantTags); + Multiset metrics = HashMultiset.create(); + metrics.add(metric); + + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(tracing).metrics(metrics).build(); + List actions = generator.buildTracingExitActions(config); + + Optional maybeAction = actions.stream().filter(action -> action instanceof WriteSpanAttributesAction).findFirst(); + assertThat(maybeAction.isPresent()).isTrue(); + WriteSpanAttributesAction attributeAction = (WriteSpanAttributesAction) maybeAction.get(); + + Map accessors = attributeAction.getAttributeAccessors(); + assertThat(accessors.size()).isEqualTo(expectedResult.size()); + assertThat(accessors).containsAllEntriesOf(expectedResult); + } + + @Test + void verifyOnlyDataTags() { + when(environment.getCurrentConfig().getTracing().isAddMetricTags()).thenReturn(true); + VariableAccessor dataAccessor = (context) -> "data-text"; + when(variableAccessorFactory.getVariableAccessor("data-value")).thenReturn(dataAccessor); + Map expectedResult = new HashMap<>(); + expectedResult.put("data-tag", dataAccessor); + + Map attributes = Collections.emptyMap(); + RuleTracingSettings tracing = createTracingSettings(attributes); + + Map dataTags = ImmutableMap.of("data-tag", "data-value"); + Map constantTags = Collections.emptyMap(); + MetricRecordingSettings metric = createMetricSettings(dataTags, constantTags); + Multiset metrics = HashMultiset.create(); + metrics.add(metric); + + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(tracing).metrics(metrics).build(); + List actions = generator.buildTracingExitActions(config); + + Optional maybeAction = actions.stream().filter(action -> action instanceof WriteSpanAttributesAction).findFirst(); + assertThat(maybeAction.isPresent()).isTrue(); + WriteSpanAttributesAction attributeAction = (WriteSpanAttributesAction) maybeAction.get(); + + Map accessors = attributeAction.getAttributeAccessors(); + assertThat(accessors.size()).isEqualTo(expectedResult.size()); + assertThat(accessors).containsAllEntriesOf(expectedResult); + } + + @Test + void verifyOnlyConstantTags() { + when(environment.getCurrentConfig().getTracing().isAddMetricTags()).thenReturn(true); + VariableAccessor constantAccessor = (context) -> "constant-text"; + when(variableAccessorFactory.getConstantAccessor(any())).thenReturn(constantAccessor); + Map expectedResult = new HashMap<>(); + expectedResult.put("constant-tag", constantAccessor); + + Map attributes = Collections.emptyMap(); + RuleTracingSettings tracing = createTracingSettings(attributes); + + Map dataTags = Collections.emptyMap(); + Map constantTags = ImmutableMap.of("constant-tag", "constant-value"); + MetricRecordingSettings metric = createMetricSettings(dataTags, constantTags); + Multiset metrics = HashMultiset.create(); + metrics.add(metric); + + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(tracing).metrics(metrics).build(); + List actions = generator.buildTracingExitActions(config); + + Optional maybeAction = actions.stream().filter(action -> action instanceof WriteSpanAttributesAction).findFirst(); + assertThat(maybeAction.isPresent()).isTrue(); + WriteSpanAttributesAction attributeAction = (WriteSpanAttributesAction) maybeAction.get(); + + Map accessors = attributeAction.getAttributeAccessors(); + assertThat(accessors.size()).isEqualTo(expectedResult.size()); + assertThat(accessors).containsAllEntriesOf(expectedResult); + } + + @Test + void verifyTagsOfMultipleMetrics() { + when(environment.getCurrentConfig().getTracing().isAddMetricTags()).thenReturn(true); + VariableAccessor tracingAccessor = (context) -> "tracing-text"; + VariableAccessor dataAccessor1 = (context) -> "data-text-1"; + VariableAccessor dataAccessor2 = (context) -> "data-text-2"; + when(variableAccessorFactory.getVariableAccessor("tracing-value")).thenReturn(tracingAccessor); + when(variableAccessorFactory.getVariableAccessor("data-value-1")).thenReturn(dataAccessor1); + when(variableAccessorFactory.getVariableAccessor("data-value-2")).thenReturn(dataAccessor2); + Map expectedResult = new HashMap<>(); + expectedResult.put("tracing-key", tracingAccessor); + expectedResult.put("data-tag-1", dataAccessor1); + expectedResult.put("data-tag-2", dataAccessor2); + + Map attributes = ImmutableMap.of("tracing-key", "tracing-value"); + RuleTracingSettings tracing = createTracingSettings(attributes); + + Map dataTags1 = ImmutableMap.of("data-tag-1", "data-value-1"); + Map dataTags2 = ImmutableMap.of("data-tag-2", "data-value-2"); + Map constantTags = Collections.emptyMap(); + MetricRecordingSettings metric1 = createMetricSettings(dataTags1, constantTags); + MetricRecordingSettings metric2 = createMetricSettings(dataTags2, constantTags); + Multiset metrics = HashMultiset.create(); + metrics.add(metric1); + metrics.add(metric2); + + MethodHookConfiguration config = MethodHookConfiguration.builder().tracing(tracing).metrics(metrics).build(); + List actions = generator.buildTracingExitActions(config); + + Optional maybeAction = actions.stream().filter(action -> action instanceof WriteSpanAttributesAction).findFirst(); + assertThat(maybeAction.isPresent()).isTrue(); + WriteSpanAttributesAction attributeAction = (WriteSpanAttributesAction) maybeAction.get(); + + Map accessors = attributeAction.getAttributeAccessors(); + assertThat(accessors.size()).isEqualTo(expectedResult.size()); + assertThat(accessors).containsAllEntriesOf(expectedResult); + } + } } diff --git a/inspectit-ocelot-documentation/docs/tracing/tracing.md b/inspectit-ocelot-documentation/docs/tracing/tracing.md index 01b5770e68..ff999e0dad 100644 --- a/inspectit-ocelot-documentation/docs/tracing/tracing.md +++ b/inspectit-ocelot-documentation/docs/tracing/tracing.md @@ -75,7 +75,7 @@ inspectit: propagation-format: B3 # the format for propagating correlation headers ``` -Currently the following formats are supported for sending correlation information: +Currently, the following formats are supported for sending correlation information: | Property | Format | Description |---|---|---| @@ -87,6 +87,61 @@ Currently the following formats are supported for sending correlation informatio It is important to note that this configuration refers to the format of the correlation information used to **send this data**. When processing correlation information that the agent receives, it automatically uses the correct format. ::: +### Adding Metric Tags + +It is possible to include all metrics tags of the current rule scope as tracing attributes. +This way it isn't necessary to define key-value pairs twice for metrics as well as tracing. +However, it is only possible to use metric tags as tracing attributes, but not vice versa! + +You can disable this feature in the tracing configuration: + +```YAML +inspectit: + tracing: + add-metric-tags: true +``` + +In this example, both tags of the metric `my_counter` will be used as attributes for the tracing within this rule. + +```YAML +rules: + 'r_example': + include: + 'r_myRule': true + entry: + 'my_data': + action: 'a_getData' + metrics: + my_counter: + value: 1 + data-tags: + 'example': 'my_data' + constant-tags: + 'scope': 'EXAMPLE' +``` + +Each tag key can only be used once within one trace. Thus, if a tag key has been assigned multiple values within one rule, +the acquired tag value will be determined hierarchically. Tag keys defined in `metrics.data-tags` will overwrite tag keys +defined in `metrics.constant-tags`. Tag keys defined in `tracing.attributes` will always overwrite tag keys defined in `metrics`. +In the example below, the tracing attributes will use 'trace' as value for 'myTag' and 'yourData' as value for 'yourTag'. + + +```YAML +rules: + 'r_example': + tracing: + attributes: + 'myTag': 'trace' + metrics: + my_counter: + data-tags: + 'myTag': 'myData' + 'yourTag': 'yourData' + constant-tags: + 'yourTag': 'CONSTANT' +``` + + ### Using 64-Bit Trace IDs Since version 2.0.0, the inspectIT Ocelot Agent is able to generate trace IDs with a size of 64 bits instead of the 128 bit trace IDs used by default by the agent. @@ -100,4 +155,4 @@ inspectit: :::important Please note that some propagation formats do not support 64-bit Ids, such as the W3C "Trace Context". In this case the 64-bit trace IDs are padded with leading zeros. -::: \ No newline at end of file +:::