From 52cc4c8f829d2ee519038fdf0d197a61e4e0c847 Mon Sep 17 00:00:00 2001 From: Tolyan Korniltsev Date: Mon, 22 Jun 2026 13:03:29 +0700 Subject: [PATCH 1/4] feat: add otel.scope.name otel.scope.version process.runtime.name process.runtime.version labels --- agent/build.gradle | 9 +- .../pyroscope/javaagent/config/AppName.java | 14 ++ .../javaagent/impl/PyroscopeExporter.java | 37 ++++++ .../javaagent/impl/PyroscopeExporterTest.java | 123 ++++++++++++++++++ 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java diff --git a/agent/build.gradle b/agent/build.gradle index ffaf0f55..6b36b760 100644 --- a/agent/build.gradle +++ b/agent/build.gradle @@ -63,7 +63,8 @@ javadoc{ jar { manifest { attributes( - 'Premain-Class': 'io.pyroscope.javaagent.PyroscopeAgent' + 'Premain-Class': 'io.pyroscope.javaagent.PyroscopeAgent', + 'Implementation-Version': pyroscopeVersion ) } from(asyncProfilerDistDir) { @@ -92,6 +93,12 @@ java { shadowJar { dependsOn copyBootstrapToResources + manifest { + attributes( + 'Premain-Class': 'io.pyroscope.javaagent.PyroscopeAgent', + 'Implementation-Version': pyroscopeVersion + ) + } exclude 'Log4j-*' exclude 'META-INF/org/apache/logging/log4j/**' diff --git a/agent/src/main/java/io/pyroscope/javaagent/config/AppName.java b/agent/src/main/java/io/pyroscope/javaagent/config/AppName.java index 82b2a496..3ff7d826 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/config/AppName.java +++ b/agent/src/main/java/io/pyroscope/javaagent/config/AppName.java @@ -62,6 +62,20 @@ public Builder addLabels(Map labels) { return this; } + public Builder addLabelsIfAbsent(Map labels) { + for (Map.Entry it : labels.entrySet()) { + addLabelIfAbsent(it.getKey(), it.getValue()); + } + return this; + } + + public Builder addLabelIfAbsent(String k, String v) { + if (!this.labels.containsKey(k)) { + addLabel(k, v); + } + return this; + } + public AppName build() { return new AppName(name, labels); } diff --git a/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java b/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java index 2aabe099..83498ce5 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java +++ b/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java @@ -12,12 +12,20 @@ import java.io.IOException; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import java.util.Random; import java.util.zip.Deflater; public class PyroscopeExporter implements Exporter { private static final MediaType PROTOBUF = MediaType.parse("application/x-protobuf"); + private static final String OTEL_SCOPE_NAME = "otel.scope.name"; + private static final String OTEL_SCOPE_VERSION = "otel.scope.version"; + private static final String PROCESS_RUNTIME_NAME = "process.runtime.name"; + private static final String PROCESS_RUNTIME_VERSION = "process.runtime.version"; + private static final String PYROSCOPE_SCOPE_NAME = "io.pyroscope.javaagent"; + private static final String UNKNOWN_SCOPE_VERSION = "unknown"; final Config config; final Logger logger; @@ -171,7 +179,36 @@ private String nameWithStaticLabels() { return config.timeseries.newBuilder() .addLabels(config.labels) .addLabels(Pyroscope.getStaticLabels()) + .addLabelsIfAbsent(otelRequiredLabels()) .build() .toString(); } + + private static Map otelRequiredLabels() { + Map labels = new HashMap<>(); + labels.put(OTEL_SCOPE_NAME, PYROSCOPE_SCOPE_NAME); + labels.put(OTEL_SCOPE_VERSION, scopeVersion()); + putSystemProperty(labels, PROCESS_RUNTIME_NAME, "java.runtime.name"); + putSystemProperty(labels, PROCESS_RUNTIME_VERSION, "java.runtime.version"); + return labels; + } + + private static void putSystemProperty(Map labels, String labelName, String propertyName) { + String propertyValue = System.getProperty(propertyName); + if (propertyValue != null && !propertyValue.isEmpty()) { + labels.put(labelName, propertyValue); + } + } + + private static String scopeVersion() { + Package packageInfo = PyroscopeExporter.class.getPackage(); + if (packageInfo == null) { + return UNKNOWN_SCOPE_VERSION; + } + String version = packageInfo.getImplementationVersion(); + if (version == null || version.isEmpty()) { + return UNKNOWN_SCOPE_VERSION; + } + return version; + } } diff --git a/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java b/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java new file mode 100644 index 00000000..cbbf1dba --- /dev/null +++ b/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java @@ -0,0 +1,123 @@ +package io.pyroscope.javaagent.impl; + +import io.pyroscope.javaagent.api.Logger; +import io.pyroscope.javaagent.config.Config; +import io.pyroscope.labels.v2.Pyroscope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class PyroscopeExporterTest { + private static final Logger NOOP_LOGGER = (level, msg, args) -> {}; + + @BeforeEach + @AfterEach + void resetStaticLabels() { + Pyroscope.setStaticLabels(Collections.emptyMap()); + } + + @Test + void addsRequiredOtelLabels() { + PyroscopeExporter exporter = new PyroscopeExporter( + new Config.Builder() + .setApplicationName("test.app") + .build(), + NOOP_LOGGER); + try { + Map labels = labelsFromSeriesName(exporter.staticLabels); + + assertEquals("io.pyroscope.javaagent", labels.get("otel.scope.name")); + assertFalse(labels.get("otel.scope.version").isEmpty()); + assertEquals(System.getProperty("java.runtime.name"), labels.get("process.runtime.name")); + assertEquals(System.getProperty("java.runtime.version"), labels.get("process.runtime.version")); + } finally { + exporter.stop(); + } + } + + @Test + void staticLabelsOverrideRequiredOtelDefaults() { + Pyroscope.setStaticLabels(mapOf( + "otel.scope.name", "staticScopeName", + "otel.scope.version", "staticScopeVersion", + "process.runtime.name", "staticRuntimeName", + "process.runtime.version", "staticRuntimeVersion")); + + PyroscopeExporter exporter = new PyroscopeExporter( + new Config.Builder() + .setApplicationName("test.app") + .build(), + NOOP_LOGGER); + try { + Map labels = labelsFromSeriesName(exporter.staticLabels); + + assertEquals("staticScopeName", labels.get("otel.scope.name")); + assertEquals("staticScopeVersion", labels.get("otel.scope.version")); + assertEquals("staticRuntimeName", labels.get("process.runtime.name")); + assertEquals("staticRuntimeVersion", labels.get("process.runtime.version")); + } finally { + exporter.stop(); + } + } + + @Test + void userLabelsOverrideRequiredOtelDefaults() { + Map configLabels = mapOf( + "process.runtime.name", "configRuntimeName", + "process.runtime.version", "configRuntimeVersion"); + Map staticLabels = mapOf( + "otel.scope.version", "staticScopeVersion"); + Pyroscope.setStaticLabels(staticLabels); + + PyroscopeExporter exporter = new PyroscopeExporter( + new Config.Builder() + .setApplicationName("test.app{otel.scope.name=appScopeName}") + .setLabels(configLabels) + .build(), + NOOP_LOGGER); + try { + Map labels = labelsFromSeriesName(exporter.staticLabels); + + assertEquals("appScopeName", labels.get("otel.scope.name")); + assertEquals("staticScopeVersion", labels.get("otel.scope.version")); + assertEquals("configRuntimeName", labels.get("process.runtime.name")); + assertEquals("configRuntimeVersion", labels.get("process.runtime.version")); + } finally { + exporter.stop(); + } + } + + private static Map labelsFromSeriesName(String seriesName) { + int labelsStart = seriesName.indexOf('{'); + int labelsEnd = seriesName.lastIndexOf('}'); + if (labelsStart == -1 || labelsEnd == -1 || labelsStart >= labelsEnd) { + return Collections.emptyMap(); + } + + Map labels = new HashMap<>(); + String labelsString = seriesName.substring(labelsStart + 1, labelsEnd); + for (String label : labelsString.split(",")) { + int separator = label.indexOf('='); + if (separator == -1) { + continue; + } + labels.put(label.substring(0, separator), label.substring(separator + 1)); + } + return labels; + } + + private static Map mapOf(String... pairs) { + Map labels = new HashMap<>(); + for (int i = 0; i < pairs.length; i += 2) { + labels.put(pairs[i], pairs[i + 1]); + } + return labels; + } +} From 641c3f49cf74669febd711ef0d98ed4c47891e66 Mon Sep 17 00:00:00 2001 From: Tolyan Korniltsev Date: Mon, 22 Jun 2026 13:19:33 +0700 Subject: [PATCH 2/4] fix --- .../javaagent/impl/PyroscopeExporter.java | 16 +++++++++------- .../javaagent/impl/PyroscopeExporterTest.java | 4 +++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java b/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java index 83498ce5..e5801f2c 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java +++ b/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java @@ -25,7 +25,6 @@ public class PyroscopeExporter implements Exporter { private static final String PROCESS_RUNTIME_NAME = "process.runtime.name"; private static final String PROCESS_RUNTIME_VERSION = "process.runtime.version"; private static final String PYROSCOPE_SCOPE_NAME = "io.pyroscope.javaagent"; - private static final String UNKNOWN_SCOPE_VERSION = "unknown"; final Config config; final Logger logger; @@ -187,27 +186,30 @@ private String nameWithStaticLabels() { private static Map otelRequiredLabels() { Map labels = new HashMap<>(); labels.put(OTEL_SCOPE_NAME, PYROSCOPE_SCOPE_NAME); - labels.put(OTEL_SCOPE_VERSION, scopeVersion()); + putLabelIfNotEmpty(labels, OTEL_SCOPE_VERSION, scopeVersion()); putSystemProperty(labels, PROCESS_RUNTIME_NAME, "java.runtime.name"); putSystemProperty(labels, PROCESS_RUNTIME_VERSION, "java.runtime.version"); return labels; } private static void putSystemProperty(Map labels, String labelName, String propertyName) { - String propertyValue = System.getProperty(propertyName); - if (propertyValue != null && !propertyValue.isEmpty()) { - labels.put(labelName, propertyValue); + putLabelIfNotEmpty(labels, labelName, System.getProperty(propertyName)); + } + + private static void putLabelIfNotEmpty(Map labels, String labelName, String labelValue) { + if (labelValue != null && !labelValue.isEmpty()) { + labels.put(labelName, labelValue); } } private static String scopeVersion() { Package packageInfo = PyroscopeExporter.class.getPackage(); if (packageInfo == null) { - return UNKNOWN_SCOPE_VERSION; + return null; } String version = packageInfo.getImplementationVersion(); if (version == null || version.isEmpty()) { - return UNKNOWN_SCOPE_VERSION; + return null; } return version; } diff --git a/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java b/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java index cbbf1dba..181be9c9 100644 --- a/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java +++ b/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java @@ -34,7 +34,9 @@ void addsRequiredOtelLabels() { Map labels = labelsFromSeriesName(exporter.staticLabels); assertEquals("io.pyroscope.javaagent", labels.get("otel.scope.name")); - assertFalse(labels.get("otel.scope.version").isEmpty()); + if (labels.containsKey("otel.scope.version")) { + assertFalse(labels.get("otel.scope.version").isEmpty()); + } assertEquals(System.getProperty("java.runtime.name"), labels.get("process.runtime.name")); assertEquals(System.getProperty("java.runtime.version"), labels.get("process.runtime.version")); } finally { From a6b99aad6267b359ee6a34bf5f2cb2a23216eb1f Mon Sep 17 00:00:00 2001 From: Tolyan Korniltsev Date: Mon, 22 Jun 2026 14:20:14 +0700 Subject: [PATCH 3/4] fix scope name --- .../java/io/pyroscope/javaagent/impl/PyroscopeExporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java b/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java index e5801f2c..565e55f7 100644 --- a/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java +++ b/agent/src/main/java/io/pyroscope/javaagent/impl/PyroscopeExporter.java @@ -24,7 +24,7 @@ public class PyroscopeExporter implements Exporter { private static final String OTEL_SCOPE_VERSION = "otel.scope.version"; private static final String PROCESS_RUNTIME_NAME = "process.runtime.name"; private static final String PROCESS_RUNTIME_VERSION = "process.runtime.version"; - private static final String PYROSCOPE_SCOPE_NAME = "io.pyroscope.javaagent"; + private static final String PYROSCOPE_SCOPE_NAME = "com.grafana.pyroscope/java"; final Config config; final Logger logger; From eaa72681044698a85aa0177739594ec53a77fac1 Mon Sep 17 00:00:00 2001 From: Tolyan Korniltsev Date: Mon, 22 Jun 2026 14:27:23 +0700 Subject: [PATCH 4/4] fix scope name --- .../java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java b/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java index 181be9c9..373a729e 100644 --- a/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java +++ b/agent/src/test/java/io/pyroscope/javaagent/impl/PyroscopeExporterTest.java @@ -33,7 +33,7 @@ void addsRequiredOtelLabels() { try { Map labels = labelsFromSeriesName(exporter.staticLabels); - assertEquals("io.pyroscope.javaagent", labels.get("otel.scope.name")); + assertEquals("com.grafana.pyroscope/java", labels.get("otel.scope.name")); if (labels.containsKey("otel.scope.version")) { assertFalse(labels.get("otel.scope.version").isEmpty()); }