From 687e885a79e472ad4cb85f8b79e78b942bcf7d2a Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:42:06 +0700 Subject: [PATCH 1/3] fix(exposition): export internal package for OSGi resolution prometheus-metrics-exposition-textformats imports io.prometheus.metrics.expositionformats.internal because PrometheusProtobufWriter loads the protobuf implementation via Class.forName, but the exposition-formats bundles kept that package private. Export it via bnd _exportcontents so OSGi can wire the two bundles together. Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- .../pom.xml | 15 +++++++++++++++ prometheus-metrics-exposition-formats/pom.xml | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/prometheus-metrics-exposition-formats-shaded/pom.xml b/prometheus-metrics-exposition-formats-shaded/pom.xml index f7e65bcb8..c29006334 100644 --- a/prometheus-metrics-exposition-formats-shaded/pom.xml +++ b/prometheus-metrics-exposition-formats-shaded/pom.xml @@ -61,6 +61,21 @@ + + org.apache.felix + maven-bundle-plugin + + + + + io.prometheus.metrics.expositionformats.generated*;version="${project.version}" + + <_exportcontents> + io.prometheus.metrics.expositionformats.internal;version="${project.version}" + + + + org.apache.maven.plugins maven-resources-plugin diff --git a/prometheus-metrics-exposition-formats/pom.xml b/prometheus-metrics-exposition-formats/pom.xml index 4f0202faf..26e2d350d 100644 --- a/prometheus-metrics-exposition-formats/pom.xml +++ b/prometheus-metrics-exposition-formats/pom.xml @@ -61,6 +61,21 @@ + + org.apache.felix + maven-bundle-plugin + + + + + io.prometheus.metrics.expositionformats.generated*;version="${project.version}" + + <_exportcontents> + io.prometheus.metrics.expositionformats.internal;version="${project.version}" + + + + org.codehaus.mojo build-helper-maven-plugin From bc2860ede7ee400168640ce9e9e01141ce16b498 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:46:37 +0700 Subject: [PATCH 2/3] test(exposition): assert OSGi export of internal package Lock the formats bundle Export-Package and textformats Import-Package headers so OSGi wiring of expositionformats.internal is regression-tested. Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- .../OsgiBundleManifestTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java new file mode 100644 index 000000000..5673cd6ac --- /dev/null +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java @@ -0,0 +1,94 @@ +package io.prometheus.metrics.expositionformats; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.CodeSource; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; + +class OsgiBundleManifestTest { + + private static final String INTERNAL_PACKAGE = + "io.prometheus.metrics.expositionformats.internal"; + + @Test + void formatsBundleExportsInternalPackage() throws Exception { + Manifest manifest = loadBundleManifest(PrometheusProtobufWriterImpl.class); + assertThat(bundleSymbolicName(manifest)).contains("exposition-formats"); + assertThat(packageNames(manifest, "Export-Package")).contains(INTERNAL_PACKAGE); + } + + @Test + void textformatsBundleImportsInternalPackage() throws Exception { + Manifest manifest = loadBundleManifest(PrometheusProtobufWriter.class); + assertThat(bundleSymbolicName(manifest)) + .isEqualTo("io.prometheus.metrics-exposition-textformats"); + assertThat(packageNames(manifest, "Import-Package")).contains(INTERNAL_PACKAGE); + } + + private static String bundleSymbolicName(Manifest manifest) { + return manifest.getMainAttributes().getValue("Bundle-SymbolicName"); + } + + private static Manifest loadBundleManifest(Class type) throws Exception { + CodeSource codeSource = type.getProtectionDomain().getCodeSource(); + assertThat(codeSource).as("code source for %s", type.getName()).isNotNull(); + URI location = codeSource.getLocation().toURI(); + Path path = Path.of(location); + if (Files.isDirectory(path)) { + Path manifestFile = path.resolve("META-INF/MANIFEST.MF"); + assertThat(Files.exists(manifestFile)) + .as("bnd MANIFEST.MF for %s at %s", type.getName(), manifestFile) + .isTrue(); + try (InputStream in = Files.newInputStream(manifestFile)) { + return new Manifest(in); + } + } + try (JarFile jar = new JarFile(path.toFile())) { + Manifest manifest = jar.getManifest(); + assertThat(manifest).as("MANIFEST.MF in %s", path).isNotNull(); + return manifest; + } + } + + /** OSGi headers are comma-separated clauses; attributes may contain quoted commas. */ + private static List packageNames(Manifest manifest, String header) { + String value = manifest.getMainAttributes().getValue(header); + assertThat(value).as("%s", header).isNotBlank(); + List names = new ArrayList<>(); + int i = 0; + int n = value.length(); + while (i < n) { + while (i < n && value.charAt(i) == ' ') { + i++; + } + int start = i; + while (i < n) { + char c = value.charAt(i); + if (c == ';' || c == ',') { + break; + } + i++; + } + names.add(value.substring(start, i).trim()); + boolean inQuote = false; + while (i < n) { + char c = value.charAt(i++); + if (c == '"') { + inQuote = !inQuote; + } else if (!inQuote && c == ',') { + break; + } + } + } + return names; + } +} From 0e7138abf0f7e866b4d701f645a3d9803a1a2d65 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:57:52 +0700 Subject: [PATCH 3/3] fix(exposition): drop SNAPSHOT OSGi versions and optional import Let bnd emit 1.8.1 package versions like sibling bundles. Exclude the phantom protobuf import from the shaded formats bundle, and mark the textformats internal import optional so a textformats-only install still resolves. Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- .../pom.xml | 17 ++- prometheus-metrics-exposition-formats/pom.xml | 14 +- .../OsgiBundleManifestTest.java | 142 ++++++++++++++---- .../pom.xml | 19 +++ 4 files changed, 158 insertions(+), 34 deletions(-) diff --git a/prometheus-metrics-exposition-formats-shaded/pom.xml b/prometheus-metrics-exposition-formats-shaded/pom.xml index c29006334..849000be6 100644 --- a/prometheus-metrics-exposition-formats-shaded/pom.xml +++ b/prometheus-metrics-exposition-formats-shaded/pom.xml @@ -66,15 +66,26 @@ maven-bundle-plugin - - io.prometheus.metrics.expositionformats.generated*;version="${project.version}" + io.prometheus.metrics.expositionformats.generated* + <_exportcontents> - io.prometheus.metrics.expositionformats.internal;version="${project.version}" + io.prometheus.metrics.expositionformats.internal + + !com.google.protobuf,* + + + + + + manifest + + + org.apache.maven.plugins diff --git a/prometheus-metrics-exposition-formats/pom.xml b/prometheus-metrics-exposition-formats/pom.xml index 26e2d350d..2f93f4cc8 100644 --- a/prometheus-metrics-exposition-formats/pom.xml +++ b/prometheus-metrics-exposition-formats/pom.xml @@ -66,15 +66,23 @@ maven-bundle-plugin - - io.prometheus.metrics.expositionformats.generated*;version="${project.version}" + io.prometheus.metrics.expositionformats.generated* + <_exportcontents> - io.prometheus.metrics.expositionformats.internal;version="${project.version}" + io.prometheus.metrics.expositionformats.internal + + + + + manifest + + + org.codehaus.mojo diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java index 5673cd6ac..6930a525b 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/OsgiBundleManifestTest.java @@ -9,7 +9,9 @@ import java.nio.file.Path; import java.security.CodeSource; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.jar.JarFile; import java.util.jar.Manifest; import org.junit.jupiter.api.Test; @@ -18,20 +20,48 @@ class OsgiBundleManifestTest { private static final String INTERNAL_PACKAGE = "io.prometheus.metrics.expositionformats.internal"; + private static final String GENERATED_PREFIX = + "io.prometheus.metrics.expositionformats.generated"; + private static final String SHADED_BSN = "io.prometheus.metrics-exposition-formats"; + private static final String TEXTFORMATS_BSN = "io.prometheus.metrics-exposition-textformats"; @Test void formatsBundleExportsInternalPackage() throws Exception { Manifest manifest = loadBundleManifest(PrometheusProtobufWriterImpl.class); assertThat(bundleSymbolicName(manifest)).contains("exposition-formats"); - assertThat(packageNames(manifest, "Export-Package")).contains(INTERNAL_PACKAGE); + List exported = parsePackageHeader(manifest, "Export-Package"); + assertThat(names(exported)).contains(INTERNAL_PACKAGE); + assertThat(exported).anyMatch(clause -> clause.name.startsWith(GENERATED_PREFIX)); + for (PackageClause clause : exported) { + if (!INTERNAL_PACKAGE.equals(clause.name) && !clause.name.startsWith(GENERATED_PREFIX)) { + continue; + } + assertThat(clause.attributes.get("version")) + .as("version of %s", clause.name) + .isNotBlank() + .doesNotContain(".SNAPSHOT") + .doesNotContain("-SNAPSHOT"); + } } @Test - void textformatsBundleImportsInternalPackage() throws Exception { + void textformatsBundleImportsInternalPackageOptionally() throws Exception { Manifest manifest = loadBundleManifest(PrometheusProtobufWriter.class); - assertThat(bundleSymbolicName(manifest)) - .isEqualTo("io.prometheus.metrics-exposition-textformats"); - assertThat(packageNames(manifest, "Import-Package")).contains(INTERNAL_PACKAGE); + assertThat(bundleSymbolicName(manifest)).isEqualTo(TEXTFORMATS_BSN); + PackageClause internal = + requireClause(parsePackageHeader(manifest, "Import-Package"), INTERNAL_PACKAGE); + assertThat(internal.directives.get("resolution")).isEqualTo("optional"); + } + + @Test + void protobufImportMatchesShading() throws Exception { + Manifest manifest = loadBundleManifest(PrometheusProtobufWriterImpl.class); + List imported = names(parsePackageHeader(manifest, "Import-Package")); + if (SHADED_BSN.equals(bundleSymbolicName(manifest))) { + assertThat(imported).doesNotContain("com.google.protobuf"); + } else { + assertThat(imported).contains("com.google.protobuf"); + } } private static String bundleSymbolicName(Manifest manifest) { @@ -59,36 +89,92 @@ private static Manifest loadBundleManifest(Class type) throws Exception { } } + private static PackageClause requireClause(List clauses, String packageName) { + return clauses.stream() + .filter(clause -> packageName.equals(clause.name)) + .findFirst() + .orElseThrow(() -> new AssertionError("missing package clause " + packageName)); + } + + private static List names(List clauses) { + List names = new ArrayList<>(); + for (PackageClause clause : clauses) { + names.add(clause.name); + } + return names; + } + /** OSGi headers are comma-separated clauses; attributes may contain quoted commas. */ - private static List packageNames(Manifest manifest, String header) { + private static List parsePackageHeader(Manifest manifest, String header) { String value = manifest.getMainAttributes().getValue(header); assertThat(value).as("%s", header).isNotBlank(); - List names = new ArrayList<>(); - int i = 0; - int n = value.length(); - while (i < n) { - while (i < n && value.charAt(i) == ' ') { - i++; + List clauses = new ArrayList<>(); + for (String rawClause : splitRespectingQuotes(value, ',')) { + List parts = splitRespectingQuotes(rawClause, ';'); + if (parts.isEmpty()) { + continue; } - int start = i; - while (i < n) { - char c = value.charAt(i); - if (c == ';' || c == ',') { - break; - } - i++; + String name = parts.get(0).trim(); + if (name.isEmpty()) { + continue; } - names.add(value.substring(start, i).trim()); - boolean inQuote = false; - while (i < n) { - char c = value.charAt(i++); - if (c == '"') { - inQuote = !inQuote; - } else if (!inQuote && c == ',') { - break; + Map attributes = new LinkedHashMap<>(); + Map directives = new LinkedHashMap<>(); + for (int i = 1; i < parts.size(); i++) { + String part = parts.get(i).trim(); + int directiveEq = part.indexOf(":="); + int attributeEq = part.indexOf('='); + if (directiveEq >= 0 && (attributeEq < 0 || directiveEq <= attributeEq)) { + directives.put( + part.substring(0, directiveEq).trim(), + unquote(part.substring(directiveEq + 2).trim())); + } else if (attributeEq >= 0) { + attributes.put( + part.substring(0, attributeEq).trim(), + unquote(part.substring(attributeEq + 1).trim())); } } + clauses.add(new PackageClause(name, attributes, directives)); + } + return clauses; + } + + private static List splitRespectingQuotes(String value, char separator) { + List parts = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuote = false; + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '"') { + inQuote = !inQuote; + current.append(c); + } else if (!inQuote && c == separator) { + parts.add(current.toString()); + current.setLength(0); + } else { + current.append(c); + } + } + parts.add(current.toString()); + return parts; + } + + private static String unquote(String value) { + if (value.length() >= 2 && value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') { + return value.substring(1, value.length() - 1); + } + return value; + } + + static final class PackageClause { + final String name; + final Map attributes; + final Map directives; + + PackageClause(String name, Map attributes, Map directives) { + this.name = name; + this.attributes = attributes; + this.directives = directives; } - return names; } } diff --git a/prometheus-metrics-exposition-textformats/pom.xml b/prometheus-metrics-exposition-textformats/pom.xml index 55a4d5375..2a7c2f4b8 100644 --- a/prometheus-metrics-exposition-textformats/pom.xml +++ b/prometheus-metrics-exposition-textformats/pom.xml @@ -42,6 +42,25 @@ + + org.apache.felix + maven-bundle-plugin + + + + io.prometheus.metrics.expositionformats.internal;resolution:=optional,* + + + + + + + + manifest + + + + org.apache.maven.plugins maven-jar-plugin