Filter metrics by scope (#14136)

This commit is contained in:
Jay DeLuca 2025-06-28 00:22:55 -04:00 committed by GitHub
parent 236f2fba17
commit 7689228d47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 572 additions and 186 deletions

View File

@ -2836,24 +2836,6 @@ libraries:
target_versions:
javaagent:
- io.projectreactor.netty:reactor-netty:[0.8.2.RELEASE,1.0.0)
telemetry:
- when: default
metrics:
- name: http.client.request.duration
description: Duration of HTTP client requests.
type: HISTOGRAM
unit: s
attributes:
- name: http.request.method
type: STRING
- name: http.response.status_code
type: LONG
- name: network.protocol.version
type: STRING
- name: server.address
type: STRING
- name: server.port
type: LONG
- name: reactor-netty-1.0
source_path: instrumentation/reactor/reactor-netty/reactor-netty-1.0
scope:
@ -3214,21 +3196,6 @@ libraries:
type: STRING
- name: server.port
type: LONG
- name: http.server.request.duration
description: Duration of HTTP server requests.
type: HISTOGRAM
unit: s
attributes:
- name: http.request.method
type: STRING
- name: http.response.status_code
type: LONG
- name: http.route
type: STRING
- name: network.protocol.version
type: STRING
- name: url.scheme
type: STRING
- name: spring-webflux-5.3
source_path: instrumentation/spring/spring-webflux/spring-webflux-5.3
scope:

View File

@ -7,19 +7,17 @@ package io.opentelemetry.instrumentation.docs;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics;
import io.opentelemetry.instrumentation.docs.internal.InstrumentationMetaData;
import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule;
import io.opentelemetry.instrumentation.docs.internal.InstrumentationType;
import io.opentelemetry.instrumentation.docs.parsers.EmittedMetricsParser;
import io.opentelemetry.instrumentation.docs.parsers.GradleParser;
import io.opentelemetry.instrumentation.docs.parsers.MetricParser;
import io.opentelemetry.instrumentation.docs.parsers.ModuleParser;
import io.opentelemetry.instrumentation.docs.parsers.SpanParser;
import io.opentelemetry.instrumentation.docs.utils.FileManager;
import io.opentelemetry.instrumentation.docs.utils.InstrumentationPath;
import io.opentelemetry.instrumentation.docs.utils.YamlHelper;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -65,7 +63,7 @@ class InstrumentationAnalyzer {
}
module.setTargetVersions(getVersionInformation(module));
module.setMetrics(MetricsProcessor.getMetrics(module, fileManager));
module.setMetrics(MetricParser.getMetrics(module, fileManager));
module.setSpans(SpanParser.getSpans(module, fileManager));
}
@ -89,26 +87,4 @@ class InstrumentationAnalyzer {
List<String> gradleFiles = fileManager.findBuildGradleFiles(module.getSrcPath());
return GradleParser.extractVersions(gradleFiles, module);
}
/** Handles processing of metrics data for instrumentation modules. */
static class MetricsProcessor {
public static Map<String, List<EmittedMetrics.Metric>> getMetrics(
InstrumentationModule module, FileManager fileManager) {
Map<String, EmittedMetrics> metrics =
EmittedMetricsParser.getMetricsFromFiles(fileManager.rootDir(), module.getSrcPath());
Map<String, List<EmittedMetrics.Metric>> result = new HashMap<>();
metrics.entrySet().stream()
.filter(MetricsProcessor::hasValidMetrics)
.forEach(entry -> result.put(entry.getKey(), entry.getValue().getMetrics()));
return result;
}
private static boolean hasValidMetrics(Map.Entry<String, EmittedMetrics> entry) {
return entry.getValue() != null && entry.getValue().getMetrics() != null;
}
private MetricsProcessor() {}
}
}

View File

@ -5,6 +5,9 @@
package io.opentelemetry.instrumentation.docs.internal;
import static java.util.Collections.emptyList;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
@ -16,16 +19,18 @@ import java.util.List;
public class EmittedMetrics {
// Condition in which the metrics are emitted (ex: default, or configuration option names).
private String when;
private List<Metric> metrics;
@JsonProperty("metrics_by_scope")
private List<MetricsByScope> metricsByScope;
public EmittedMetrics() {
this.when = "";
this.metrics = new ArrayList<>();
this.metricsByScope = emptyList();
}
public EmittedMetrics(String when, List<Metric> metrics) {
this.when = "";
this.metrics = metrics;
public EmittedMetrics(String when, List<MetricsByScope> metricsByScope) {
this.when = when;
this.metricsByScope = metricsByScope;
}
public String getWhen() {
@ -36,12 +41,49 @@ public class EmittedMetrics {
this.when = when;
}
public List<Metric> getMetrics() {
return metrics;
@JsonProperty("metrics_by_scope")
public List<MetricsByScope> getMetricsByScope() {
return metricsByScope;
}
public void setMetrics(List<Metric> metrics) {
this.metrics = metrics;
@JsonProperty("metrics_by_scope")
public void setMetricsByScope(List<MetricsByScope> metricsByScope) {
this.metricsByScope = metricsByScope;
}
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public static class MetricsByScope {
private String scope;
private List<Metric> metrics;
public MetricsByScope(String scope, List<Metric> metrics) {
this.scope = scope;
this.metrics = metrics;
}
public MetricsByScope() {
this.scope = "";
this.metrics = new ArrayList<>();
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public List<Metric> getMetrics() {
return metrics;
}
public void setMetrics(List<Metric> metrics) {
this.metrics = metrics;
}
}
/**

View File

@ -5,6 +5,7 @@
package io.opentelemetry.instrumentation.docs.parsers;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics;
import io.opentelemetry.instrumentation.docs.utils.FileManager;
import io.opentelemetry.instrumentation.docs.utils.YamlHelper;
@ -35,9 +36,24 @@ public class EmittedMetricsParser {
*/
public static Map<String, EmittedMetrics> getMetricsFromFiles(
String rootDir, String instrumentationDirectory) {
Map<String, StringBuilder> metricsByWhen = new HashMap<>();
Path telemetryDir = Paths.get(rootDir + "/" + instrumentationDirectory, ".telemetry");
Map<String, List<EmittedMetrics.MetricsByScope>> metricsByWhen =
parseAllMetricFiles(telemetryDir);
return aggregateMetricsByScope(metricsByWhen);
}
/**
* Parses all metric files in the given .telemetry directory and returns a map where the key is
* the 'when' condition and the value is a list of metrics grouped by scope.
*
* @param telemetryDir the path to the .telemetry directory
* @return a map of 'when' to list of metrics by scope
*/
private static Map<String, List<EmittedMetrics.MetricsByScope>> parseAllMetricFiles(
Path telemetryDir) {
Map<String, List<EmittedMetrics.MetricsByScope>> metricsByWhen = new HashMap<>();
if (Files.exists(telemetryDir) && Files.isDirectory(telemetryDir)) {
try (Stream<Path> files = Files.list(telemetryDir)) {
files
@ -49,14 +65,21 @@ public class EmittedMetricsParser {
String when = content.substring(0, content.indexOf('\n'));
String whenKey = when.replace("when: ", "");
metricsByWhen.putIfAbsent(whenKey, new StringBuilder("metrics:\n"));
// Skip the metric label ("metrics:") so we can aggregate into one list
int metricsIndex = content.indexOf("metrics:\n");
int metricsIndex = content.indexOf("metrics_by_scope:");
if (metricsIndex != -1) {
String contentAfterMetrics =
content.substring(metricsIndex + "metrics:\n".length());
metricsByWhen.get(whenKey).append(contentAfterMetrics);
String yaml = "when: " + whenKey + "\n" + content.substring(metricsIndex);
EmittedMetrics parsed;
try {
parsed = YamlHelper.emittedMetricsParser(yaml);
} catch (Exception e) {
logger.severe(
"Error parsing metrics file (" + path + "): " + e.getMessage());
return;
}
if (parsed.getMetricsByScope() != null) {
metricsByWhen.putIfAbsent(whenKey, new ArrayList<>());
metricsByWhen.get(whenKey).addAll(parsed.getMetricsByScope());
}
}
}
});
@ -64,37 +87,80 @@ public class EmittedMetricsParser {
logger.severe("Error reading metrics files: " + e.getMessage());
}
}
return metricsByWhen;
}
return parseMetrics(metricsByWhen);
/**
* Aggregates metrics under the same scope for each 'when' condition, deduplicating metrics by
* name.
*
* @param metricsByWhen map of 'when' to list of metrics by scope
* @return a map of 'when' to aggregated EmittedMetrics
*/
private static Map<String, EmittedMetrics> aggregateMetricsByScope(
Map<String, List<EmittedMetrics.MetricsByScope>> metricsByWhen) {
Map<String, EmittedMetrics> result = new HashMap<>();
for (Map.Entry<String, List<EmittedMetrics.MetricsByScope>> entry : metricsByWhen.entrySet()) {
String when = entry.getKey();
List<EmittedMetrics.MetricsByScope> allScopes = entry.getValue();
Map<String, Map<String, EmittedMetrics.Metric>> metricsByScopeName = new HashMap<>();
for (EmittedMetrics.MetricsByScope scopeEntry : allScopes) {
String scope = scopeEntry.getScope();
metricsByScopeName.putIfAbsent(scope, new HashMap<>());
Map<String, EmittedMetrics.Metric> metricMap = metricsByScopeName.get(scope);
for (EmittedMetrics.Metric metric : scopeEntry.getMetrics()) {
metricMap.put(metric.getName(), metric); // deduplicate by name
}
}
List<EmittedMetrics.MetricsByScope> mergedScopes = new ArrayList<>();
for (Map.Entry<String, Map<String, EmittedMetrics.Metric>> scopeEntry :
metricsByScopeName.entrySet()) {
mergedScopes.add(
new EmittedMetrics.MetricsByScope(
scopeEntry.getKey(), new ArrayList<>(scopeEntry.getValue().values())));
}
result.put(when, new EmittedMetrics(when, mergedScopes));
}
return result;
}
/**
* Takes in a raw string representation of the aggregated EmittedMetrics yaml map, separated by
* the `when`, indicating the conditions under which the metrics are emitted. deduplicates the
* metrics by name and then returns a new map of EmittedMetrics objects.
* the {@code when}, indicating the conditions under which the metrics are emitted. Deduplicates
* the metrics by name and then returns a new map of EmittedMetrics objects.
*
* @param input raw string representation of EmittedMetrics yaml
* @return {@code Map<String, EmittedMetrics>} where the key is the `when` condition
* @return map where the key is the {@code when} condition and the value is the corresponding
* EmittedMetrics
* @throws JsonProcessingException if parsing fails
*/
// visible for testing
public static Map<String, EmittedMetrics> parseMetrics(Map<String, StringBuilder> input) {
public static Map<String, EmittedMetrics> parseMetrics(Map<String, StringBuilder> input)
throws JsonProcessingException {
Map<String, EmittedMetrics> metricsMap = new HashMap<>();
for (Map.Entry<String, StringBuilder> entry : input.entrySet()) {
String when = entry.getKey();
StringBuilder content = entry.getValue();
EmittedMetrics metrics = YamlHelper.emittedMetricsParser(content.toString());
if (metrics.getMetrics() == null) {
if (metrics.getMetricsByScope() == null) {
continue;
}
Map<String, EmittedMetrics.Metric> deduplicatedMetrics = new HashMap<>();
for (EmittedMetrics.Metric metric : metrics.getMetrics()) {
deduplicatedMetrics.put(metric.getName(), metric);
List<EmittedMetrics.MetricsByScope> deduplicatedScopes = new ArrayList<>();
for (EmittedMetrics.MetricsByScope scopeEntry : metrics.getMetricsByScope()) {
String scope = scopeEntry.getScope();
Map<String, EmittedMetrics.Metric> dedupedMetrics = new HashMap<>();
for (EmittedMetrics.Metric metric : scopeEntry.getMetrics()) {
dedupedMetrics.put(metric.getName(), metric);
}
deduplicatedScopes.add(
new EmittedMetrics.MetricsByScope(scope, new ArrayList<>(dedupedMetrics.values())));
}
List<EmittedMetrics.Metric> uniqueMetrics = new ArrayList<>(deduplicatedMetrics.values());
metricsMap.put(when, new EmittedMetrics(when, uniqueMetrics));
metricsMap.put(when, new EmittedMetrics(when, deduplicatedScopes));
}
return metricsMap;
}

View File

@ -0,0 +1,180 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.docs.parsers;
import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics;
import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule;
import io.opentelemetry.instrumentation.docs.internal.TelemetryAttribute;
import io.opentelemetry.instrumentation.docs.utils.FileManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This class is responsible for parsing metric files from the `.telemetry` directory of an
* instrumentation module and filtering them by scope.
*/
public class MetricParser {
/**
* Retrieves metrics for a given instrumentation module, filtered by scope.
*
* @param module the instrumentation module
* @param fileManager the file manager to use for file operations
* @return a map where the key is the 'when' condition and the value is a list of metrics
*/
public static Map<String, List<EmittedMetrics.Metric>> getMetrics(
InstrumentationModule module, FileManager fileManager) {
Map<String, EmittedMetrics> metrics =
EmittedMetricsParser.getMetricsFromFiles(fileManager.rootDir(), module.getSrcPath());
if (metrics.isEmpty()) {
return new HashMap<>();
}
String scopeName = module.getScopeInfo().getName();
return filterMetricsByScope(metrics, scopeName);
}
/**
* Filters metrics by scope and aggregates attributes for each metric kind.
*
* @param metricsByScope the map of metrics by scope
* @param scopeName the name of the scope to filter metrics for
* @return a map of filtered metrics by 'when'
*/
private static Map<String, List<EmittedMetrics.Metric>> filterMetricsByScope(
Map<String, EmittedMetrics> metricsByScope, String scopeName) {
Map<String, Map<String, MetricAggregator.AggregatedMetricInfo>> aggregatedMetrics =
new HashMap<>();
for (Map.Entry<String, EmittedMetrics> entry : metricsByScope.entrySet()) {
if (!hasValidMetrics(entry.getValue())) {
continue;
}
String when = entry.getValue().getWhen();
Map<String, Map<String, MetricAggregator.AggregatedMetricInfo>> result =
MetricAggregator.aggregateMetrics(when, entry.getValue(), scopeName);
// Merge result into aggregatedMetrics
for (Map.Entry<String, Map<String, MetricAggregator.AggregatedMetricInfo>> e :
result.entrySet()) {
String whenKey = e.getKey();
Map<String, MetricAggregator.AggregatedMetricInfo> metricMap =
aggregatedMetrics.computeIfAbsent(whenKey, k -> new HashMap<>());
for (Map.Entry<String, MetricAggregator.AggregatedMetricInfo> metricEntry :
e.getValue().entrySet()) {
String metricName = metricEntry.getKey();
MetricAggregator.AggregatedMetricInfo newInfo = metricEntry.getValue();
MetricAggregator.AggregatedMetricInfo existingInfo = metricMap.get(metricName);
if (existingInfo == null) {
metricMap.put(metricName, newInfo);
} else {
existingInfo.attributes.addAll(newInfo.attributes);
}
}
}
}
return MetricAggregator.buildFilteredMetrics(aggregatedMetrics);
}
private static boolean hasValidMetrics(EmittedMetrics metrics) {
return metrics != null && metrics.getMetricsByScope() != null;
}
/** Helper class to aggregate metrics by scope and name. */
static class MetricAggregator {
/**
* Aggregates metrics for a given 'when' condition, metrics object, and target scope name.
*
* @param when the 'when' condition
* @param metrics the EmittedMetrics object
* @param targetScopeName the scope name to filter by
* @return a map of aggregated metrics by 'when' and metric name
*/
public static Map<String, Map<String, AggregatedMetricInfo>> aggregateMetrics(
String when, EmittedMetrics metrics, String targetScopeName) {
Map<String, Map<String, AggregatedMetricInfo>> aggregatedMetrics = new HashMap<>();
Map<String, AggregatedMetricInfo> metricKindMap =
aggregatedMetrics.computeIfAbsent(when, k -> new HashMap<>());
for (EmittedMetrics.MetricsByScope metricsByScope : metrics.getMetricsByScope()) {
if (metricsByScope.getScope().equals(targetScopeName)) {
for (EmittedMetrics.Metric metric : metricsByScope.getMetrics()) {
AggregatedMetricInfo aggInfo =
metricKindMap.computeIfAbsent(
metric.getName(),
k ->
new AggregatedMetricInfo(
metric.getName(),
metric.getDescription(),
metric.getType(),
metric.getUnit()));
if (metric.getAttributes() != null) {
for (TelemetryAttribute attr : metric.getAttributes()) {
aggInfo.attributes.add(new TelemetryAttribute(attr.getName(), attr.getType()));
}
}
}
}
}
return aggregatedMetrics;
}
/**
* Builds a filtered metrics map from aggregated metrics.
*
* @param aggregatedMetrics the aggregated metrics map
* @return a map where the key is the 'when' condition and the value is a list of metrics
*/
public static Map<String, List<EmittedMetrics.Metric>> buildFilteredMetrics(
Map<String, Map<String, AggregatedMetricInfo>> aggregatedMetrics) {
Map<String, List<EmittedMetrics.Metric>> result = new HashMap<>();
for (Map.Entry<String, Map<String, AggregatedMetricInfo>> entry :
aggregatedMetrics.entrySet()) {
String when = entry.getKey();
List<EmittedMetrics.Metric> metrics = result.computeIfAbsent(when, k -> new ArrayList<>());
for (AggregatedMetricInfo aggInfo : entry.getValue().values()) {
metrics.add(
new EmittedMetrics.Metric(
aggInfo.name,
aggInfo.description,
aggInfo.type,
aggInfo.unit,
new ArrayList<>(aggInfo.attributes)));
}
}
return result;
}
/** Data class to hold aggregated metric information. */
static class AggregatedMetricInfo {
final String name;
final String description;
final String type;
final String unit;
final Set<TelemetryAttribute> attributes = new HashSet<>();
AggregatedMetricInfo(String name, String description, String type, String unit) {
this.name = name;
this.description = description;
this.type = type;
this.unit = unit;
}
}
private MetricAggregator() {}
}
private MetricParser() {}
}

View File

@ -291,8 +291,8 @@ public class YamlHelper {
return mapper.readValue(input, InstrumentationMetaData.class);
}
public static EmittedMetrics emittedMetricsParser(String input) {
return new Yaml().loadAs(input, EmittedMetrics.class);
public static EmittedMetrics emittedMetricsParser(String input) throws JsonProcessingException {
return mapper.readValue(input, EmittedMetrics.class);
}
public static EmittedSpans emittedSpansParser(String input) throws JsonProcessingException {

View File

@ -0,0 +1,128 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.docs.parsers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mockStatic;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics;
import io.opentelemetry.instrumentation.docs.utils.FileManager;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
@SuppressWarnings("NullAway")
class EmittedMetricsParserTest {
@Test
void parseMetricsDeduplicatesMetricsByName() throws JsonProcessingException {
String input =
"""
metrics_by_scope:
- scope: io.opentelemetry.alibaba-druid-1.0
metrics:
- name: metric1
type: counter
- name: metric1
type: counter
- name: metric2
type: gauge
""";
Map<String, StringBuilder> metricMap = new HashMap<>();
metricMap.put("default", new StringBuilder(input));
Map<String, EmittedMetrics> result = EmittedMetricsParser.parseMetrics(metricMap);
List<String> metricNames =
result.get("default").getMetricsByScope().get(0).getMetrics().stream()
.map(EmittedMetrics.Metric::getName)
.sorted()
.toList();
assertThat(metricNames).hasSize(2);
assertThat(metricNames).containsExactly("metric1", "metric2");
}
@Test
void parseMetricsHandlesEmptyInput() throws JsonProcessingException {
String input = "metrics_by_scope:\n";
Map<String, StringBuilder> metricMap = new HashMap<>();
metricMap.put("default", new StringBuilder(input));
Map<String, EmittedMetrics> result = EmittedMetricsParser.parseMetrics(metricMap);
assertThat(result).isEmpty();
}
@Test
void getMetricsFromFilesCombinesFilesCorrectly(@TempDir Path tempDir) throws IOException {
Path telemetryDir = Files.createDirectories(tempDir.resolve(".telemetry"));
String file1Content =
"""
when: default
metrics_by_scope:
- scope: io.opentelemetry.MetricParserTest
metrics:
- name: metric1
type: counter
""";
String file2Content =
"""
when: default
metrics_by_scope:
- scope: io.opentelemetry.MetricParserTest
metrics:
- name: metric2
type: gauge
""";
Files.writeString(telemetryDir.resolve("metrics-1.yaml"), file1Content);
Files.writeString(telemetryDir.resolve("metrics-2.yaml"), file2Content);
// Create a non-metrics file that should be ignored
Files.writeString(telemetryDir.resolve("other-file.yaml"), "some content");
try (MockedStatic<FileManager> fileManagerMock = mockStatic(FileManager.class)) {
fileManagerMock
.when(
() -> FileManager.readFileToString(telemetryDir.resolve("metrics-1.yaml").toString()))
.thenReturn(file1Content);
fileManagerMock
.when(
() -> FileManager.readFileToString(telemetryDir.resolve("metrics-2.yaml").toString()))
.thenReturn(file2Content);
Map<String, EmittedMetrics> result =
EmittedMetricsParser.getMetricsFromFiles(tempDir.toString(), "");
EmittedMetrics.MetricsByScope metrics =
result.get("default").getMetricsByScope().stream()
.filter(scope -> scope.getScope().equals("io.opentelemetry.MetricParserTest"))
.findFirst()
.orElseThrow();
assertThat(metrics.getMetrics()).hasSize(2);
List<String> metricNames =
metrics.getMetrics().stream().map(EmittedMetrics.Metric::getName).sorted().toList();
assertThat(metricNames).containsExactly("metric1", "metric2");
}
}
@Test
void getMetricsFromFilesHandlesNonexistentDirectory() throws JsonProcessingException {
Map<String, EmittedMetrics> result =
EmittedMetricsParser.getMetricsFromFiles("/nonexistent", "path");
assertThat(result).isEmpty();
}
}

View File

@ -6,99 +6,110 @@
package io.opentelemetry.instrumentation.docs.parsers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mockStatic;
import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics;
import io.opentelemetry.instrumentation.docs.utils.FileManager;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import io.opentelemetry.instrumentation.docs.internal.TelemetryAttribute;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
@SuppressWarnings("NullAway")
class MetricParserTest {
@Test
void parseMetricsDeduplicatesMetricsByName() {
String input =
"""
metrics:
- name: metric1
type: counter
- name: metric1
type: counter
- name: metric2
type: gauge
""";
void testFiltersMetricsByScope() {
String targetScopeName = "my-instrumentation-scope";
Map<String, StringBuilder> metricMap = new HashMap<>();
metricMap.put("default", new StringBuilder(input));
EmittedMetrics.Metric metric1 = createMetric("my.metric1", "desc1", "attr1");
EmittedMetrics.Metric otherMetric = createMetric("other.metric", "desc2", "other.attr");
Map<String, EmittedMetrics> result = EmittedMetricsParser.parseMetrics(metricMap);
List<String> metricNames =
result.get("default").getMetrics().stream()
.map(EmittedMetrics.Metric::getName)
.sorted()
.toList();
EmittedMetrics.MetricsByScope targetMetricsByScope =
new EmittedMetrics.MetricsByScope(targetScopeName, List.of(metric1));
assertThat(metricNames).hasSize(2);
assertThat(metricNames).containsExactly("metric1", "metric2");
EmittedMetrics.MetricsByScope otherMetricsByScope =
new EmittedMetrics.MetricsByScope("other-scope", List.of(otherMetric));
EmittedMetrics emittedMetrics =
new EmittedMetrics("default", List.of(targetMetricsByScope, otherMetricsByScope));
Map<String, Map<String, MetricParser.MetricAggregator.AggregatedMetricInfo>> metrics =
MetricParser.MetricAggregator.aggregateMetrics("default", emittedMetrics, targetScopeName);
Map<String, List<EmittedMetrics.Metric>> result =
MetricParser.MetricAggregator.buildFilteredMetrics(metrics);
assertThat(result.get("default")).hasSize(1);
assertThat(result.get("default").get(0).getName()).isEqualTo("my.metric1");
}
@Test
void parseMetricsHandlesEmptyInput() {
String input = "metrics:\n";
Map<String, StringBuilder> metricMap = new HashMap<>();
metricMap.put("default", new StringBuilder(input));
void testAggregatesAndDeduplicatesAttributes() {
String targetScopeName = "my-instrumentation-scope";
Map<String, EmittedMetrics> result = EmittedMetricsParser.parseMetrics(metricMap);
assertThat(result).isEmpty();
EmittedMetrics.Metric metric1 =
new EmittedMetrics.Metric(
"my.metric1",
"desc1",
"gauge",
"unit",
List.of(
new TelemetryAttribute("attr1", "STRING"),
new TelemetryAttribute("attr2", "STRING")));
EmittedMetrics.Metric metric1Dup =
new EmittedMetrics.Metric(
"my.metric1",
"desc1",
"gauge",
"unit",
List.of(
new TelemetryAttribute("attr1", "STRING"),
new TelemetryAttribute("attr3", "STRING")));
EmittedMetrics.MetricsByScope targetMetricsByScope =
new EmittedMetrics.MetricsByScope(targetScopeName, List.of(metric1, metric1Dup));
EmittedMetrics emittedMetrics = new EmittedMetrics("default", List.of(targetMetricsByScope));
Map<String, Map<String, MetricParser.MetricAggregator.AggregatedMetricInfo>> metrics =
MetricParser.MetricAggregator.aggregateMetrics("default", emittedMetrics, targetScopeName);
Map<String, List<EmittedMetrics.Metric>> result =
MetricParser.MetricAggregator.buildFilteredMetrics(metrics);
List<TelemetryAttribute> attrs = result.get("default").get(0).getAttributes();
assertThat(attrs).hasSize(3);
assertThat(attrs.stream().map(TelemetryAttribute::getName))
.containsExactlyInAnyOrder("attr1", "attr2", "attr3");
}
@Test
void getMetricsFromFilesCombinesFilesCorrectly(@TempDir Path tempDir) throws IOException {
Path telemetryDir = Files.createDirectories(tempDir.resolve(".telemetry"));
void testPreservesMetricMetadata() {
String targetScopeName = "my-instrumentation-scope";
String file1Content = "when: default\n metrics:\n - name: metric1\n type: counter\n";
String file2Content = "when: default\n metrics:\n - name: metric2\n type: gauge\n";
EmittedMetrics.Metric metric1 =
createMetric("my.metric1", "description of my.metric1", "attr1");
Files.writeString(telemetryDir.resolve("metrics-1.yaml"), file1Content);
Files.writeString(telemetryDir.resolve("metrics-2.yaml"), file2Content);
EmittedMetrics.MetricsByScope targetMetricsByScope =
new EmittedMetrics.MetricsByScope(targetScopeName, List.of(metric1));
// Create a non-metrics file that should be ignored
Files.writeString(telemetryDir.resolve("other-file.yaml"), "some content");
EmittedMetrics emittedMetrics = new EmittedMetrics("default", List.of(targetMetricsByScope));
try (MockedStatic<FileManager> fileManagerMock = mockStatic(FileManager.class)) {
fileManagerMock
.when(
() -> FileManager.readFileToString(telemetryDir.resolve("metrics-1.yaml").toString()))
.thenReturn(file1Content);
fileManagerMock
.when(
() -> FileManager.readFileToString(telemetryDir.resolve("metrics-2.yaml").toString()))
.thenReturn(file2Content);
Map<String, Map<String, MetricParser.MetricAggregator.AggregatedMetricInfo>> metrics =
MetricParser.MetricAggregator.aggregateMetrics("default", emittedMetrics, targetScopeName);
Map<String, EmittedMetrics> result =
EmittedMetricsParser.getMetricsFromFiles(tempDir.toString(), "");
Map<String, List<EmittedMetrics.Metric>> result =
MetricParser.MetricAggregator.buildFilteredMetrics(metrics);
EmittedMetrics metrics = result.get("default");
assertThat(metrics.getMetrics()).hasSize(2);
List<String> metricNames =
metrics.getMetrics().stream().map(EmittedMetrics.Metric::getName).sorted().toList();
assertThat(metricNames).containsExactly("metric1", "metric2");
}
EmittedMetrics.Metric foundMetric = result.get("default").get(0);
assertThat(foundMetric.getName()).isEqualTo("my.metric1");
assertThat(foundMetric.getDescription()).isEqualTo("description of my.metric1");
assertThat(foundMetric.getType()).isEqualTo("gauge");
assertThat(foundMetric.getUnit()).isEqualTo("unit");
}
@Test
void getMetricsFromFilesHandlesNonexistentDirectory() {
Map<String, EmittedMetrics> result =
EmittedMetricsParser.getMetricsFromFiles("/nonexistent", "path");
assertThat(result).isEmpty();
private static EmittedMetrics.Metric createMetric(
String name, String description, String attrName) {
return new EmittedMetrics.Metric(
name, description, "gauge", "unit", List.of(new TelemetryAttribute(attrName, "STRING")));
}
}

View File

@ -73,7 +73,7 @@ public final class AgentTestRunner extends InstrumentationTestRunner {
}
String path = Paths.get(resource.getPath()).toString();
MetaDataCollector.writeTelemetryToFiles(path, metrics, tracesByScope);
MetaDataCollector.writeTelemetryToFiles(path, metricsByScope, tracesByScope);
}
// additional library ignores are ignored during tests, because they can make it really

View File

@ -51,7 +51,7 @@ import org.awaitility.core.ConditionTimeoutException;
public abstract class InstrumentationTestRunner {
private final TestInstrumenters testInstrumenters;
protected Map<String, MetricData> metrics = new HashMap<>();
protected Map<InstrumentationScopeInfo, Map<String, MetricData>> metricsByScope = new HashMap<>();
/**
* Stores traces by scope, where each scope contains a map of span kinds to a map of attribute
@ -205,8 +205,12 @@ public abstract class InstrumentationTestRunner {
private void collectEmittedMetrics(List<MetricData> metrics) {
for (MetricData metric : metrics) {
if (!this.metrics.containsKey(metric.getName())) {
this.metrics.put(metric.getName(), metric);
Map<String, MetricData> scopeMap =
this.metricsByScope.computeIfAbsent(
metric.getInstrumentationScopeInfo(), m -> new HashMap<>());
if (!scopeMap.containsKey(metric.getName())) {
scopeMap.put(metric.getName(), metric);
}
}
}

View File

@ -130,7 +130,7 @@ public final class LibraryTestRunner extends InstrumentationTestRunner {
}
String path = Paths.get(resource.getPath()).toString();
MetaDataCollector.writeTelemetryToFiles(path, metrics, tracesByScope);
MetaDataCollector.writeTelemetryToFiles(path, metricsByScope, tracesByScope);
}
}

View File

@ -39,13 +39,13 @@ public final class MetaDataCollector {
public static void writeTelemetryToFiles(
String path,
Map<String, MetricData> metrics,
Map<InstrumentationScopeInfo, Map<String, MetricData>> metricsByScope,
Map<InstrumentationScopeInfo, Map<SpanKind, Map<InternalAttributeKeyImpl<?>, AttributeType>>>
spansByScopeAndKind)
throws IOException {
String moduleRoot = extractInstrumentationPath(path);
writeMetricData(moduleRoot, metrics);
writeMetricData(moduleRoot, metricsByScope);
writeSpanData(moduleRoot, spansByScopeAndKind);
}
@ -125,10 +125,12 @@ public final class MetaDataCollector {
}
}
private static void writeMetricData(String instrumentationPath, Map<String, MetricData> metrics)
private static void writeMetricData(
String instrumentationPath,
Map<InstrumentationScopeInfo, Map<String, MetricData>> metricsByScope)
throws IOException {
if (metrics.isEmpty()) {
if (metricsByScope.isEmpty()) {
return;
}
@ -144,26 +146,36 @@ public final class MetaDataCollector {
writer.write("when: " + when + "\n");
writer.write("metrics:\n");
for (MetricData metric : metrics.values()) {
writer.write(" - name: " + metric.getName() + "\n");
writer.write(" description: " + metric.getDescription() + "\n");
writer.write(" type: " + metric.getType().toString() + "\n");
writer.write(" unit: " + sanitizeUnit(metric.getUnit()) + "\n");
writer.write(" attributes: \n");
metric.getData().getPoints().stream()
.findFirst()
.get()
.getAttributes()
.forEach(
(key, value) -> {
try {
writer.write(" - name: " + key.getKey() + "\n");
writer.write(" type: " + key.getType().toString() + "\n");
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
writer.write("metrics_by_scope:\n");
for (Map.Entry<InstrumentationScopeInfo, Map<String, MetricData>> entry :
metricsByScope.entrySet()) {
InstrumentationScopeInfo scope = entry.getKey();
Map<String, MetricData> metrics = entry.getValue();
writer.write(" - scope: " + scope.getName() + "\n");
writer.write(" metrics:\n");
for (MetricData metric : metrics.values()) {
writer.write(" - name: " + metric.getName() + "\n");
writer.write(" description: " + metric.getDescription() + "\n");
writer.write(" type: " + metric.getType().toString() + "\n");
writer.write(" unit: " + sanitizeUnit(metric.getUnit()) + "\n");
writer.write(" attributes: \n");
metric.getData().getPoints().stream()
.findFirst()
.get()
.getAttributes()
.forEach(
(key, value) -> {
try {
writer.write(" - name: " + key.getKey() + "\n");
writer.write(" type: " + key.getType().toString() + "\n");
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
}
}
}
}