\n"
+ " */\n"
+ )
+
+ for cls in (
+ "CounterBenchmark",
+ "HistogramBenchmark",
+ "TextFormatUtilBenchmark",
+ ):
+ fname = os.path.join(self.module_path, f"{cls}.java")
+ with open(fname, "w", encoding="utf-8") as f:
+ f.write(javadoc_pre)
+ f.write(f"public class {cls} {{}}\n")
+ self.files[cls] = fname
+
+ def tearDown(self):
+ self.tmpdir.cleanup()
+
+ def _read_pre_contents(self, path):
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+ m = re.search(r"
\n([\s\S]*?)
", content)
+ return m.group(1) if m else ""
+
+ def test_update_only_inserts_matching_class_lines(self):
+ updated = update_pre_blocks_under_module(self.module_path, self.table)
+ # All three files should be updated
+ self.assertEqual(
+ set(os.path.basename(p) for p in updated),
+ {
+ os.path.basename(self.files["CounterBenchmark"]),
+ os.path.basename(self.files["HistogramBenchmark"]),
+ os.path.basename(self.files["TextFormatUtilBenchmark"]),
+ },
+ )
+
+ # Verify CounterBenchmark file contains only CounterBenchmark lines
+ cb_pre = self._read_pre_contents(self.files["CounterBenchmark"])
+ self.assertIn("CounterBenchmark.codahaleIncNoLabels", cb_pre)
+ self.assertIn("CounterBenchmark.prometheusInc", cb_pre)
+ self.assertNotIn("HistogramBenchmark.prometheusNative", cb_pre)
+ self.assertNotIn("TextFormatUtilBenchmark.prometheusWriteToNull", cb_pre)
+
+ # Verify HistogramBenchmark contains only its line
+ hb_pre = self._read_pre_contents(self.files["HistogramBenchmark"])
+ self.assertIn("HistogramBenchmark.prometheusNative", hb_pre)
+ self.assertNotIn("CounterBenchmark.codahaleIncNoLabels", hb_pre)
+ self.assertNotIn("TextFormatUtilBenchmark.prometheusWriteToNull", hb_pre)
+
+ # Verify TextFormatUtilBenchmark contains only its line
+ tf_pre = self._read_pre_contents(self.files["TextFormatUtilBenchmark"])
+ self.assertIn("TextFormatUtilBenchmark.prometheusWriteToNull", tf_pre)
+ self.assertNotIn("CounterBenchmark.prometheusInc", tf_pre)
+ self.assertNotIn("HistogramBenchmark.prometheusNative", tf_pre)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.mise/tasks/update_benchmarks.py b/.mise/tasks/update_benchmarks.py
new file mode 100755
index 000000000..3cb550877
--- /dev/null
+++ b/.mise/tasks/update_benchmarks.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+
+# [MISE] description="Run and update JMH benchmark outputs in the benchmarks module"
+# [MISE] alias="update-benchmarks"
+
+"""
+Run benchmarks for the `benchmarks` module, capture JMH text output, and update
+any
...
blocks containing "thrpt" under the `benchmarks/` module
+(files such as Java sources with embedded example output in javadocs).
+
+Usage: ./.mise/tasks/update_benchmarks.py [--mvnw ./mvnw] [--module benchmarks] [--java java]
+ [--jmh-args "-f 1 -wi 0 -i 1"]
+
+By default this will:
+ - run the maven wrapper to package the benchmarks: `./mvnw -pl benchmarks -am -DskipTests package`
+ - locate the shaded jar under `benchmarks/target/` (named containing "benchmarks")
+ - run `java -jar -rf text` (add extra JMH args with --jmh-args)
+ - parse the first JMH table (the block starting with the "Benchmark Mode" header)
+ - update all files under the `benchmarks/` directory which contain a `
` block with the substring "thrpt"
+
+This script is careful to preserve Javadoc comment prefixes like " * " when replacing the
+contents of the
block.
+"""
+
+import argparse
+import glob
+import os
+import re
+import shlex
+import subprocess
+import sys
+from typing import List, Optional
+
+
+def run_cmd(cmd: List[str], cwd: Optional[str] = None) -> str:
+ """Run a command, stream stdout/stderr to the console for progress, and return the full output.
+
+ This replaces the previous blocking subprocess.run approach so users can see build / JMH
+ progress in real time while the command runs.
+ """
+ try:
+ proc = subprocess.Popen(
+ cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
+ )
+ except FileNotFoundError:
+ # Helpful message if the executable is not found
+ print(f"Command not found: {cmd[0]}")
+ raise
+
+ output_lines: List[str] = []
+ try:
+ assert proc.stdout is not None
+ # Stream lines as they appear and capture them for returning
+ for line in proc.stdout:
+ # Print immediately so callers (and CI) can observe progress
+ print(line, end="")
+ output_lines.append(line)
+ proc.wait()
+ except KeyboardInterrupt:
+ # If the user interrupts, ensure the child process is terminated
+ proc.kill()
+ proc.wait()
+ print("\nCommand interrupted by user.")
+ raise
+
+ output = "".join(output_lines)
+ if proc.returncode != 0:
+ print(
+ f"Command failed: {' '.join(cmd)}\nExit: {proc.returncode}\nOutput:\n{output}"
+ )
+ raise SystemExit(proc.returncode)
+ return output
+
+
+def build_benchmarks(mvnw: str, module: str) -> None:
+ print(f"Building Maven module '{module}' using {mvnw} (this may take a while)...")
+ cmd = [mvnw, "-pl", module, "-am", "-DskipTests", "clean", "package"]
+ run_cmd(cmd)
+ print("Build completed.")
+
+
+def find_benchmarks_jar(module: str) -> str:
+ pattern = os.path.join(module, "target", "*.jar")
+ jars = [p for p in glob.glob(pattern) if "original" not in p and p.endswith(".jar")]
+ # prefer jar whose basename contains module name
+ jars_pref = [j for j in jars if module in os.path.basename(j)]
+ chosen = (jars_pref or jars)[:1]
+ if not chosen:
+ raise FileNotFoundError(
+ f"No jar found in {os.path.join(module, 'target')} (tried: {pattern})"
+ )
+ jar = chosen[0]
+ print(f"Using jar: {jar}")
+ return jar
+
+
+def run_jmh(jar: str, java_cmd: str, extra_args: Optional[str]) -> str:
+ args = [java_cmd, "-jar", jar, "-rf", "text"]
+ if extra_args:
+ args += shlex.split(extra_args)
+ print(f"Running JMH: {' '.join(args)}")
+ output = run_cmd(args)
+ print("JMH run completed.")
+ return output
+
+
+def extract_first_table(jmh_output: str) -> str:
+ # Try to extract the first table that starts with "Benchmark" header and continues until a blank line
+ m = re.search(r"(\nBenchmark\s+Mode[\s\S]*?)(?:\n\s*\n|\Z)", jmh_output)
+ if not m:
+ # fallback: collect all lines that contain 'thrpt' plus a header if present
+ lines = [line for line in jmh_output.splitlines() if "thrpt" in line]
+ if not lines:
+ raise ValueError('Could not find any "thrpt" lines in JMH output')
+ # try to find header
+ header = next(
+ (
+ line
+ for line in jmh_output.splitlines()
+ if line.startswith("Benchmark") and "Mode" in line
+ ),
+ "Benchmark Mode Cnt Score Error Units",
+ )
+ return header + "\n" + "\n".join(lines)
+ table = m.group(1).strip("\n")
+ # Ensure we return only the table lines (remove any leading iteration info lines that JMH sometimes prints)
+ # Normalize spaces: keep as-is
+ return table
+
+
+def filter_table_for_class(table: str, class_name: str) -> Optional[str]:
+ """
+ Return a table string that contains only the header and the lines belonging to `class_name`.
+ If no matching lines are found, return None.
+ """
+ lines = table.splitlines()
+ # find header line index (starts with 'Benchmark' and contains 'Mode')
+ header_idx = None
+ for i, ln in enumerate(lines):
+ if ln.strip().startswith("Benchmark") and "Mode" in ln:
+ header_idx = i
+ break
+ header = (
+ lines[header_idx]
+ if header_idx is not None
+ else "Benchmark Mode Cnt Score Error Units"
+ )
+
+ matched = []
+ pattern = re.compile(r"^\s*" + re.escape(class_name) + r"\.")
+ for ln in lines[header_idx + 1 if header_idx is not None else 0 :]:
+ if "thrpt" in ln and pattern.search(ln):
+ matched.append(ln)
+
+ if not matched:
+ return None
+ return header + "\n" + "\n".join(matched)
+
+
+def update_pre_blocks_under_module(module: str, table: str) -> List[str]:
+ # Find files under module and update any
...
block that contains 'thrpt'
+ updated_files = []
+ for path in glob.glob(os.path.join(module, "**"), recursive=True):
+ if os.path.isdir(path):
+ continue
+ try:
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+ except Exception:
+ continue
+ # quick filter
+ if "
" not in content or "thrpt" not in content:
+ continue
+
+ original = content
+
+ # Determine the class name from the filename (e.g. TextFormatUtilBenchmark.java -> TextFormatUtilBenchmark)
+ base = os.path.basename(path)
+ class_name = os.path.splitext(base)[0]
+
+ # Build a filtered table for this class; if no matching lines, skip updating this file
+ filtered_table = filter_table_for_class(table, class_name)
+ if filtered_table is None:
+ # nothing to update for this class
+ continue
+
+ # Regex to find any line-starting Javadoc prefix like " * " before
+ # This will match patterns like: " *
...
" and capture the prefix (e.g. " * ")
+ pattern = re.compile(r"(?m)^(?P[ \t]*\*[ \t]*)
[\s\S]*?
")
+
+ def repl(m: re.Match) -> str:
+ prefix = m.group("prefix")
+ # Build the new block with the same prefix on each line
+ lines = filtered_table.splitlines()
+ replaced = prefix + "
\n"
+ for ln in lines:
+ replaced += prefix + ln.rstrip() + "\n"
+ replaced += prefix + "
"
+ return replaced
+
+ new_content, nsubs = pattern.subn(repl, content)
+ if nsubs > 0 and new_content != original:
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ updated_files.append(path)
+ print(f"Updated {path}: replaced {nsubs}
block(s)")
+ return updated_files
+
+
+def main(argv: List[str]):
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--mvnw", default="./mvnw", help="Path to maven wrapper")
+ parser.add_argument(
+ "--module", default="benchmarks", help="Module directory to build/run"
+ )
+ parser.add_argument("--java", default="java", help="Java command")
+ parser.add_argument(
+ "--jmh-args",
+ default="",
+ help='Extra arguments to pass to the JMH main (e.g. "-f 1 -wi 0 -i 1")',
+ )
+ args = parser.parse_args(argv)
+
+ build_benchmarks(args.mvnw, args.module)
+ jar = find_benchmarks_jar(args.module)
+ output = run_jmh(jar, args.java, args.jmh_args)
+
+ # Print a short preview of the JMH output
+ preview = "\n".join(output.splitlines()[:120])
+ print("\n--- JMH output preview ---")
+ print(preview)
+ print("--- end preview ---\n")
+
+ table = extract_first_table(output)
+
+ updated = update_pre_blocks_under_module(args.module, table)
+
+ if not updated:
+ print(
+ 'No files were updated (no
blocks with "thrpt" found under the module).'
+ )
+ else:
+ print("\nUpdated files:")
+ for p in updated:
+ print(" -", p)
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 12fbe1e90..5f1f57004 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1,19 +1,2 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-wrapperVersion=3.3.2
distributionType=only-script
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..3e5ed1592
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,133 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Build Commands
+
+This project uses Maven with mise for task automation. The Maven wrapper (`./mvnw`) is used for all builds.
+
+```bash
+# Full CI build (clean + install + all checks)
+mise run ci
+
+# Quick compile without tests or checks (fastest for development)
+mise run compile
+
+# Run unit tests only (skips formatting/coverage/checkstyle checks)
+mise run test
+
+# Run all tests including integration tests
+mise run test-all
+
+# Format code with Google Java Format
+mise run format
+# or directly: ./mvnw spotless:apply
+
+# Run a single test class
+./mvnw test -Dtest=CounterTest -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true
+
+# Run a single test method
+./mvnw test -Dtest=CounterTest#testIncrement -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true
+
+# Run tests in a specific module
+./mvnw test -pl prometheus-metrics-core -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true
+
+# Regenerate protobuf classes (after protobuf dependency update)
+mise run generate
+```
+
+## Architecture
+
+The library follows a layered architecture where metrics flow from core types through a registry to exporters:
+
+```
+prometheus-metrics-core (user-facing API)
+ │
+ ▼ collect()
+prometheus-metrics-model (immutable snapshots)
+ │
+ ▼
+prometheus-metrics-exposition-formats (text/protobuf/OpenMetrics)
+ │
+ ▼
+Exporters (httpserver, servlet, pushgateway, opentelemetry)
+```
+
+### Key Modules
+
+- **prometheus-metrics-core**: User-facing metric types (Counter, Gauge, Histogram, Summary, Info, StateSet). All metrics implement the `Collector` interface with a `collect()` method.
+- **prometheus-metrics-model**: Internal read-only immutable snapshot types returned by `collect()`. Contains `PrometheusRegistry` for metric registration.
+- **prometheus-metrics-config**: Runtime configuration via properties files or system properties.
+- **prometheus-metrics-exposition-formats**: Converts snapshots to Prometheus exposition formats.
+- **prometheus-metrics-tracer**: Exemplar support with OpenTelemetry tracing integration.
+- **prometheus-metrics-simpleclient-bridge**: Allows legacy simpleclient 0.16.0 metrics to work with the new registry.
+
+### Instrumentation Modules
+
+Pre-built instrumentations: `prometheus-metrics-instrumentation-jvm`, `-caffeine`, `-guava`, `-dropwizard`, `-dropwizard5`.
+
+## Code Style
+
+- **Formatter**: Google Java Format (enforced via Spotless)
+- **Line length**: 100 characters (enforced for ALL files including Markdown, Java, YAML, etc.)
+- **Indentation**: 2 spaces
+- **Static analysis**: Error Prone with NullAway (`io.prometheus.metrics` package)
+- **Logger naming**: Logger fields must be named `logger` (not `log`, `LOG`, or `LOGGER`)
+- **Assertions in tests**: Use static imports from AssertJ (`import static org.assertj.core.api.Assertions.assertThat`)
+- **Empty catch blocks**: Use `ignored` as the exception variable name
+- **Markdown code blocks**: Always specify language (e.g., ` ```java`, ` ```bash`, ` ```text`)
+
+## Linting and Validation
+
+**CRITICAL**: These checks MUST be run before creating any commits. CI will fail if these checks fail.
+
+### Java Files
+
+- **ALWAYS** run `mise run build` after modifying Java files to ensure:
+ - Code formatting (Spotless with Google Java Format)
+ - Static analysis (Error Prone with NullAway)
+ - Checkstyle validation
+ - Build succeeds (tests are skipped; run `mise run test` or `mise run test-all` to execute tests)
+
+### Non-Java Files (Markdown, YAML, JSON, shell scripts, etc.)
+
+- **ALWAYS** run `mise run lint` after modifying non-Java files
+ (runs super-linter + link checking + BOM check)
+- `mise run fix` auto-fixes lint issues
+- Super-linter will **auto-fix** many issues (formatting, trailing whitespace, etc.)
+- It only reports ERROR-level issues (configured via `LOG_LEVEL=ERROR` in `.github/super-linter.env`)
+- Common issues caught:
+ - Lines exceeding 100 characters in Markdown files
+ - Missing language tags in fenced code blocks
+ - Table formatting issues
+ - YAML/JSON syntax errors
+
+### Running Linters
+
+```bash
+# After modifying Java files (run BEFORE committing)
+mise run build
+
+# After modifying non-Java files (run BEFORE committing)
+mise run lint
+# or to auto-fix: mise run fix
+```
+
+## Testing
+
+- JUnit 5 (Jupiter) with `@Test` annotations
+- AssertJ for fluent assertions
+- Mockito for mocking
+- **Test visibility**: Test classes and test methods must be package-protected (no `public` modifier)
+- Integration tests are in `integration-tests/` and run during `verify` phase
+- Acceptance tests use OATs framework: `mise run acceptance-test`
+
+## Documentation
+
+- Docs live under `docs/content/` and use `$version` as a placeholder for the library version
+- When publishing GitHub Pages, `mise run set-release-version-github-pages` replaces `$version` with the latest git tag across all `docs/content/**/*.md` files (the published site is not versioned)
+- Use `$version` for the Prometheus client version and `$otelVersion-alpha` for the OTel instrumentation version — never hardcode them
+
+## Java Version
+
+Source compatibility: Java 8. Tests run on Java 25 (configured in `mise.toml`).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 50d60bfcc..2abbd91e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,6 +19,14 @@ the code.
Run `./mvnw spotless:apply` to format the code (only changed files) before committing.
+Or run all the linters:
+
+`mise run lint`
+
+To autofix linting issues:
+
+`mise run fix`
+
## Running Tests
If you're getting errors when running tests:
@@ -29,7 +37,7 @@ If you're getting errors when running tests:
### Running native tests
```shell
-mise --env native test
+mise --cd .mise/envs/native run native-test
```
### Avoid failures while running tests
@@ -52,12 +60,34 @@ or simply
mise run compile
```
+## Version Numbers in Examples
+
+Example `pom.xml` files (under `examples/`) should reference the latest
+**released** version, not a SNAPSHOT. After each release, Renovate
+updates these versions automatically.
+
+Only use a SNAPSHOT version in an example when it demonstrates a new
+feature that has not been released yet.
+
## Updating the Protobuf Java Classes
+The generated protobuf `Metrics.java` lives in a versioned package
+(e.g., `...generated.com_google_protobuf_4_33_5`) that changes with each
+protobuf release. A stable extending class at
+`...generated/Metrics.java` reexports all types so that consumer code
+only imports from the version-free package. On protobuf upgrades only
+the `extends` clause in the stable class changes.
+
In the failing PR from renovate, run:
```shell
mise run generate
```
-Add the new `Metrics.java` to Git and commit it.
+The script will:
+
+1. Re-generate the protobuf sources with the new version.
+2. Update the versioned package name in all Java files
+ (including the stable `Metrics.java` extends clause).
+
+Add the updated files to Git and commit them.
diff --git a/RELEASING.md b/RELEASING.md
index d98ae94b1..25f2d59a5 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -1,20 +1,58 @@
-# Create a Release
+# Releasing Instructions for Prometheus Java Client
-1. Go to
-2. Click on "Choose a tag", enter the tag name (e.g. `v0.1.0`), and click "Create a new tag".
-3. Click on "Generate release notes" to auto-generate the release notes based on the commits since
- the last release.
-4. Click on "Publish release".
+Releases are automated via
+[release-please](https://github.com/googleapis/release-please).
-## If the GPG key expired
+## How It Works
+
+1. Commits to `main` using
+ [Conventional Commits](https://www.conventionalcommits.org/) are
+ tracked by release-please.
+2. Release-please maintains a release PR that accumulates changes and
+ updates the changelog.
+3. When the release PR is merged, release-please creates a GitHub
+ release and a `vX.Y.Z` tag.
+4. The tag triggers the existing `release.yml` workflow, which deploys
+ to Maven Central.
+5. After tagging, release-please opens a follow-up PR to bump the
+ SNAPSHOT version in all `pom.xml` files.
+
+## Patch Release (default)
+
+Simply merge the release PR — release-please bumps the patch version
+by default (e.g. `1.5.0` -> `1.5.1`).
+
+## Minor or Major Release
+
+Add a `release-as: X.Y.0` footer to any commit on `main`:
+
+```text
+feat: add new feature
+
+release-as: 1.6.0
+```
+
+Alternatively, edit the release PR title to
+`chore(main): release 1.6.0`.
+
+## Before the Release
+
+If there have been significant changes since the last release, update
+the benchmarks before merging the release PR:
+
+```shell
+mise run update-benchmarks
+```
+
+## If the GPG Key Expired
1. Generate a new key:
-2. Distribute the
- key:
-3. use `gpg --armor --export-secret-keys YOUR_ID` to
- export ([docs](https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#gpg))
-4. Update the
- passphrase:
-5. Update the GPG
- key:
+2. Distribute the key:
+
+3. Use `gpg --armor --export-secret-keys YOUR_ID` to export
+ ([docs](https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#gpg))
+4. Update the passphrase:
+
+5. Update the GPG key:
+
diff --git a/benchmarks/README.md b/benchmarks/README.md
index cb445c7bf..b4c824d85 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -2,6 +2,27 @@
## How to Run
+### Running benchmarks
+
+Run benchmarks and update the results in the Javadoc of the benchmark classes:
+
+```shell
+mise run update-benchmarks
+```
+
+### Different benchmark configurations
+
+The full benchmark suite takes approximately 2 hours with JMH defaults.
+For faster iterations, use these preset configurations:
+
+| Command | Duration | Use Case |
+| ----------------------------- | -------- | ---------------------------------------- |
+| `mise run benchmark:quick` | ~10 min | Quick smoke test during development |
+| `mise run benchmark:standard` | ~60 min | CI/nightly runs with good accuracy |
+| `mise run benchmark:full` | ~2 hours | Full JMH defaults for release validation |
+
+### Running benchmarks manually
+
```shell
java -jar ./benchmarks/target/benchmarks.jar
```
@@ -12,12 +33,33 @@ Run only one specific benchmark:
java -jar ./benchmarks/target/benchmarks.jar CounterBenchmark
```
+### Custom JMH arguments
+
+You can pass custom JMH arguments:
+
+```shell
+# Quick run: 1 fork, 1 warmup iteration, 3 measurement iterations
+mise run update-benchmarks -- --jmh-args "-f 1 -wi 1 -i 3"
+
+# Standard CI: 3 forks, 3 warmup iterations, 5 measurement iterations
+mise run update-benchmarks -- --jmh-args "-f 3 -wi 3 -i 5"
+```
+
+JMH parameter reference:
+
+- `-f N`: Number of forks (JVM restarts)
+- `-wi N`: Number of warmup iterations
+- `-i N`: Number of measurement iterations
+- `-w Ns`: Warmup iteration time (default: 10s)
+- `-r Ns`: Measurement iteration time (default: 10s)
+
## Results
See Javadoc of the benchmark classes:
-- [CounterBenchmark](https://github.com/prometheus/client_java/blob/1.0.x/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/CounterBenchmark.java)
-- [HistogramBenchmark](https://github.com/prometheus/client_java/blob/1.0.x/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramBenchmark.java)
+- [CounterBenchmark](https://github.com/prometheus/client_java/blob/main/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/CounterBenchmark.java)
+- [HistogramBenchmark](https://github.com/prometheus/client_java/blob/main/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramBenchmark.java)
+- [TextFormatUtilBenchmark](https://github.com/prometheus/client_java/blob/main/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/TextFormatUtilBenchmark.java)
## What Prometheus Java client optimizes for
@@ -31,6 +73,14 @@ Prometheus client Java metrics support concurrent updates and scrapes. This show
multiple threads recording data in shared
metrics.
+## Test the benchmark creation script
+
+To test the benchmark creation script, run:
+
+```shell
+python ./.mise/tasks/test_update-benchmarks.py
+```
+
## Archive
The `src/main/archive/` directory contains the old benchmarks from 0.16.0 and earlier. It will be
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index 3cdbc09c0..7c211006b 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -8,7 +8,7 @@
io.prometheusclient_java
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTbenchmarks
@@ -23,6 +23,7 @@
0.16.03.0.2true
+ true
@@ -85,8 +86,8 @@
1.81.8
-
-
+
+
-parameters
diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/CounterBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/CounterBenchmark.java
index f3c1e9309..f5d0a1a0f 100644
--- a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/CounterBenchmark.java
+++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/CounterBenchmark.java
@@ -18,21 +18,20 @@
import org.openjdk.jmh.annotations.Threads;
/**
- * Results on a machine with dedicated Core i7 1265U:
+ * Results on a machine with dedicated Ubuntu 24.04 LTS, AMD Ryzen™ 9 7900 × 24, 96.0 GiB RAM:
*
*
*
* Prometheus counters are faster than counters of other libraries. For example, incrementing a
diff --git a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramBenchmark.java b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramBenchmark.java
index a8e6bddb1..9ada40f35 100644
--- a/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramBenchmark.java
+++ b/benchmarks/src/main/java/io/prometheus/metrics/benchmarks/HistogramBenchmark.java
@@ -17,15 +17,15 @@
import org.openjdk.jmh.annotations.Threads;
/**
- * Results on a machine with dedicated Core i7 1265U:
+ * Results on a machine with dedicated Ubuntu 24.04 LTS, AMD Ryzen™ 9 7900 × 24, 96.0 GiB RAM:
*
*
+ */
public class TextFormatUtilBenchmark {
private static final MetricSnapshots SNAPSHOTS;
@@ -69,14 +81,15 @@ public OutputStream openMetricsWriteToByteArray(WriterState writerState) throws
// avoid growing the array
ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream;
byteArrayOutputStream.reset();
- OPEN_METRICS_TEXT_FORMAT_WRITER.write(byteArrayOutputStream, SNAPSHOTS);
+ OPEN_METRICS_TEXT_FORMAT_WRITER.write(
+ byteArrayOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8);
return byteArrayOutputStream;
}
@Benchmark
public OutputStream openMetricsWriteToNull() throws IOException {
OutputStream nullOutputStream = NullOutputStream.INSTANCE;
- OPEN_METRICS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS);
+ OPEN_METRICS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8);
return nullOutputStream;
}
@@ -85,14 +98,15 @@ public OutputStream prometheusWriteToByteArray(WriterState writerState) throws I
// avoid growing the array
ByteArrayOutputStream byteArrayOutputStream = writerState.byteArrayOutputStream;
byteArrayOutputStream.reset();
- PROMETHEUS_TEXT_FORMAT_WRITER.write(byteArrayOutputStream, SNAPSHOTS);
+ PROMETHEUS_TEXT_FORMAT_WRITER.write(
+ byteArrayOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8);
return byteArrayOutputStream;
}
@Benchmark
public OutputStream prometheusWriteToNull() throws IOException {
OutputStream nullOutputStream = NullOutputStream.INSTANCE;
- PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS);
+ PROMETHEUS_TEXT_FORMAT_WRITER.write(nullOutputStream, SNAPSHOTS, EscapingScheme.ALLOW_UTF8);
return nullOutputStream;
}
diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml
index 5f632c578..82e964658 100644
--- a/checkstyle-suppressions.xml
+++ b/checkstyle-suppressions.xml
@@ -5,5 +5,20 @@
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/content/_index.md b/docs/content/_index.md
index 3b8966cf3..28e5165cd 100644
--- a/docs/content/_index.md
+++ b/docs/content/_index.md
@@ -30,7 +30,7 @@ synchronization. See Javadoc comments in
**More Info**
The Grafana Labs Blog has a post
-[Introducing the Prometheus Java Client 1.0.0](https://grafana.com/blog/2023/09/27/introducing-the-prometheus-java-client-1.0.0/)
+[Introducing the Prometheus Java Client 1.0.0](https://grafana.com/blog/2023/09/27/introducing-the-prometheus-java-client-1-0-0/)
with a good overview of the release.
There will also be a presentation at the [PromCon](https://promcon.io) conference on 29 Sep 2023.
diff --git a/docs/content/config/config.md b/docs/content/config/config.md
index 833a72653..cd7f7af7b 100644
--- a/docs/content/config/config.md
+++ b/docs/content/config/config.md
@@ -9,21 +9,21 @@ The Prometheus metrics library provides multiple options how to override configu
- Properties file
- System properties
-
-Future releases will add more options, like configuration via environment variables.
+- Environment variables
Example:
```properties
-io.prometheus.exporter.httpServer.port = 9401
+io.prometheus.exporter.http_server.port=9401
```
The property above changes the port for the
[HTTPServer exporter]({{< relref "/exporters/httpserver.md" >}}) to _9401_.
-- Properties file: Add the line above to the properties file.
-- System properties: Use the command line parameter `-Dio.prometheus.exporter.httpServer.port=9401`
-- when starting your application.
+- **Properties file**: Add the line above to the properties file.
+- **System properties**: Use the command line parameter
+ `-Dio.prometheus.exporter.http_server.port=9401` when starting your application.
+- **Environment variables**: Set `IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORT=9401`
## Location of the Properties File
@@ -34,25 +34,61 @@ The properties file is searched in the following locations:
- System property `-Dprometheus.config=/path/to/prometheus.properties`.
- Environment variable `PROMETHEUS_CONFIG=/path/to/prometheus.properties`.
+## Property Naming Conventions
+
+Properties use **snake_case** format with underscores separating words
+(e.g., `http_server`, `exemplars_enabled`).
+
+For backward compatibility, camelCase property names are also supported in
+properties files and system properties, but snake_case is the preferred format.
+
+### Environment Variables
+
+Environment variables follow standard conventions:
+
+- All uppercase letters: `IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORT`
+- Underscores for all separators (both package and word boundaries)
+- Prefix must be `IO_PROMETHEUS`
+
+The library automatically converts environment variables to the correct property format.
+
+**Examples:**
+
+| Environment Variable | Property Equivalent |
+| --------------------------------------------- | --------------------------------------------- |
+| `IO_PROMETHEUS_METRICS_EXEMPLARS_ENABLED` | `io.prometheus.metrics.exemplars_enabled` |
+| `IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORT` | `io.prometheus.exporter.http_server.port` |
+| `IO_PROMETHEUS_METRICS_HISTOGRAM_NATIVE_ONLY` | `io.prometheus.metrics.histogram_native_only` |
+
+### Property Precedence
+
+When the same property is defined in multiple sources, the following precedence order applies
+(highest to lowest):
+
+1. **External properties** (passed explicitly via API)
+2. **Environment variables**
+3. **System properties** (command line `-D` flags)
+4. **Properties file** (from file or classpath)
+
## Metrics Properties
-| Name | Javadoc | Note |
-| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
-| io.prometheus.metrics.exemplarsEnabled | [Counter.Builder.withExemplars()]() | (1) (2) |
-| io.prometheus.metrics.histogramNativeOnly | [Histogram.Builder.nativeOnly()]() | (2) |
-| io.prometheus.metrics.histogramClassicOnly | [Histogram.Builder.classicOnly()]() | (2) |
-| io.prometheus.metrics.histogramClassicUpperBounds | [Histogram.Builder.classicUpperBounds()]() | (3) |
-| io.prometheus.metrics.histogramNativeInitialSchema | [Histogram.Builder.nativeInitialSchema()]() | |
-| io.prometheus.metrics.histogramNativeMinZeroThreshold | [Histogram.Builder.nativeMinZeroThreshold()]() | |
-| io.prometheus.metrics.histogramNativeMaxZeroThreshold | [Histogram.Builder.nativeMaxZeroThreshold()]() | |
-| io.prometheus.metrics.histogramNativeMaxNumberOfBuckets | [Histogram.Builder.nativeMaxNumberOfBuckets()]() | |
-| io.prometheus.metrics.histogramNativeResetDurationSeconds | [Histogram.Builder.nativeResetDuration()]() | |
-| io.prometheus.metrics.summaryQuantiles | [Summary.Builder.quantile(double)]() | (4) |
-| io.prometheus.metrics.summaryQuantileErrors | [Summary.Builder.quantile(double, double)]() | (5) |
-| io.prometheus.metrics.summaryMaxAgeSeconds | [Summary.Builder.maxAgeSeconds()]() | |
-| io.prometheus.metrics.summaryNumberOfAgeBuckets | [Summary.Builder.numberOfAgeBuckets()]() | |
+| Name | Javadoc | Note |
+| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
+| io.prometheus.metrics.exemplars_enabled | [Counter.Builder.withExemplars()]() | (1) (2) |
+| io.prometheus.metrics.histogram_native_only | [Histogram.Builder.nativeOnly()]() | (2) |
+| io.prometheus.metrics.histogram_classic_only | [Histogram.Builder.classicOnly()]() | (2) |
+| io.prometheus.metrics.histogram_classic_upper_bounds | [Histogram.Builder.classicUpperBounds()]() | (3) |
+| io.prometheus.metrics.histogram_native_initial_schema | [Histogram.Builder.nativeInitialSchema()]() | |
+| io.prometheus.metrics.histogram_native_min_zero_threshold | [Histogram.Builder.nativeMinZeroThreshold()]() | |
+| io.prometheus.metrics.histogram_native_max_zero_threshold | [Histogram.Builder.nativeMaxZeroThreshold()]() | |
+| io.prometheus.metrics.histogram_native_max_number_of_buckets | [Histogram.Builder.nativeMaxNumberOfBuckets()]() | |
+| io.prometheus.metrics.histogram_native_reset_duration_seconds | [Histogram.Builder.nativeResetDuration()]() | |
+| io.prometheus.metrics.summary_quantiles | [Summary.Builder.quantile(double)]() | (4) |
+| io.prometheus.metrics.summary_quantile_errors | [Summary.Builder.quantile(double, double)]() | (5) |
+| io.prometheus.metrics.summary_max_age_seconds | [Summary.Builder.maxAgeSeconds()]() | |
+| io.prometheus.metrics.summary_number_of_age_buckets | [Summary.Builder.numberOfAgeBuckets()]() | |
@@ -64,20 +100,20 @@ not just for counters
(3) Comma-separated list. Example: `.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10`.
(4) Comma-separated list. Example: `0.5, 0.95, 0.99`.
(5) Comma-separated list. If specified, the list must have the same length as
-`io.prometheus.metrics.summaryQuantiles`. Example: `0.01, 0.005, 0.005`.
+`io.prometheus.metrics.summary_quantiles`. Example: `0.01, 0.005, 0.005`.
There's one special feature about metric properties: You can set a property for one specific
metric only by specifying the metric name. Example:
Let's say you have a histogram named `latency_seconds`.
```properties
-io.prometheus.metrics.histogramClassicUpperBounds = 0.2, 0.4, 0.8, 1.0
+io.prometheus.metrics.histogram_classic_upper_bounds=0.2, 0.4, 0.8, 1.0
```
The line above sets histogram buckets for all histograms. However:
```properties
-io.prometheus.metrics.latency_seconds.histogramClassicUpperBounds = 0.2, 0.4, 0.8, 1.0
+io.prometheus.metrics.latency_seconds.histogram_classic_upper_bounds=0.2, 0.4, 0.8, 1.0
```
The line above sets histogram buckets only for the histogram named `latency_seconds`.
@@ -88,11 +124,11 @@ This works for all Metrics properties.
-| Name | Javadoc | Note |
-| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
-| io.prometheus.exemplars.minRetentionPeriodSeconds | [ExemplarsProperties.getMinRetentionPeriodSeconds()]() | |
-| io.prometheus.exemplars.maxRetentionPeriodSeconds | [ExemplarsProperties.getMaxRetentionPeriodSeconds()]() | |
-| io.prometheus.exemplars.sampleIntervalMilliseconds | [ExemplarsProperties.getSampleIntervalMilliseconds()]() | |
+| Name | Javadoc | Note |
+| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
+| io.prometheus.exemplars.min_retention_period_seconds | [ExemplarsProperties.getMinRetentionPeriodSeconds()]() | |
+| io.prometheus.exemplars.max_retention_period_seconds | [ExemplarsProperties.getMaxRetentionPeriodSeconds()]() | |
+| io.prometheus.exemplars.sample_interval_milliseconds | [ExemplarsProperties.getSampleIntervalMilliseconds()]() | |
@@ -100,10 +136,10 @@ This works for all Metrics properties.
-| Name | Javadoc | Note |
-| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
-| io.prometheus.exporter.includeCreatedTimestamps | [ExporterProperties.getIncludeCreatedTimestamps()]() | (1) |
-| io.prometheus.exporter.exemplarsOnAllMetricTypes | [ExporterProperties.getExemplarsOnAllMetricTypes()]() | (1) |
+| Name | Javadoc | Note |
+| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
+| io.prometheus.exporter.include_created_timestamps | [ExporterProperties.getIncludeCreatedTimestamps()]() | (1) |
+| io.prometheus.exporter.exemplars_on_all_metric_types | [ExporterProperties.getExemplarsOnAllMetricTypes()]() | (1) |
@@ -113,12 +149,12 @@ This works for all Metrics properties.
-| Name | Javadoc | Note |
-| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
-| io.prometheus.exporter.filter.metricNameMustBeEqualTo | [ExporterFilterProperties.getAllowedMetricNames()]() | (1) |
-| io.prometheus.exporter.filter.metricNameMustNotBeEqualTo | [ExporterFilterProperties.getExcludedMetricNames()]() | (2) |
-| io.prometheus.exporter.filter.metricNameMustStartWith | [ExporterFilterProperties.getAllowedMetricNamePrefixes()]() | (3) |
-| io.prometheus.exporter.filter.metricNameMustNotStartWith | [ExporterFilterProperties.getExcludedMetricNamePrefixes()]() | (4) |
+| Name | Javadoc | Note |
+| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
+| io.prometheus.exporter.filter.metric_name_must_be_equal_to | [ExporterFilterProperties.getAllowedMetricNames()]() | (1) |
+| io.prometheus.exporter.filter.metric_name_must_not_be_equal_to | [ExporterFilterProperties.getExcludedMetricNames()]() | (2) |
+| io.prometheus.exporter.filter.metric_name_must_start_with | [ExporterFilterProperties.getAllowedMetricNamePrefixes()]() | (3) |
+| io.prometheus.exporter.filter.metric_name_must_not_start_with | [ExporterFilterProperties.getExcludedMetricNamePrefixes()]() | (4) |
@@ -132,9 +168,9 @@ Only metrics starting with these prefixes will be exposed.
-| Name | Javadoc | Note |
-| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---- |
-| io.prometheus.exporter.httpServer.port | [HTTPServer.Builder.port()]() | |
+| Name | Javadoc | Note |
+| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---- |
+| io.prometheus.exporter.http_server.port | [HTTPServer.Builder.port()]() | |
@@ -142,18 +178,18 @@ Only metrics starting with these prefixes will be exposed.
-| Name | Javadoc | Note |
-| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
-| io.prometheus.exporter.opentelemetry.protocol | [OpenTelemetryExporter.Builder.protocol()]() | (1) |
-| io.prometheus.exporter.opentelemetry.endpoint | [OpenTelemetryExporter.Builder.endpoint()]() | |
-| io.prometheus.exporter.opentelemetry.headers | [OpenTelemetryExporter.Builder.headers()]() | (2) |
-| io.prometheus.exporter.opentelemetry.intervalSeconds | [OpenTelemetryExporter.Builder.intervalSeconds()]() | |
-| io.prometheus.exporter.opentelemetry.timeoutSeconds | [OpenTelemetryExporter.Builder.timeoutSeconds()]() | |
-| io.prometheus.exporter.opentelemetry.serviceName | [OpenTelemetryExporter.Builder.serviceName()]() | |
-| io.prometheus.exporter.opentelemetry.serviceNamespace | [OpenTelemetryExporter.Builder.serviceNamespace()]() | |
-| io.prometheus.exporter.opentelemetry.serviceInstanceId | [OpenTelemetryExporter.Builder.serviceInstanceId()]() | |
-| io.prometheus.exporter.opentelemetry.serviceVersion | [OpenTelemetryExporter.Builder.serviceVersion()]() | |
-| io.prometheus.exporter.opentelemetry.resourceAttributes | [OpenTelemetryExporter.Builder.resourceAttributes()]() | (3) |
+| Name | Javadoc | Note |
+| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
+| io.prometheus.exporter.opentelemetry.protocol | [OpenTelemetryExporter.Builder.protocol()]() | (1) |
+| io.prometheus.exporter.opentelemetry.endpoint | [OpenTelemetryExporter.Builder.endpoint()]() | |
+| io.prometheus.exporter.opentelemetry.headers | [OpenTelemetryExporter.Builder.headers()]() | (2) |
+| io.prometheus.exporter.opentelemetry.interval_seconds | [OpenTelemetryExporter.Builder.intervalSeconds()]() | |
+| io.prometheus.exporter.opentelemetry.timeout_seconds | [OpenTelemetryExporter.Builder.timeoutSeconds()]() | |
+| io.prometheus.exporter.opentelemetry.service_name | [OpenTelemetryExporter.Builder.serviceName()]() | |
+| io.prometheus.exporter.opentelemetry.service_namespace | [OpenTelemetryExporter.Builder.serviceNamespace()]() | |
+| io.prometheus.exporter.opentelemetry.service_instance_id | [OpenTelemetryExporter.Builder.serviceInstanceId()]() | |
+| io.prometheus.exporter.opentelemetry.service_version | [OpenTelemetryExporter.Builder.serviceVersion()]() | |
+| io.prometheus.exporter.opentelemetry.resource_attributes | [OpenTelemetryExporter.Builder.resourceAttributes()]() | (3) |
@@ -170,10 +206,15 @@ See Javadoc for details.
-| Name | Javadoc | Note |
-| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
-| io.prometheus.exporter.pushgateway.address | [PushGateway.Builder.address()]() | |
-| io.prometheus.exporter.pushgateway.scheme | [PushGateway.Builder.scheme()]() | |
-| io.prometheus.exporter.pushgateway.job | [PushGateway.Builder.job()]() | |
+| Name | Javadoc | Note |
+| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---- |
+| io.prometheus.exporter.pushgateway.address | [PushGateway.Builder.address()]() | |
+| io.prometheus.exporter.pushgateway.scheme | [PushGateway.Builder.scheme()]() | |
+| io.prometheus.exporter.pushgateway.job | [PushGateway.Builder.job()]() | |
+| io.prometheus.exporter.pushgateway.escaping_scheme | [PushGateway.Builder.escapingScheme()]() | (1) |
+
+(1) Escaping scheme can be `allow-utf-8`, `underscores`, `dots`, or `values` as described in
+[escaping schemes](https://github.com/prometheus/docs/blob/main/docs/instrumenting/escaping_schemes.md#escaping-schemes)
+and in the [Unicode documentation]({{< relref "../exporters/unicode.md" >}}).
diff --git a/docs/content/exporters/filter.md b/docs/content/exporters/filter.md
index 01ac48e69..ae7ad9564 100644
--- a/docs/content/exporters/filter.md
+++ b/docs/content/exporters/filter.md
@@ -1,6 +1,6 @@
---
title: Filter
-weight: 2
+weight: 3
---
All exporters support a `name[]` URL parameter for querying only specific metric names. Examples:
diff --git a/docs/content/exporters/httpserver.md b/docs/content/exporters/httpserver.md
index 4d0aab5f2..a9017b0de 100644
--- a/docs/content/exporters/httpserver.md
+++ b/docs/content/exporters/httpserver.md
@@ -1,6 +1,6 @@
---
title: HTTPServer
-weight: 3
+weight: 4
---
The `HTTPServer` is a standalone server for exposing a metric endpoint. A minimal example
@@ -40,4 +40,4 @@ You can find an example of authentication and SSL in the
See _config_ section (_todo_) on runtime configuration options.
-- `io.prometheus.exporter.httpServer.port`: The port to bind to.
+- `io.prometheus.exporter.http_server.port`: The port to bind to.
diff --git a/docs/content/exporters/pushgateway.md b/docs/content/exporters/pushgateway.md
index b4c531022..497aa9b57 100644
--- a/docs/content/exporters/pushgateway.md
+++ b/docs/content/exporters/pushgateway.md
@@ -1,6 +1,6 @@
---
title: Pushgateway
-weight: 5
+weight: 6
---
The [Prometheus Pushgateway](https://github.com/prometheus/pushgateway) exists to allow ephemeral
diff --git a/docs/content/exporters/servlet.md b/docs/content/exporters/servlet.md
index 93b870b1d..2b0873b70 100644
--- a/docs/content/exporters/servlet.md
+++ b/docs/content/exporters/servlet.md
@@ -1,6 +1,6 @@
---
title: Servlet
-weight: 4
+weight: 5
---
The
diff --git a/docs/content/exporters/spring.md b/docs/content/exporters/spring.md
index fc0d946dd..45df21431 100644
--- a/docs/content/exporters/spring.md
+++ b/docs/content/exporters/spring.md
@@ -1,6 +1,6 @@
---
title: Spring
-weight: 5
+weight: 7
---
## Alternative: Use Spring's Built-in Metrics Library
diff --git a/docs/content/exporters/unicode.md b/docs/content/exporters/unicode.md
new file mode 100644
index 000000000..2a34e0400
--- /dev/null
+++ b/docs/content/exporters/unicode.md
@@ -0,0 +1,34 @@
+---
+title: Unicode
+weight: 2
+---
+
+{{< hint type=warning >}}
+Unicode support is experimental, because [OpenMetrics specification](https://openmetrics.io/) is not
+updated yet to support Unicode characters in metric and label names.
+{{< /hint >}}
+
+The Prometheus Java client library allows all Unicode characters, that can be encoded as UTF-8.
+
+At scrape time, some characters are replaced based on the `encoding` header according
+to
+the [Escaping scheme](https://github.com/prometheus/docs/blob/main/docs/instrumenting/escaping_schemes.md).
+
+For example, if you use the `underscores` escaping scheme, dots in metric and label names are
+replaced with underscores, so that the metric name `http.server.duration` becomes
+`http_server_duration`.
+
+Prometheus servers that do not support Unicode at all will not pass the `encoding` header, and the
+Prometheus Java client library will replace dots, as well as any character that is not in the legacy
+character set (`a-zA-Z0-9_:`), with underscores by default.
+
+When `escaping=allow-utf-8` is passed, add valid UTF-8 characters to the metric and label names
+without replacing them. This allows you to use dots in metric and label names, as well as
+other UTF-8 characters, without any replacements.
+
+## PushGateway
+
+When using the [Pushgateway]({{< relref "pushgateway.md" >}}), Unicode support has to be enabled
+explicitly by setting `io.prometheus.exporter.pushgateway.escapingScheme` to `allow-utf-8` in the
+Pushgateway configuration file - see
+[Pushgateway configuration]({{< relref "/config/config.md#exporter-pushgateway-properties" >}})
diff --git a/docs/content/getting-started/metric-types.md b/docs/content/getting-started/metric-types.md
index 3bcda84fe..97424ef5c 100644
--- a/docs/content/getting-started/metric-types.md
+++ b/docs/content/getting-started/metric-types.md
@@ -109,9 +109,9 @@ most cases you don't need them, defaults are good. The following is an incomplet
most important options:
- `nativeOnly()` / `classicOnly()`: Create a histogram with one representation only.
-- `classicBuckets(...)`: Set the classic bucket boundaries. Default buckets are `.005`, `.01`,
- `.025`, `.05`, `.1`, `.25`, `.5`, `1`, `2.5`, `5`, `and 10`. The default bucket boundaries are
- designed for measuring request durations in seconds.
+- `classicUpperBounds(...)`: Set the classic bucket upper boundaries. Default bucket upper
+ boundaries are `.005`, `.01`, `.025`, `.05`, `.1`, `.25`, `.5`, `1`, `2.5`, `5`, `and 10`. The
+ default bucket boundaries are designed for measuring request durations in seconds.
- `nativeMaxNumberOfBuckets()`: Upper limit for the number of native histogram buckets.
Default is 160. When the maximum is reached, the native histogram automatically
reduces resolution to stay below the limit.
@@ -121,6 +121,94 @@ for [Histogram.Builder](/client_java/api/io/prometheus/metrics/core/metrics/Hist
for a complete list of options. Some options can be configured at runtime,
see [config]({{< relref "../config/config.md" >}}).
+### Custom Bucket Boundaries
+
+The default bucket boundaries are designed for measuring request durations in seconds. For other
+use cases, you may want to define custom bucket boundaries. The histogram builder provides three
+methods for this:
+
+**1. Arbitrary Custom Boundaries**
+
+Use `classicUpperBounds(...)` to specify arbitrary bucket boundaries:
+
+```java
+Histogram responseSize = Histogram.builder()
+ .name("http_response_size_bytes")
+ .help("HTTP response size in bytes")
+ .classicUpperBounds(100, 1000, 10000, 100000, 1000000) // bytes
+ .register();
+```
+
+**2. Linear Boundaries**
+
+Use `classicLinearUpperBounds(start, width, count)` for equal-width buckets:
+
+```java
+Histogram queueSize = Histogram.builder()
+ .name("queue_size")
+ .help("Number of items in queue")
+ .classicLinearUpperBounds(10, 10, 10) // 10, 20, 30, ..., 100
+ .register();
+```
+
+**3. Exponential Boundaries**
+
+Use `classicExponentialUpperBounds(start, factor, count)` for exponential growth:
+
+```java
+Histogram dataSize = Histogram.builder()
+ .name("data_size_bytes")
+ .help("Data size in bytes")
+ .classicExponentialUpperBounds(100, 10, 5) // 100, 1k, 10k, 100k, 1M
+ .register();
+```
+
+### Native Histograms with Custom Buckets (NHCB)
+
+Prometheus supports a special mode called Native Histograms with Custom Buckets (NHCB) that uses
+schema -53. In this mode, custom bucket boundaries from classic histograms are preserved when
+converting to native histograms.
+
+The Java client library automatically supports NHCB:
+
+1. By default, histograms maintain both classic (with custom buckets) and native representations
+2. The classic representation with custom buckets is exposed to Prometheus
+3. Prometheus servers can convert these to NHCB upon ingestion when configured with the
+ `convert_classic_histograms_to_nhcb` scrape option
+
+Example:
+
+```java
+// This histogram will work seamlessly with NHCB
+Histogram apiLatency = Histogram.builder()
+ .name("api_request_duration_seconds")
+ .help("API request duration")
+ .classicUpperBounds(0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0) // custom boundaries
+ .register();
+```
+
+On the Prometheus side, configure the scrape job:
+
+```yaml
+scrape_configs:
+ - job_name: "my-app"
+ scrape_protocols: ["PrometheusProto"]
+ convert_classic_histograms_to_nhcb: true
+ static_configs:
+ - targets: ["localhost:9400"]
+```
+
+{{< hint type=note >}}
+NHCB is useful when:
+
+- You need precise bucket boundaries for your specific use case
+- You're migrating from classic histograms and want to preserve bucket boundaries
+- Exponential bucketing from standard native histograms isn't a good fit for your distribution
+ {{< /hint >}}
+
+See [examples/example-custom-buckets](https://github.com/prometheus/client_java/tree/main/examples/example-custom-buckets)
+for a complete example with Prometheus and Grafana.
+
Histograms and summaries are both used for observing distributions. Therefore, the both implement
the `DistributionDataPoint` interface. Using the `DistributionDataPoint` interface directly gives
you the option to switch between histograms and summaries later with minimal code changes.
@@ -276,3 +364,6 @@ in the `prometheus-metrics-core` API.
However, `prometheus-metrics-model` implements the underlying data model for these types.
To use these types, you need to implement your own `Collector` where the `collect()` method returns
an `UnknownSnapshot` or a `HistogramSnapshot` with `.gaugeHistogram(true)`.
+If your custom collector does not implement `getMetricType()` and `getLabelNames()`, ensure it does
+not produce the same metric name and label set as another collector, or the exposition may contain
+duplicate time series.
diff --git a/docs/content/getting-started/performance.md b/docs/content/getting-started/performance.md
index 31b8de162..435f0d18a 100644
--- a/docs/content/getting-started/performance.md
+++ b/docs/content/getting-started/performance.md
@@ -60,13 +60,13 @@ or you use the corresponding [config options]({{< relref "../config/config.md" >
One way to do this is with system properties in the command line when you start your application
```sh
-java -Dio.prometheus.metrics.histogramClassicOnly=true my-app.jar
+java -Dio.prometheus.metrics.histogram_classic_only=true my-app.jar
```
or
```sh
-java -Dio.prometheus.metrics.histogramNativeOnly=true my-app.jar
+java -Dio.prometheus.metrics.histogram_native_only=true my-app.jar
```
If you don't want to add a command line parameter every time you start your application, you can add
@@ -75,13 +75,13 @@ that it gets packed into your JAR file). The `prometheus.properties` file should
line:
```properties
-io.prometheus.metrics.histogramClassicOnly=true
+io.prometheus.metrics.histogram_classic_only=true
```
or
```properties
-io.prometheus.metrics.histogramNativeOnly=true
+io.prometheus.metrics.histogram_native_only=true
```
Future releases will add more configuration options, like support for configuration via environment
diff --git a/docs/content/getting-started/registry.md b/docs/content/getting-started/registry.md
index afebbb304..7f561ecef 100644
--- a/docs/content/getting-started/registry.md
+++ b/docs/content/getting-started/registry.md
@@ -6,7 +6,7 @@ weight: 2
In order to expose metrics, you need to register them with a `PrometheusRegistry`. We are using a
counter as an example here, but the `register()` method is the same for all metric types.
-## Registering a Metrics with the Default Registry
+## Registering a Metric with the Default Registry
```java
Counter eventsTotal = Counter.builder()
@@ -18,7 +18,7 @@ Counter eventsTotal = Counter.builder()
The `register()` call above builds the counter and registers it with the global static
`PrometheusRegistry.defaultRegistry`. Using the default registry is recommended.
-## Registering a Metrics with a Custom Registry
+## Registering a Metric with a Custom Registry
You can also register your metric with a custom registry:
@@ -78,12 +78,30 @@ Counter eventsTotal2 = Counter.builder()
.register(); // IllegalArgumentException, because a metric with that name is already registered
```
+## Validation at registration only
+
+Validation of duplicate metric names and label schemas happens at registration time only.
+Built-in metrics (Counter, Gauge, Histogram, etc.) participate in this validation.
+
+Custom collectors that implement the `Collector` or `MultiCollector` interface can optionally
+implement `getPrometheusName()` and `getMetricType()` (and the MultiCollector per-name variants) so
+the registry can enforce consistency. **Validation is skipped when metric name or type is
+unavailable:** if `getPrometheusName()` or `getMetricType()` returns `null`, the registry does not
+validate that collector. If two such collectors produce the same metric name and same label set at
+scrape time, the exposition output may contain duplicate time series and be invalid for Prometheus.
+
+When validation _is_ performed (name and type are non-null), **null label names are treated as an
+empty label schema:** `getLabelNames()` returning `null` is normalized to `Collections.emptySet()`
+and full label-schema validation and duplicate detection still apply. A collector that returns a
+non-null type but leaves `getLabelNames()` as `null` is still validated, with its labels treated as
+empty.
+
## Unregistering a Metric
There is no automatic expiry of unused metrics (yet), once a metric is registered it will remain
registered forever.
-However, you can programmatically unregistered an obsolete metric like this:
+However, you can programmatically unregister an obsolete metric like this:
```java
PrometheusRegistry.defaultRegistry.unregister(eventsTotal);
diff --git a/docs/content/instrumentation/jvm.md b/docs/content/instrumentation/jvm.md
index 804c1b09b..a9a15341f 100644
--- a/docs/content/instrumentation/jvm.md
+++ b/docs/content/instrumentation/jvm.md
@@ -3,6 +3,16 @@ title: JVM
weight: 1
---
+{{< hint type=note >}}
+
+Looking for JVM metrics that follow OTel semantic
+conventions? See
+[OTel JVM Runtime Metrics]({{< relref "../otel/jvm-runtime-metrics.md" >}})
+for an alternative based on OpenTelemetry's
+runtime-telemetry module.
+
+{{< /hint >}}
+
The JVM instrumentation module provides a variety of out-of-the-box JVM and process metrics. To use
it, add the following dependency:
diff --git a/docs/content/internals/model.md b/docs/content/internals/model.md
index c54e79ee3..e1b2af644 100644
--- a/docs/content/internals/model.md
+++ b/docs/content/internals/model.md
@@ -19,7 +19,10 @@ All metric types implement
the [Collector](/client_java/api/io/prometheus/metrics/model/registry/Collector.html) interface,
i.e. they provide
a [collect()]()
-method to produce snapshots.
+method to produce snapshots. Implementers that do not provide metric type or label names (returning
+null from `getMetricType()` and `getLabelNames()`) are not validated at registration; they must
+avoid producing the same metric name and label schema as another collector, or exposition may be
+invalid.
## prometheus-metrics-model
diff --git a/docs/content/otel/jvm-runtime-metrics.md b/docs/content/otel/jvm-runtime-metrics.md
new file mode 100644
index 000000000..d61da1861
--- /dev/null
+++ b/docs/content/otel/jvm-runtime-metrics.md
@@ -0,0 +1,241 @@
+---
+title: JVM Runtime Metrics
+weight: 4
+---
+
+OpenTelemetry's
+[runtime-telemetry](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry)
+module is an alternative to
+[prometheus-metrics-instrumentation-jvm]({{< relref "../instrumentation/jvm.md" >}})
+for users who want JVM metrics following OTel semantic conventions.
+
+Key advantages:
+
+- Metric names follow
+ [OTel semantic conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/)
+- Java 17+ JFR support (context switches, network I/O,
+ lock contention, memory allocation)
+- Alignment with the broader OTel ecosystem
+
+Since OpenTelemetry's `opentelemetry-exporter-prometheus`
+already depends on this library's `PrometheusRegistry`,
+no additional code is needed in this library — only the
+OTel SDK wiring shown below.
+
+## Dependencies
+
+Use the [OTel Support]({{< relref "support.md" >}}) module
+to pull in the OTel SDK and Prometheus exporter, then add
+the runtime-telemetry instrumentation:
+
+{{< tabs "jvm-runtime-deps" >}}
+{{< tab "Gradle" >}}
+
+```groovy
+implementation 'io.prometheus:prometheus-metrics-otel-support:$version'
+
+// Use opentelemetry-runtime-telemetry-java8 (Java 8+)
+// or opentelemetry-runtime-telemetry-java17 (Java 17+, JFR-based)
+implementation(
+ 'io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:$otelVersion-alpha'
+)
+```
+
+{{< /tab >}}
+{{< tab "Maven" >}}
+
+```xml
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ $version
+ pom
+
+
+
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-runtime-telemetry-java8
+ $otelVersion-alpha
+
+
+
+```
+
+{{< /tab >}}
+{{< /tabs >}}
+
+## Standalone Setup
+
+If you **only** want OTel runtime metrics exposed as
+Prometheus, without any Prometheus Java client metrics:
+
+```java
+import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
+import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+
+PrometheusHttpServer prometheusServer =
+ PrometheusHttpServer.builder()
+ .setPort(9464)
+ .build();
+
+OpenTelemetrySdk openTelemetry =
+ OpenTelemetrySdk.builder()
+ .setMeterProvider(
+ SdkMeterProvider.builder()
+ .registerMetricReader(prometheusServer)
+ .build())
+ .build();
+
+RuntimeMetrics runtimeMetrics =
+ RuntimeMetrics.builder(openTelemetry).build();
+
+// Close on shutdown to stop metric collection and server
+Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ runtimeMetrics.close();
+ prometheusServer.close();
+}));
+
+// Scrape at http://localhost:9464/metrics
+```
+
+## Combined with Prometheus Java Client Metrics
+
+If you already have Prometheus Java client metrics and want to
+add OTel runtime metrics to the **same** `/metrics`
+endpoint, use `PrometheusMetricReader` to bridge OTel
+metrics into a `PrometheusRegistry`:
+
+```java
+import io.prometheus.metrics.core.metrics.Counter;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.model.registry.PrometheusRegistry;
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+
+PrometheusRegistry registry =
+ new PrometheusRegistry();
+
+// Register Prometheus metrics as usual
+Counter myCounter = Counter.builder()
+ .name("my_requests_total")
+ .register(registry);
+
+// Bridge OTel metrics into the same registry
+PrometheusMetricReader reader =
+ PrometheusMetricReader.create();
+registry.register(reader);
+
+OpenTelemetrySdk openTelemetry =
+ OpenTelemetrySdk.builder()
+ .setMeterProvider(
+ SdkMeterProvider.builder()
+ .registerMetricReader(reader)
+ .build())
+ .build();
+
+RuntimeMetrics runtimeMetrics =
+ RuntimeMetrics.builder(openTelemetry).build();
+Runtime.getRuntime()
+ .addShutdownHook(new Thread(runtimeMetrics::close));
+
+// Expose everything on one endpoint
+HTTPServer.builder()
+ .port(9400)
+ .registry(registry)
+ .buildAndStart();
+```
+
+The [examples/example-otel-jvm-runtime-metrics](https://github.com/prometheus/client_java/tree/main/examples/example-otel-jvm-runtime-metrics)
+directory has a complete runnable example.
+
+## Configuration
+
+The `RuntimeMetricsBuilder` supports two configuration
+options:
+
+### `captureGcCause()`
+
+Adds a `jvm.gc.cause` attribute to the `jvm.gc.duration`
+metric, indicating why the garbage collection occurred
+(e.g. `G1 Evacuation Pause`, `System.gc()`):
+
+```java
+RuntimeMetrics.builder(openTelemetry)
+ .captureGcCause()
+ .build();
+```
+
+### `emitExperimentalTelemetry()`
+
+Enables additional experimental metrics beyond the stable
+set. These are not yet part of the OTel semantic conventions
+and may change in future releases:
+
+- Buffer pool metrics (direct and mapped byte buffers)
+- Extended CPU metrics
+- Extended memory pool metrics
+- File descriptor metrics
+
+```java
+RuntimeMetrics.builder(openTelemetry)
+ .emitExperimentalTelemetry()
+ .build();
+```
+
+Both options can be combined:
+
+```java
+RuntimeMetrics.builder(openTelemetry)
+ .captureGcCause()
+ .emitExperimentalTelemetry()
+ .build();
+```
+
+Selective per-metric registration is not supported by the
+runtime-telemetry API — it is all-or-nothing with these
+two toggles.
+
+## Java 17 JFR Support
+
+The `opentelemetry-runtime-telemetry-java17` variant adds
+JFR-based metrics. You can selectively enable features:
+
+```java
+import io.opentelemetry.instrumentation.runtimemetrics.java17.JfrFeature;
+import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetrics;
+
+RuntimeMetrics.builder(openTelemetry)
+ .enableFeature(JfrFeature.BUFFER_METRICS)
+ .enableFeature(JfrFeature.NETWORK_IO_METRICS)
+ .enableFeature(JfrFeature.LOCK_METRICS)
+ .enableFeature(JfrFeature.CONTEXT_SWITCH_METRICS)
+ .build();
+```
+
+## Metric Names
+
+OTel metric names are converted to Prometheus format by
+the exporter. Examples:
+
+| OTel name | Prometheus name |
+| ---------------------------- | ---------------------------------- |
+| `jvm.memory.used` | `jvm_memory_used_bytes` |
+| `jvm.gc.duration` | `jvm_gc_duration_seconds` |
+| `jvm.thread.count` | `jvm_thread_count` |
+| `jvm.class.loaded` | `jvm_class_loaded` |
+| `jvm.cpu.recent_utilization` | `jvm_cpu_recent_utilization_ratio` |
+
+See [Names]({{< relref "names.md" >}}) for full details on
+how OTel names map to Prometheus names.
diff --git a/docs/content/otel/names.md b/docs/content/otel/names.md
index 2945d70e9..a5425e07f 100644
--- a/docs/content/otel/names.md
+++ b/docs/content/otel/names.md
@@ -17,7 +17,7 @@ the Prometheus server as if you had exposed Prometheus metrics directly.
The main steps when converting OpenTelemetry metric names to Prometheus metric names are:
-- Replace dots with underscores.
+- Escape illegal characters as described in [Unicode support]
- If the metric has a unit, append the unit to the metric name, like `_seconds`.
- If the metric type has a suffix, append it, like `_total` for counters.
@@ -29,14 +29,8 @@ OpenTelemetry's [Semantic Conventions for HTTP Metrics](https://opentelemetry.io
say that if you instrument an HTTP server with OpenTelemetry, you must have a histogram named
`http.server.duration`.
-Most names defined in semantic conventions use dots. In the Prometheus server, the dot is an illegal
-character (this might change in future versions of the Prometheus server).
+Most names defined in semantic conventions use dots.
+Dots in metric and label names are now supported in the Prometheus Java client library as
+described in [Unicode support].
-The Prometheus Java client library allows dots, so that you can use metric names and label names as
-defined in OpenTelemetry's semantic conventions.
-The dots will automatically be replaced with underscores if you expose metrics in Prometheus format,
-but you will see the original names with dots if you push your metrics in OpenTelemetry format.
-
-That way, you can use OTel-compliant metric and label names today when instrumenting your
-application with the Prometheus Java client, and you are prepared in case your monitoring backend
-adds features in the future that require OTel-compliant instrumentation.
+[Unicode support]: {{< relref "../exporters/unicode.md" >}}
diff --git a/docs/content/otel/support.md b/docs/content/otel/support.md
new file mode 100644
index 000000000..e3b8cbe3a
--- /dev/null
+++ b/docs/content/otel/support.md
@@ -0,0 +1,47 @@
+---
+title: OTel Support
+weight: 2
+---
+
+The `prometheus-metrics-otel-support` module bundles the
+OpenTelemetry SDK and the Prometheus exporter into a single
+POM dependency.
+
+Use this module when you want to combine OpenTelemetry
+instrumentations (e.g. JVM runtime metrics) with the
+Prometheus Java client on one `/metrics` endpoint.
+
+## Dependencies
+
+{{< tabs "otel-support-deps" >}}
+{{< tab "Gradle" >}}
+
+```groovy
+implementation 'io.prometheus:prometheus-metrics-otel-support:$version'
+```
+
+{{< /tab >}}
+{{< tab "Maven" >}}
+
+```xml
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ $version
+ pom
+
+```
+
+{{< /tab >}}
+{{< /tabs >}}
+
+This single dependency replaces:
+
+- `io.opentelemetry:opentelemetry-sdk`
+- `io.opentelemetry:opentelemetry-exporter-prometheus`
+
+## Use Cases
+
+See [JVM Runtime Metrics]({{< relref "jvm-runtime-metrics.md" >}})
+for a concrete example of combining OTel JVM metrics with
+the Prometheus Java client.
diff --git a/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html b/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html
index e4a71eb4e..7e49ef8c7 100644
--- a/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html
+++ b/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html
@@ -1,70 +1,58 @@
{{ $isPage := or (and (ne .Type "posts") (in "section page" .Kind )) (and (eq .Type "posts") (eq .Kind "page")) }}
{{- if eq .Kind "home" }}
+ {{- $schema := dict "@context" "http://schema.org" "@type" "WebSite" "name" .Site.Title "url" .Site.BaseURL "inLanguage" .Lang }}
+ {{- with partial "utils/description" . }}
+ {{- $schema = merge $schema (dict "description" (. | plainify | htmlUnescape | chomp)) }}
+ {{- end }}
+ {{- with partial "utils/featured" . }}
+ {{- $schema = merge $schema (dict "thumbnailUrl" .) }}
+ {{- end }}
+ {{- with .Site.Params.geekdocContentLicense }}
+ {{- $schema = merge $schema (dict "license" .name) }}
+ {{- end }}
{{- else if $isPage }}
+ {{- $title := partial "utils/title" . }}
+ {{- $schema := dict
+ "@context" "http://schema.org"
+ "@type" "TechArticle"
+ "articleSection" (.Section | humanize | title)
+ "name" $title
+ "url" .Permalink
+ "headline" $title
+ "wordCount" (string .WordCount)
+ "inLanguage" .Lang
+ "isFamilyFriendly" "true"
+ "copyrightHolder" .Site.Title
+ "copyrightYear" (.Date.Format "2006")
+ "dateCreated" (.Date.Format "2006-01-02T15:04:05.00Z")
+ "datePublished" (.PublishDate.Format "2006-01-02T15:04:05.00Z")
+ "dateModified" (.Lastmod.Format "2006-01-02T15:04:05.00Z")
+ }}
+ {{- with .Params.lead }}
+ {{- $schema = merge $schema (dict "alternativeHeadline" .) }}
+ {{- end }}
+ {{- with partial "utils/description" . }}
+ {{- $schema = merge $schema (dict "description" (. | plainify | htmlUnescape | chomp)) }}
+ {{- end }}
+ {{- with partial "utils/featured" . }}
+ {{- $schema = merge $schema (dict "thumbnailUrl" .) }}
+ {{- end }}
+ {{- with .Site.Params.geekdocContentLicense }}
+ {{- $schema = merge $schema (dict "license" .name) }}
+ {{- end }}
+ {{- $mainEntity := dict "@type" "WebPage" "@id" .Permalink }}
+ {{- $schema = merge $schema (dict "mainEntityOfPage" $mainEntity) }}
+ {{- with $tags := .Params.tags }}
+ {{- $schema = merge $schema (dict "keywords" $tags) }}
+ {{- end }}
+ {{- $logoUrl := default "brand.svg" .Site.Params.logo | absURL }}
+ {{- $logo := dict "@type" "ImageObject" "url" $logoUrl "width" "32" "height" "32" }}
+ {{- $publisher := dict "@type" "Organization" "name" .Site.Title "url" .Site.BaseURL "logo" $logo }}
+ {{- $schema = merge $schema (dict "publisher" $publisher) }}
{{- end }}
diff --git a/examples/example-custom-buckets/README.md b/examples/example-custom-buckets/README.md
new file mode 100644
index 000000000..a7a6a8564
--- /dev/null
+++ b/examples/example-custom-buckets/README.md
@@ -0,0 +1,170 @@
+# Native Histograms with Custom Buckets (NHCB) Example
+
+This example demonstrates how to use native histograms with custom bucket boundaries (NHCB) in
+Prometheus Java client. It shows three different types of custom bucket configurations and how
+Prometheus converts them to native histograms with schema -53.
+
+## What are Native Histograms with Custom Buckets?
+
+Native Histograms with Custom Buckets (NHCB) is a Prometheus feature that combines the benefits of:
+
+- **Custom bucket boundaries**: Precisely defined buckets optimized for your specific use case
+- **Native histograms**: Efficient storage and querying capabilities of native histograms
+
+When you configure Prometheus with `convert_classic_histograms_to_nhcb: true`, it converts classic
+histograms with custom buckets into native histograms using schema -53, preserving the custom
+bucket boundaries.
+
+## Example Metrics
+
+This example application generates three different histogram metrics demonstrating different
+bucket configuration strategies:
+
+### 1. API Latency - Arbitrary Custom Boundaries
+
+```java
+Histogram apiLatency = Histogram.builder()
+ .name("api_request_duration_seconds")
+ .classicUpperBounds(0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0)
+ .register();
+```
+
+**Use case**: Optimized for typical API response times in seconds.
+
+### 2. Queue Size - Linear Boundaries
+
+```java
+Histogram queueSize = Histogram.builder()
+ .name("message_queue_size")
+ .classicLinearUpperBounds(10, 10, 10) // 10, 20, 30, ..., 100
+ .register();
+```
+
+**Use case**: Equal-width buckets for monitoring queue depth or other discrete values.
+
+### 3. Response Size - Exponential Boundaries
+
+```java
+Histogram responseSize = Histogram.builder()
+ .name("http_response_size_bytes")
+ .classicExponentialUpperBounds(100, 10, 6) // 100, 1k, 10k, 100k, 1M, 10M
+ .register();
+```
+
+**Use case**: Data spanning multiple orders of magnitude (bytes, milliseconds, etc).
+
+## Build
+
+This example is built as part of the `client_java` project:
+
+```shell
+./mvnw package
+```
+
+This creates `./examples/example-custom-buckets/target/example-custom-buckets.jar`.
+
+## Run
+
+With the JAR file present, run:
+
+```shell
+cd ./examples/example-custom-buckets/
+docker-compose up
+```
+
+This starts three Docker containers:
+
+- **[http://localhost:9400/metrics](http://localhost:9400/metrics)** - Example application
+- **[http://localhost:9090](http://localhost:9090)** - Prometheus server (with NHCB enabled)
+- **[http://localhost:3000](http://localhost:3000)** - Grafana (user: _admin_, password: _admin_)
+
+You might need to replace `localhost` with `host.docker.internal` on macOS or Windows.
+
+## Verify NHCB Conversion
+
+### 1. Check Prometheus Configuration
+
+The Prometheus configuration enables NHCB conversion:
+
+```yaml
+scrape_configs:
+ - job_name: "custom-buckets-demo"
+ scrape_protocols: ["PrometheusProto"]
+ convert_classic_histograms_to_nhcb: true
+ scrape_classic_histograms: true
+```
+
+### 2. Verify in Prometheus
+
+Visit [http://localhost:9090](http://localhost:9090) and run queries:
+
+```promql
+# View histogram metadata (should show schema -53 for NHCB)
+prometheus_tsdb_head_series
+
+# Calculate quantiles from custom buckets
+histogram_quantile(0.95, rate(api_request_duration_seconds[1m]))
+
+# View raw histogram structure
+api_request_duration_seconds
+```
+
+### 3. View in Grafana
+
+The Grafana dashboard at [http://localhost:3000](http://localhost:3000) shows:
+
+- p95 and p50 latencies for API endpoints (arbitrary custom buckets)
+- Queue size distribution (linear buckets)
+- Response size distribution (exponential buckets)
+
+## Key Observations
+
+1. **Custom Buckets Preserved**: The custom bucket boundaries you define are preserved when
+ converted to NHCB (schema -53).
+
+2. **Dual Representation**: By default, histograms maintain both classic and native
+ representations, allowing gradual migration.
+
+3. **Efficient Storage**: Native histograms provide more efficient storage than classic histograms
+ while preserving your custom bucket boundaries.
+
+4. **Flexible Bucket Strategies**: You can choose arbitrary, linear, or exponential buckets based
+ on your specific monitoring needs.
+
+## When to Use Custom Buckets
+
+Consider using custom buckets (and NHCB) when:
+
+- **Precise boundaries needed**: You know the expected distribution and want specific bucket edges
+- **Migrating from classic histograms**: You want to preserve existing bucket boundaries
+- **Specific use cases**: Default exponential bucketing doesn't fit your distribution well
+ - Temperature ranges (might include negative values)
+ - Queue depths (discrete values with linear growth)
+ - File sizes (exponential growth but with specific thresholds)
+ - API latencies (specific SLA boundaries)
+
+## Differences from Standard Native Histograms
+
+| Feature | Standard Native Histograms | NHCB (Schema -53) |
+| ----------------- | ------------------------------- | --------------------------------- |
+| Bucket boundaries | Exponential (base 2^(2^-scale)) | Custom boundaries |
+| Use case | General-purpose | Specific distributions |
+| Mergeability | Can merge with same schema | Cannot merge different boundaries |
+| Configuration | Schema level (0-8) | Explicit boundary list |
+
+## Cleanup
+
+Stop the containers:
+
+```shell
+docker-compose down
+```
+
+## Further Reading
+
+
+
+
+- [Prometheus Native Histograms Specification](https://prometheus.io/docs/specs/native_histograms/)
+- [Prometheus Java Client Documentation](https://prometheus.github.io/client_java/)
+- [OpenTelemetry Exponential Histograms](https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram)
diff --git a/examples/example-custom-buckets/docker-compose.yaml b/examples/example-custom-buckets/docker-compose.yaml
new file mode 100644
index 000000000..b024481af
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose.yaml
@@ -0,0 +1,26 @@
+version: "3"
+services:
+ example-application:
+ image: eclipse-temurin:25.0.2_10-jre@sha256:0a9c973778b03b88f39ccae4f8cc26022d84a3237a818cb98770369eb6c5daf9
+ network_mode: host
+ volumes:
+ - ./target/example-custom-buckets.jar:/example-custom-buckets.jar
+ command:
+ - /opt/java/openjdk/bin/java
+ - -jar
+ - /example-custom-buckets.jar
+ prometheus:
+ image: prom/prometheus:v3.9.1@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702
+ network_mode: host
+ volumes:
+ - ./docker-compose/prometheus.yml:/prometheus.yml
+ command:
+ - --enable-feature=native-histograms
+ - --config.file=/prometheus.yml
+ grafana:
+ image: grafana/grafana:12.3.3@sha256:9e1e77ade304069aee3196e9a4f210830e96e80ce9a2640891eccc324b152faf
+ network_mode: host
+ volumes:
+ - ./docker-compose/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/grafana-datasources.yaml
+ - ./docker-compose/grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yaml
+ - ./docker-compose/grafana-dashboard-custom-buckets.json:/etc/grafana/grafana-dashboard-custom-buckets.json
diff --git a/examples/example-custom-buckets/docker-compose/grafana-dashboard-custom-buckets.json b/examples/example-custom-buckets/docker-compose/grafana-dashboard-custom-buckets.json
new file mode 100644
index 000000000..11ae25775
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/grafana-dashboard-custom-buckets.json
@@ -0,0 +1,349 @@
+{
+ "annotations": {
+ "list": []
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "API request duration with custom bucket boundaries (0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0 seconds). Shows how custom buckets are preserved in NHCB (schema -53).",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "legend": {
+ "calcs": ["mean", "max"],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(api_request_duration_seconds[1m]))",
+ "instant": false,
+ "legendFormat": "{{endpoint}} {{status}} (p95)",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.5, rate(api_request_duration_seconds[1m]))",
+ "hide": false,
+ "instant": false,
+ "legendFormat": "{{endpoint}} {{status}} (p50)",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "API Latency - Custom Buckets (Arbitrary Boundaries)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Queue size with linear bucket boundaries (10, 20, 30, ..., 100). Demonstrates equal-width buckets for discrete values.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 8
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": ["mean", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(message_queue_size[1m]))",
+ "instant": false,
+ "legendFormat": "{{queue_name}} (p95)",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.5, rate(message_queue_size[1m]))",
+ "hide": false,
+ "instant": false,
+ "legendFormat": "{{queue_name}} (p50)",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "Queue Size - Linear Buckets",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "HTTP response size with exponential bucket boundaries (100, 1k, 10k, 100k, 1M, 10M bytes). Shows exponential growth for data spanning multiple orders of magnitude.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 8
+ },
+ "id": 3,
+ "options": {
+ "legend": {
+ "calcs": ["mean", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(http_response_size_bytes[1m]))",
+ "instant": false,
+ "legendFormat": "{{endpoint}} (p95)",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.5, rate(http_response_size_bytes[1m]))",
+ "hide": false,
+ "instant": false,
+ "legendFormat": "{{endpoint}} (p50)",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "Response Size - Exponential Buckets",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "5s",
+ "schemaVersion": 39,
+ "tags": ["custom-buckets", "nhcb", "native-histogram"],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-5m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "browser",
+ "title": "Native Histograms with Custom Buckets (NHCB)",
+ "uid": "custom-buckets-nhcb",
+ "version": 1,
+ "weekStart": ""
+}
diff --git a/examples/example-custom-buckets/docker-compose/grafana-dashboards.yaml b/examples/example-custom-buckets/docker-compose/grafana-dashboards.yaml
new file mode 100644
index 000000000..3225b88ae
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/grafana-dashboards.yaml
@@ -0,0 +1,8 @@
+apiVersion: 1
+
+providers:
+ - name: "Custom Buckets (NHCB) Example"
+ type: file
+ options:
+ path: /etc/grafana/grafana-dashboard-custom-buckets.json
+ foldersFromFilesStructure: false
diff --git a/examples/example-custom-buckets/docker-compose/grafana-datasources.yaml b/examples/example-custom-buckets/docker-compose/grafana-datasources.yaml
new file mode 100644
index 000000000..d442d28d2
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/grafana-datasources.yaml
@@ -0,0 +1,7 @@
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ type: prometheus
+ uid: prometheus
+ url: http://localhost:9090
diff --git a/examples/example-custom-buckets/docker-compose/prometheus.yml b/examples/example-custom-buckets/docker-compose/prometheus.yml
new file mode 100644
index 000000000..5c5782023
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/prometheus.yml
@@ -0,0 +1,14 @@
+---
+global:
+ scrape_interval: 5s # very short interval for demo purposes
+
+scrape_configs:
+ - job_name: "custom-buckets-demo"
+ # Use protobuf format to receive native histogram data
+ scrape_protocols: ["PrometheusProto"]
+ # Convert classic histograms with custom buckets to NHCB (schema -53)
+ convert_classic_histograms_to_nhcb: true
+ # Also scrape classic histograms for comparison
+ scrape_classic_histograms: true
+ static_configs:
+ - targets: ["localhost:9400"]
diff --git a/examples/example-custom-buckets/pom.xml b/examples/example-custom-buckets/pom.xml
new file mode 100644
index 000000000..ca4c52843
--- /dev/null
+++ b/examples/example-custom-buckets/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+
+ io.prometheus
+ example-custom-buckets
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+
+
+ Example - Custom Buckets
+
+ End-to-End example of Native Histograms with Custom Buckets (NHCB): Java app -> Prometheus -> Grafana
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-core
+
+
+ io.prometheus
+ prometheus-metrics-instrumentation-jvm
+
+
+ io.prometheus
+ prometheus-metrics-exporter-httpserver
+
+
+
+
+ ${project.artifactId}
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ io.prometheus.metrics.examples.custombuckets.Main
+
+
+
+
+
+
+
+
+
diff --git a/examples/example-custom-buckets/src/main/java/io/prometheus/metrics/examples/custombuckets/Main.java b/examples/example-custom-buckets/src/main/java/io/prometheus/metrics/examples/custombuckets/Main.java
new file mode 100644
index 000000000..3d286fdf0
--- /dev/null
+++ b/examples/example-custom-buckets/src/main/java/io/prometheus/metrics/examples/custombuckets/Main.java
@@ -0,0 +1,108 @@
+package io.prometheus.metrics.examples.custombuckets;
+
+import io.prometheus.metrics.core.metrics.Histogram;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.instrumentation.jvm.JvmMetrics;
+import io.prometheus.metrics.model.snapshots.Unit;
+import java.io.IOException;
+import java.util.Random;
+
+/**
+ * Example demonstrating native histograms with custom buckets (NHCB).
+ *
+ *
This example shows three different types of custom bucket configurations:
+ *
+ *
+ *
API latency with arbitrary custom boundaries optimized for typical response times
+ *
Queue size with linear boundaries for equal-width buckets
+ *
Response size with exponential boundaries for data spanning multiple orders of magnitude
+ *
+ *
+ *
These histograms maintain both classic (with custom buckets) and native representations. When
+ * Prometheus is configured with {@code convert_classic_histograms_to_nhcb: true}, the custom bucket
+ * boundaries are preserved in the native histogram format (schema -53).
+ */
+public class Main {
+
+ public static void main(String[] args) throws IOException, InterruptedException {
+
+ JvmMetrics.builder().register();
+
+ // Example 1: API latency with arbitrary custom boundaries
+ // Optimized for typical API response times in seconds
+ Histogram apiLatency =
+ Histogram.builder()
+ .name("api_request_duration_seconds")
+ .help("API request duration with custom buckets")
+ .unit(Unit.SECONDS)
+ .classicUpperBounds(0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0)
+ .labelNames("endpoint", "status")
+ .register();
+
+ // Example 2: Queue size with linear boundaries
+ // Equal-width buckets for monitoring queue depth
+ Histogram queueSize =
+ Histogram.builder()
+ .name("message_queue_size")
+ .help("Number of messages in queue with linear buckets")
+ .classicLinearUpperBounds(10, 10, 10) // 10, 20, 30, ..., 100
+ .labelNames("queue_name")
+ .register();
+
+ // Example 3: Response size with exponential boundaries
+ // Exponential growth for data spanning multiple orders of magnitude
+ Histogram responseSize =
+ Histogram.builder()
+ .name("http_response_size_bytes")
+ .help("HTTP response size in bytes with exponential buckets")
+ .classicExponentialUpperBounds(100, 10, 6) // 100, 1k, 10k, 100k, 1M, 10M
+ .labelNames("endpoint")
+ .register();
+
+ HTTPServer server = HTTPServer.builder().port(9400).buildAndStart();
+
+ System.out.println(
+ "HTTPServer listening on port http://localhost:" + server.getPort() + "/metrics");
+ System.out.println("\nGenerating metrics with custom bucket configurations:");
+ System.out.println("1. API latency: custom boundaries optimized for response times");
+ System.out.println("2. Queue size: linear boundaries (10, 20, 30, ..., 100)");
+ System.out.println("3. Response size: exponential boundaries (100, 1k, 10k, ..., 10M)");
+ System.out.println("\nPrometheus will convert these to NHCB (schema -53) when configured.\n");
+
+ Random random = new Random(0);
+
+ while (true) {
+ // Simulate API latency observations
+ // Fast endpoint: mostly < 100ms, occasionally slow
+ double fastLatency = Math.abs(random.nextGaussian() * 0.03 + 0.05);
+ String status = random.nextInt(100) < 95 ? "200" : "500";
+ apiLatency.labelValues("/api/fast", status).observe(fastLatency);
+
+ // Slow endpoint: typically 1-3 seconds
+ double slowLatency = Math.abs(random.nextGaussian() * 0.5 + 2.0);
+ apiLatency.labelValues("/api/slow", status).observe(slowLatency);
+
+ // Simulate queue size observations
+ // Queue oscillates between 20-80 items
+ int queueDepth = 50 + (int) (random.nextGaussian() * 15);
+ queueDepth = Math.max(0, Math.min(100, queueDepth));
+ queueSize.labelValues("default").observe(queueDepth);
+
+ // Priority queue: usually smaller
+ int priorityQueueDepth = 10 + (int) (random.nextGaussian() * 5);
+ priorityQueueDepth = Math.max(0, Math.min(50, priorityQueueDepth));
+ queueSize.labelValues("priority").observe(priorityQueueDepth);
+
+ // Simulate response size observations
+ // Small responses: mostly < 10KB
+ double smallResponse = Math.abs(random.nextGaussian() * 2000 + 5000);
+ responseSize.labelValues("/api/summary").observe(smallResponse);
+
+ // Large responses: can be up to several MB
+ double largeResponse = Math.abs(random.nextGaussian() * 200000 + 500000);
+ responseSize.labelValues("/api/download").observe(largeResponse);
+
+ Thread.sleep(1000);
+ }
+ }
+}
diff --git a/examples/example-exemplars-tail-sampling/docker-compose.yaml b/examples/example-exemplars-tail-sampling/docker-compose.yaml
index 305bf9516..dee885885 100644
--- a/examples/example-exemplars-tail-sampling/docker-compose.yaml
+++ b/examples/example-exemplars-tail-sampling/docker-compose.yaml
@@ -36,14 +36,14 @@ services:
- -jar
- /example-greeting-service.jar
collector:
- image: otel/opentelemetry-collector-contrib:0.130.0@sha256:867d1074c2f750936fb9358ec9eefa009308053cf156b2c7ca1761ba5ef78452
+ image: otel/opentelemetry-collector-contrib:0.146.1@sha256:f6e429c1052ab50f85a7afa5f7e32f25931697751622b0e1f453d10f79a1df3c
network_mode: host
volumes:
- ./config/otelcol-config.yaml:/config.yaml
command:
- --config=file:/config.yaml
prometheus:
- image: prom/prometheus:v3.5.0@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
+ image: prom/prometheus:v3.9.1@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702
network_mode: host
volumes:
- ./config/prometheus.yaml:/prometheus.yaml
@@ -52,14 +52,14 @@ services:
- --enable-feature=native-histograms
- --config.file=/prometheus.yaml
tempo:
- image: grafana/tempo:2.8.1@sha256:bc9245fe3da4e63dc4c6862d9c2dad9bcd8be13d0ba4f7705fa6acda4c904d0e
+ image: grafana/tempo:2.10.1@sha256:9371af1b75b4e057eb77f22dc4dd4d9176cd6985e29f181527be6723b7f29c41
network_mode: host
volumes:
- ./config/tempo-config.yaml:/config.yaml
command:
- --config.file=/config.yaml
grafana:
- image: grafana/grafana:12.0.2@sha256:b5b59bfc7561634c2d7b136c4543d702ebcc94a3da477f21ff26f89ffd4214fa
+ image: grafana/grafana:12.3.3@sha256:9e1e77ade304069aee3196e9a4f210830e96e80ce9a2640891eccc324b152faf
network_mode: host
ports:
- "3000:3000"
@@ -68,7 +68,7 @@ services:
- ./config/grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yaml
- ./config/grafana-example-dashboard.json:/etc/grafana/example-dashboard.json
k6:
- image: grafana/k6@sha256:b1625f686ef1c733340b00de57bce840e0b4b1f7e545c58305a5db53e7ad3797
+ image: grafana/k6@sha256:5e937f439684142ba7803722b42e3c9ac9233cfa01d561de0596c1c2794fd680
network_mode: host
volumes:
- ./config/k6-script.js:/k6-script.js
diff --git a/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml b/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml
index d1938f242..8621a7e19 100644
--- a/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml
+++ b/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml
@@ -4,13 +4,14 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- example-exemplars-tail-sampling
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-greeting-service
+ 1.0-SNAPSHOT
+
+
+ 17
+ UTF-8
+ Example - OpenTelemetry Exemplars - Greeting Service
@@ -18,30 +19,35 @@
tracing
-
- 17
-
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+ io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-servlet-jakarta
- ${project.version}org.apache.tomcat.embedtomcat-embed-core
- 11.0.9
+ 11.0.18
diff --git a/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml b/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml
index f20493cff..df861667f 100644
--- a/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml
+++ b/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml
@@ -4,13 +4,14 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- example-exemplars-tail-sampling
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-hello-world-app
+ 1.0-SNAPSHOT
+
+
+ 17
+ UTF-8
+ Example - OpenTelemetry Exemplars - Hello World App
@@ -18,30 +19,35 @@
tracing
-
- 17
-
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+ io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-servlet-jakarta
- ${project.version}org.apache.tomcat.embedtomcat-embed-core
- 11.0.9
+ 11.0.18
diff --git a/examples/example-exemplars-tail-sampling/pom.xml b/examples/example-exemplars-tail-sampling/pom.xml
index 8c0c7441c..ac4c6ccf5 100644
--- a/examples/example-exemplars-tail-sampling/pom.xml
+++ b/examples/example-exemplars-tail-sampling/pom.xml
@@ -4,24 +4,16 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-exemplars-tail-sampling
+ 1.0-SNAPSHOTpomExample - Exemplars with OpenTelemetry's Tail Sampling
- Example project showing Examplars with OpenTelemetry's Tail Sampling.
+ Example project showing Exemplars with OpenTelemetry's Tail Sampling.
-
- 11
-
-
example-greeting-serviceexample-hello-world-app
diff --git a/examples/example-exporter-httpserver/pom.xml b/examples/example-exporter-httpserver/pom.xml
index 6d0b75560..7160339ad 100644
--- a/examples/example-exporter-httpserver/pom.xml
+++ b/examples/example-exporter-httpserver/pom.xml
@@ -4,34 +4,44 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-exporter-httpserver
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+ Example - HTTPServer Exporter
Prometheus Metrics Example using the HTTPServer for exposing the metrics endpoint
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+
+
io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-httpserver
- ${project.version}
diff --git a/examples/example-exporter-multi-target/pom.xml b/examples/example-exporter-multi-target/pom.xml
index 75855a9c1..e5ee1bf83 100644
--- a/examples/example-exporter-multi-target/pom.xml
+++ b/examples/example-exporter-multi-target/pom.xml
@@ -4,34 +4,44 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-exporter-multi-target
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+ Example - HTTPServer Exporter Multi Target
Prometheus Metrics Example for multi-target pattern implementation
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+
+
io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-httpserver
- ${project.version}
diff --git a/examples/example-exporter-opentelemetry/docker-compose.yaml b/examples/example-exporter-opentelemetry/docker-compose.yaml
index 2d3367e1c..97bc9ab9b 100644
--- a/examples/example-exporter-opentelemetry/docker-compose.yaml
+++ b/examples/example-exporter-opentelemetry/docker-compose.yaml
@@ -13,14 +13,14 @@ services:
#- -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005
- /example-exporter-opentelemetry.jar
collector:
- image: otel/opentelemetry-collector-contrib:0.130.0@sha256:867d1074c2f750936fb9358ec9eefa009308053cf156b2c7ca1761ba5ef78452
+ image: otel/opentelemetry-collector-contrib:0.146.1@sha256:f6e429c1052ab50f85a7afa5f7e32f25931697751622b0e1f453d10f79a1df3c
network_mode: host
volumes:
- ./config/otelcol-config.yaml:/config.yaml
command:
- --config=file:/config.yaml
prometheus:
- image: prom/prometheus:v3.5.0@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
+ image: prom/prometheus:v3.9.1@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702
network_mode: host
volumes:
- ./config/prometheus.yaml:/prometheus.yaml
diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile b/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile
index 6ea7b6ca9..bc1adac60 100644
--- a/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile
+++ b/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile
@@ -1,8 +1,8 @@
-FROM eclipse-temurin:21.0.7_6-jre@sha256:bca347dc76e38a60a1a01b29a7d1312e514603a97ba594268e5a2e4a1a0c9a8f
+FROM eclipse-temurin:25.0.2_10-jre@sha256:0a9c973778b03b88f39ccae4f8cc26022d84a3237a818cb98770369eb6c5daf9
COPY target/example-exporter-opentelemetry.jar ./app.jar
# check that the resource attributes from the agent are used, epsecially the service.instance.id should be the same
-ADD --chmod=644 https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.8.0/opentelemetry-javaagent.jar /usr/src/app/opentelemetry-javaagent.jar
+ADD --chmod=644 https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.21.0/opentelemetry-javaagent.jar /usr/src/app/opentelemetry-javaagent.jar
ENV JAVA_TOOL_OPTIONS=-javaagent:/usr/src/app/opentelemetry-javaagent.jar
#ENTRYPOINT [ "java", "-Dotel.javaagent.debug=true","-jar", "./app.jar" ] # for debugging
diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml b/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml
index 9c380dea9..899d1cd5d 100644
--- a/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml
+++ b/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml
@@ -1,5 +1,6 @@
# OATS is an acceptance testing framework for OpenTelemetry -
# https://github.com/grafana/oats/tree/main/yaml
+oats-schema-version: 2
docker-compose:
files:
- ./docker-compose.yml
diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/service_instance_id_check.py b/examples/example-exporter-opentelemetry/oats-tests/agent/service_instance_id_check.py
index 1cfe2513f..35ff88b8d 100755
--- a/examples/example-exporter-opentelemetry/oats-tests/agent/service_instance_id_check.py
+++ b/examples/example-exporter-opentelemetry/oats-tests/agent/service_instance_id_check.py
@@ -1,48 +1,52 @@
#!/usr/bin/env python3
-
-"""This script is used to check if the service instance id is present in the exported data
-The script will return 0 if the service instance id is present in the exported data"""
+"""
+Check if the service instance id is present in the exported data.
+Returns 0 if the service instance id is present in the exported data.
+"""
import json
import urllib.parse
from urllib.request import urlopen
-def get(url):
- global response, res
+def get_json(url):
with urlopen(url) as response:
- # read the response
- res = response.read()
- # decode the response
- res = json.loads(res.decode("utf-8"))
- return res
+ return json.loads(response.read().decode("utf-8"))
-res = get(" http://localhost:9090/api/v1/query?query=target_info")
+def main():
+ # Query Prometheus for target_info
+ res = get_json("http://localhost:9090/api/v1/query?query=target_info")
-# uncomment the following line to use the local file instead of the url - for debugging
-# with open('example_target_info.json') as f:
-# res = json.load(f)
+ # Uncomment for local debugging
+ # with open('example_target_info.json') as f:
+ # res = json.load(f)
-values = list(
- {
+ instance_ids = {
r["metric"]["instance"]
for r in res["data"]["result"]
- if not r["metric"]["service_name"] == "otelcol-contrib"
+ if r["metric"].get("service_name") != "otelcol-contrib"
}
-)
-print(values)
+ instance_ids = list(instance_ids)
+
+ print(f"Instance ids found:{instance_ids}")
+ if len(instance_ids) > 1:
+ print("More than one instance id found")
+ print(res)
+
+ # Both the agent and the exporter should report the same instance id
+ assert len(instance_ids) == 1, "Expected exactly one instance id"
+
+ query = f'target_info{{instance="{instance_ids[0]}"}}'
+ encoded_query = urllib.parse.quote_plus(query)
+ res = get_json(f"http://localhost:9090/api/v1/query?query={encoded_query}")
-# both the agent and the exporter should report the same instance id
-assert len(values) == 1
+ infos = res["data"]["result"]
+ print(infos)
-path = f'target_info{{instance="{values[0]}"}}'
-path = urllib.parse.quote_plus(path)
-res = get(f"http://localhost:9090/api/v1/query?query={path}")
+ # They should not have the same target info (e.g. only the agent has telemetry_distro_name)
+ assert len(infos) == 2, "Expected two target info results"
-infos = res["data"]["result"]
-print(infos)
-# they should not have the same target info
-# e.g. only the agent has telemetry_distro_name
-assert len(infos) == 2
+if __name__ == "__main__":
+ main()
diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile b/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile
index 88947a9d9..763ba191f 100644
--- a/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile
+++ b/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile
@@ -1,4 +1,4 @@
-FROM eclipse-temurin:21.0.7_6-jre@sha256:bca347dc76e38a60a1a01b29a7d1312e514603a97ba594268e5a2e4a1a0c9a8f
+FROM eclipse-temurin:25.0.2_10-jre@sha256:0a9c973778b03b88f39ccae4f8cc26022d84a3237a818cb98770369eb6c5daf9
COPY target/example-exporter-opentelemetry.jar ./app.jar
diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml b/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml
index 66430ca3b..dbcfcf84f 100644
--- a/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml
+++ b/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml
@@ -1,5 +1,6 @@
# OATS is an acceptance testing framework for OpenTelemetry -
# https://github.com/grafana/oats/tree/main/yaml
+oats-schema-version: 2
docker-compose:
files:
- ./docker-compose.yml
diff --git a/examples/example-exporter-opentelemetry/pom.xml b/examples/example-exporter-opentelemetry/pom.xml
index 4c9d1d143..7c7501a57 100644
--- a/examples/example-exporter-opentelemetry/pom.xml
+++ b/examples/example-exporter-opentelemetry/pom.xml
@@ -4,34 +4,44 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-exporter-opentelemetry
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+ Example - OpenTelemetry Metrics Exporter
Example of exposing metrics in OpenTelemetry format and pushing them to an OpenTelemetry collector
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+
+
io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-opentelemetry
- ${project.version}
diff --git a/examples/example-exporter-servlet-tomcat/README.md b/examples/example-exporter-servlet-tomcat/README.md
index 05ac894e3..01e76832d 100644
--- a/examples/example-exporter-servlet-tomcat/README.md
+++ b/examples/example-exporter-servlet-tomcat/README.md
@@ -71,7 +71,7 @@ browser:
static_configs:
- targets: ["localhost:8080"]
```
-4. Run with native histograms and examplars enabled:
+4. Run with native histograms and exemplars enabled:
```shell
./prometheus --enable-feature=native-histograms --enable-feature=exemplar-storage
```
diff --git a/examples/example-exporter-servlet-tomcat/pom.xml b/examples/example-exporter-servlet-tomcat/pom.xml
index efb8cdd63..ab5ba0198 100644
--- a/examples/example-exporter-servlet-tomcat/pom.xml
+++ b/examples/example-exporter-servlet-tomcat/pom.xml
@@ -3,43 +3,49 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-exporter-servlet-tomcat
+ 1.0-SNAPSHOT
+
+
+ 17
+ UTF-8
+ Example - Servlet Exporter with Tomcat
Prometheus Metrics Example using Embedded Tomcat and the Exporter Servlet
-
- 17
-
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+ io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-servlet-jakarta
- ${project.version}org.apache.tomcat.embedtomcat-embed-core
- 11.0.9
+ 11.0.18
diff --git a/examples/example-native-histogram/docker-compose.yaml b/examples/example-native-histogram/docker-compose.yaml
index 8490934bf..492ce015f 100644
--- a/examples/example-native-histogram/docker-compose.yaml
+++ b/examples/example-native-histogram/docker-compose.yaml
@@ -1,7 +1,7 @@
version: "3"
services:
example-application:
- image: eclipse-temurin:21.0.7_6-jre@sha256:bca347dc76e38a60a1a01b29a7d1312e514603a97ba594268e5a2e4a1a0c9a8f
+ image: eclipse-temurin:25.0.2_10-jre@sha256:0a9c973778b03b88f39ccae4f8cc26022d84a3237a818cb98770369eb6c5daf9
network_mode: host
volumes:
- ./target/example-native-histogram.jar:/example-native-histogram.jar
@@ -10,7 +10,7 @@ services:
- -jar
- /example-native-histogram.jar
prometheus:
- image: prom/prometheus:v3.5.0@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
+ image: prom/prometheus:v3.9.1@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702
network_mode: host
volumes:
- ./docker-compose/prometheus.yml:/prometheus.yml
@@ -18,7 +18,7 @@ services:
- --enable-feature=native-histograms
- --config.file=/prometheus.yml
grafana:
- image: grafana/grafana:12.0.2@sha256:b5b59bfc7561634c2d7b136c4543d702ebcc94a3da477f21ff26f89ffd4214fa
+ image: grafana/grafana:12.3.3@sha256:9e1e77ade304069aee3196e9a4f210830e96e80ce9a2640891eccc324b152faf
network_mode: host
volumes:
- ./docker-compose/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/grafana-datasources.yaml
diff --git a/examples/example-native-histogram/pom.xml b/examples/example-native-histogram/pom.xml
index 9ad73b092..7d799cc52 100644
--- a/examples/example-native-histogram/pom.xml
+++ b/examples/example-native-histogram/pom.xml
@@ -3,34 +3,44 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-native-histogram
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+ Example - Native Histogram
End-to-End example of a Native histogram: Java app -> Prometheus -> Grafana
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+
+
io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-httpserver
- ${project.version}
diff --git a/examples/example-otel-jvm-runtime-metrics/README.md b/examples/example-otel-jvm-runtime-metrics/README.md
new file mode 100644
index 000000000..a58584694
--- /dev/null
+++ b/examples/example-otel-jvm-runtime-metrics/README.md
@@ -0,0 +1,41 @@
+# OTel JVM Runtime Metrics with Prometheus HTTPServer
+
+## Build
+
+This example is built as part of the `client_java` project.
+
+```shell
+./mvnw package
+```
+
+## Run
+
+The build creates a JAR file with the example application in
+`./examples/example-otel-jvm-runtime-metrics/target/`.
+
+```shell
+java -jar ./examples/example-otel-jvm-runtime-metrics/target/example-otel-jvm-runtime-metrics.jar
+```
+
+## Manually Testing the Metrics Endpoint
+
+Accessing
+[http://localhost:9400/metrics](http://localhost:9400/metrics)
+with a Web browser should yield both a Prometheus counter metric
+and OTel JVM runtime metrics on the same endpoint.
+
+Prometheus counter:
+
+```text
+# HELP uptime_seconds_total total number of seconds since this application was started
+# TYPE uptime_seconds_total counter
+uptime_seconds_total 42.0
+```
+
+OTel JVM runtime metrics (excerpt):
+
+```text
+# HELP jvm_memory_used_bytes Measure of memory used.
+# TYPE jvm_memory_used_bytes gauge
+jvm_memory_used_bytes{jvm_memory_pool_name="G1 Eden Space",jvm_memory_type="heap"} 4194304.0
+```
diff --git a/examples/example-otel-jvm-runtime-metrics/pom.xml b/examples/example-otel-jvm-runtime-metrics/pom.xml
new file mode 100644
index 000000000..58869bfd7
--- /dev/null
+++ b/examples/example-otel-jvm-runtime-metrics/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+
+ io.prometheus
+ example-otel-jvm-runtime-metrics
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+
+
+ Example - OTel JVM Runtime Metrics
+
+ Example of combining Prometheus metrics with OpenTelemetry JVM runtime metrics on one endpoint
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-bom-alpha
+ 2.25.0-alpha
+ pom
+ import
+
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-core
+
+
+ io.prometheus
+ prometheus-metrics-exporter-httpserver
+
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ pom
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-runtime-telemetry-java8
+
+
+
+
+ ${project.artifactId}
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ io.prometheus.metrics.examples.otelruntimemetrics.Main
+
+
+
+
+
+
+
+
+
diff --git a/examples/example-otel-jvm-runtime-metrics/src/main/java/io/prometheus/metrics/examples/otelruntimemetrics/Main.java b/examples/example-otel-jvm-runtime-metrics/src/main/java/io/prometheus/metrics/examples/otelruntimemetrics/Main.java
new file mode 100644
index 000000000..49a608651
--- /dev/null
+++ b/examples/example-otel-jvm-runtime-metrics/src/main/java/io/prometheus/metrics/examples/otelruntimemetrics/Main.java
@@ -0,0 +1,76 @@
+package io.prometheus.metrics.examples.otelruntimemetrics;
+
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.prometheus.metrics.core.metrics.Counter;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.model.registry.PrometheusRegistry;
+import io.prometheus.metrics.model.snapshots.Unit;
+import java.io.IOException;
+
+/**
+ * Example combining Prometheus metrics with OpenTelemetry JVM runtime metrics on a single endpoint.
+ *
+ *
This demonstrates:
+ *
+ *
+ *
Registering a Prometheus counter metric
+ *
Bridging OTel runtime metrics into the same PrometheusRegistry
+ *
Exposing everything via the built-in HTTPServer on /metrics
+ *
+ */
+public class Main {
+
+ public static void main(String[] args) throws IOException, InterruptedException {
+
+ PrometheusRegistry registry = new PrometheusRegistry();
+
+ // 1. Register a Prometheus counter metric
+ Counter counter =
+ Counter.builder()
+ .name("uptime_seconds_total")
+ .help("total number of seconds since this application was started")
+ .unit(Unit.SECONDS)
+ .register(registry);
+
+ // 2. Create a PrometheusMetricReader and register it with the same registry.
+ // This bridges OTel metrics into the Prometheus registry.
+ PrometheusMetricReader reader = PrometheusMetricReader.create();
+ registry.register(reader);
+
+ // 3. Build the OTel SDK with the reader.
+ OpenTelemetrySdk openTelemetry =
+ OpenTelemetrySdk.builder()
+ .setMeterProvider(SdkMeterProvider.builder().registerMetricReader(reader).build())
+ .build();
+
+ // 4. Start OTel JVM runtime metrics collection.
+ // - captureGcCause() adds a jvm.gc.cause attribute to jvm.gc.duration
+ // - emitExperimentalTelemetry() enables buffer pools, extended CPU,
+ // extended memory pools, and file descriptor metrics
+ RuntimeMetrics runtimeMetrics =
+ RuntimeMetrics.builder(openTelemetry).captureGcCause().emitExperimentalTelemetry().build();
+
+ // 5. Expose both Prometheus and OTel metrics on a single endpoint.
+ HTTPServer server = HTTPServer.builder().port(9400).registry(registry).buildAndStart();
+
+ // 6. Close RuntimeMetrics and server on shutdown to stop JMX metric collection.
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread(
+ () -> {
+ runtimeMetrics.close();
+ server.close();
+ }));
+
+ System.out.println(
+ "HTTPServer listening on port http://localhost:" + server.getPort() + "/metrics");
+
+ while (true) {
+ Thread.sleep(1000);
+ counter.inc();
+ }
+ }
+}
diff --git a/examples/example-prometheus-properties/pom.xml b/examples/example-prometheus-properties/pom.xml
index 4556e7f73..bedd517f7 100644
--- a/examples/example-prometheus-properties/pom.xml
+++ b/examples/example-prometheus-properties/pom.xml
@@ -3,34 +3,44 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-prometheus-properties
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+ Example - prometheus.properties
Example of runtime configuration with prometheus.properties
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+
+
io.prometheusprometheus-metrics-core
- ${project.version}io.prometheusprometheus-metrics-instrumentation-jvm
- ${project.version}io.prometheusprometheus-metrics-exporter-httpserver
- ${project.version}
diff --git a/examples/example-prometheus-properties/src/main/resources/prometheus.properties b/examples/example-prometheus-properties/src/main/resources/prometheus.properties
index a786fd370..be895f2fe 100644
--- a/examples/example-prometheus-properties/src/main/resources/prometheus.properties
+++ b/examples/example-prometheus-properties/src/main/resources/prometheus.properties
@@ -1,8 +1,8 @@
-io.prometheus.exporter.httpServer.port = 9401
-io.prometheus.exporter.includeCreatedTimestamps = true
+io.prometheus.exporter.http_server.port = 9401
+io.prometheus.exporter.include_created_timestamps = true
# Set a new default for all histograms
-io.prometheus.metrics.histogramClassicUpperBounds = .2, .4, .8, .1
+io.prometheus.metrics.histogram_classic_upper_bounds = .2, .4, .8, .1
# Override the default for one specific histogram
-io.prometheus.metrics.request_size_bytes.histogramClassicUpperBounds = 256, 512, 768, 1024
+io.prometheus.metrics.request_size_bytes.histogram_classic_upper_bounds = 256, 512, 768, 1024
diff --git a/examples/example-simpleclient-bridge/pom.xml b/examples/example-simpleclient-bridge/pom.xml
index f5e465d06..d0edb60cf 100644
--- a/examples/example-simpleclient-bridge/pom.xml
+++ b/examples/example-simpleclient-bridge/pom.xml
@@ -3,19 +3,32 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheusexample-simpleclient-bridge
+ 1.0-SNAPSHOT
+
+
+ 8
+ UTF-8
+ Example - Simpleclient Bridge
Prometheus Metrics Example of the Simpleclient Backwards Compatibility module
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.0
+ pom
+ import
+
+
+
+
io.prometheus
@@ -25,12 +38,10 @@
io.prometheusprometheus-metrics-simpleclient-bridge
- ${project.version}io.prometheusprometheus-metrics-exporter-httpserver
- ${project.version}
diff --git a/examples/pom.xml b/examples/pom.xml
index b5f8886ce..e033a3d1e 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -6,7 +6,7 @@
io.prometheusclient_java
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTexamples
@@ -19,6 +19,7 @@
true
+ true
@@ -29,7 +30,9 @@
example-exporter-opentelemetryexample-simpleclient-bridgeexample-native-histogram
+ example-custom-bucketsexample-prometheus-properties
+ example-otel-jvm-runtime-metrics
diff --git a/integration-tests/it-common/pom.xml b/integration-tests/it-common/pom.xml
index 5586a7f9a..9ae86303c 100644
--- a/integration-tests/it-common/pom.xml
+++ b/integration-tests/it-common/pom.xml
@@ -6,7 +6,7 @@
io.prometheusintegration-tests
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-common
diff --git a/integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java b/integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java
index 00a3d544f..91a7ed712 100644
--- a/integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java
+++ b/integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java
@@ -4,13 +4,13 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
-import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
+import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_33_5.Metrics;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
+import java.net.URI;
import java.net.URISyntaxException;
-import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -24,7 +24,7 @@
import org.testcontainers.containers.GenericContainer;
public abstract class ExporterTest {
- private final GenericContainer> sampleAppContainer;
+ protected final GenericContainer> sampleAppContainer;
private final Volume sampleAppVolume;
protected final String sampleApp;
@@ -33,8 +33,12 @@ public ExporterTest(String sampleApp) throws IOException, URISyntaxException {
this.sampleAppVolume =
Volume.create("it-exporter")
.copy("../../it-" + sampleApp + "/target/" + sampleApp + ".jar");
+ String javaVersion = System.getenv("TEST_JAVA_VERSION");
+ if (javaVersion == null || javaVersion.isEmpty()) {
+ javaVersion = "25";
+ }
this.sampleAppContainer =
- new GenericContainer<>("openjdk:17")
+ new GenericContainer<>("eclipse-temurin:" + javaVersion)
.withFileSystemBind(sampleAppVolume.getHostPath(), "/app", BindMode.READ_ONLY)
.withWorkingDirectory("/app")
.withLogConsumer(LogConsumer.withPrefix(sampleApp))
@@ -53,7 +57,7 @@ protected void start(String outcome) {
}
@AfterEach
- public void tearDown() throws IOException {
+ void tearDown() throws IOException {
sampleAppContainer.stop();
sampleAppVolume.remove();
}
@@ -68,7 +72,7 @@ protected Response scrape(String method, String queryString, String... requestHe
throws IOException {
return scrape(
method,
- new URL(
+ URI.create(
"http://localhost:"
+ sampleAppContainer.getMappedPort(9400)
+ "/metrics?"
@@ -76,10 +80,10 @@ protected Response scrape(String method, String queryString, String... requestHe
requestHeaders);
}
- public static Response scrape(String method, URL url, String... requestHeaders)
+ public static Response scrape(String method, URI uri, String... requestHeaders)
throws IOException {
long timeoutMillis = TimeUnit.SECONDS.toMillis(5);
- HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ HttpURLConnection con = (HttpURLConnection) uri.toURL().openConnection();
con.setRequestMethod(method);
for (int i = 0; i < requestHeaders.length; i += 2) {
con.setRequestProperty(requestHeaders[i], requestHeaders[i + 1]);
@@ -111,7 +115,7 @@ public static Response scrape(String method, URL url, String... requestHeaders)
if (exception != null) {
exception.printStackTrace();
}
- fail("timeout while getting metrics from " + url);
+ fail("timeout while getting metrics from " + uri);
return null; // will not happen
}
diff --git a/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/pom.xml b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/pom.xml
new file mode 100644
index 000000000..13364ec5d
--- /dev/null
+++ b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+
+ io.prometheus
+ it-exporter
+ 1.6.0-SNAPSHOT
+
+
+ it-exporter-duplicate-metrics-sample
+
+ Integration Tests - Duplicate Metrics Sample
+
+ HTTPServer Sample demonstrating duplicate metric names with different label sets
+
+
+
+
+ io.prometheus
+ prometheus-metrics-exporter-httpserver
+ ${project.version}
+
+
+ io.prometheus
+ prometheus-metrics-core
+ ${project.version}
+
+
+
+
+ exporter-duplicate-metrics-sample
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+
+ io.prometheus.metrics.it.exporter.duplicatemetrics.DuplicateMetricsSample
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/src/main/java/io/prometheus/metrics/it/exporter/duplicatemetrics/DuplicateMetricsSample.java b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/src/main/java/io/prometheus/metrics/it/exporter/duplicatemetrics/DuplicateMetricsSample.java
new file mode 100644
index 000000000..c6005674a
--- /dev/null
+++ b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/src/main/java/io/prometheus/metrics/it/exporter/duplicatemetrics/DuplicateMetricsSample.java
@@ -0,0 +1,91 @@
+package io.prometheus.metrics.it.exporter.duplicatemetrics;
+
+import io.prometheus.metrics.core.metrics.Counter;
+import io.prometheus.metrics.core.metrics.Gauge;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.model.snapshots.Unit;
+import java.io.IOException;
+
+/** Integration test sample demonstrating metrics with duplicate names but different label sets. */
+public class DuplicateMetricsSample {
+
+ public static void main(String[] args) throws IOException, InterruptedException {
+ if (args.length != 2) {
+ System.err.println("Usage: java -jar duplicate-metrics-sample.jar ");
+ System.err.println("Where outcome is \"success\" or \"error\".");
+ System.exit(1);
+ }
+
+ int port = parsePortOrExit(args[0]);
+ String outcome = args[1];
+ run(port, outcome);
+ }
+
+ private static void run(int port, String outcome) throws IOException, InterruptedException {
+ // Register multiple counters with the same Prometheus name "http_requests_total"
+ // but different label sets
+ Counter requestsSuccess =
+ Counter.builder()
+ .name("http_requests_total")
+ .help("Total HTTP requests by status")
+ .labelNames("status", "method")
+ .register();
+ requestsSuccess.labelValues("success", "GET").inc(150);
+ requestsSuccess.labelValues("success", "POST").inc(45);
+
+ Counter requestsError =
+ Counter.builder()
+ .name("http_requests_total")
+ .help("Total HTTP requests by status")
+ .labelNames("status", "endpoint")
+ .register();
+ requestsError.labelValues("error", "/api").inc(5);
+ requestsError.labelValues("error", "/health").inc(2);
+
+ // Register multiple gauges with the same Prometheus name "active_connections"
+ // but different label sets
+ Gauge connectionsByRegion =
+ Gauge.builder()
+ .name("active_connections")
+ .help("Active connections")
+ .labelNames("region", "protocol")
+ .register();
+ connectionsByRegion.labelValues("us-east", "http").set(42);
+ connectionsByRegion.labelValues("us-west", "http").set(38);
+ connectionsByRegion.labelValues("eu-west", "https").set(55);
+
+ Gauge connectionsByPool =
+ Gauge.builder()
+ .name("active_connections")
+ .help("Active connections")
+ .labelNames("pool", "type")
+ .register();
+ connectionsByPool.labelValues("primary", "read").set(30);
+ connectionsByPool.labelValues("replica", "write").set(10);
+
+ // Also add a regular metric without duplicates for reference
+ Counter uniqueMetric =
+ Counter.builder()
+ .name("unique_metric_total")
+ .help("A unique metric for reference")
+ .unit(Unit.BYTES)
+ .register();
+ uniqueMetric.inc(1024);
+
+ HTTPServer server = HTTPServer.builder().port(port).buildAndStart();
+
+ System.out.println(
+ "DuplicateMetricsSample listening on http://localhost:" + server.getPort() + "/metrics");
+ Thread.currentThread().join(); // wait forever
+ }
+
+ private static int parsePortOrExit(String port) {
+ try {
+ return Integer.parseInt(port);
+ } catch (NumberFormatException e) {
+ System.err.println("\"" + port + "\": Invalid port number.");
+ System.exit(1);
+ }
+ return 0; // this won't happen
+ }
+}
diff --git a/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml b/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml
index 08f3e392f..3dfa05f07 100644
--- a/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml
@@ -6,7 +6,7 @@
io.prometheusit-exporter
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-exporter-httpserver-sample
diff --git a/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml b/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml
index 5e2c64ce3..6bfd7c5cb 100644
--- a/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml
@@ -6,7 +6,7 @@
io.prometheusit-exporter
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-exporter-no-protobuf
diff --git a/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml b/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml
index fef7042df..ed5285769 100644
--- a/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml
@@ -6,7 +6,7 @@
io.prometheusit-exporter
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-exporter-servlet-jetty-sample
@@ -16,8 +16,8 @@
Jetty Sample for the Exporter Integration Test
- 12.0.23
- 17
+ 12.1.6
+ 25
diff --git a/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml b/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml
index 16124705e..be9c7704d 100644
--- a/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml
@@ -6,7 +6,7 @@
io.prometheusit-exporter
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-exporter-servlet-tomcat-sample
@@ -17,7 +17,7 @@
- 17
+ 25
@@ -34,7 +34,7 @@
org.apache.tomcat.embedtomcat-embed-core
- 11.0.9
+ 11.0.18
diff --git a/integration-tests/it-exporter/it-exporter-test/pom.xml b/integration-tests/it-exporter/it-exporter-test/pom.xml
index b50529468..027631fbd 100644
--- a/integration-tests/it-exporter/it-exporter-test/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-test/pom.xml
@@ -6,7 +6,7 @@
io.prometheusit-exporter
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-exporter-test
diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/DuplicateMetricsIT.java b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/DuplicateMetricsIT.java
new file mode 100644
index 000000000..7530070ac
--- /dev/null
+++ b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/DuplicateMetricsIT.java
@@ -0,0 +1,181 @@
+package io.prometheus.metrics.it.exporter.test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.prometheus.client.it.common.ExporterTest;
+import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_33_5.Metrics;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class DuplicateMetricsIT extends ExporterTest {
+
+ public DuplicateMetricsIT() throws IOException, URISyntaxException {
+ super("exporter-duplicate-metrics-sample");
+ }
+
+ @Test
+ void testDuplicateMetricsInPrometheusTextFormat() throws IOException {
+ start();
+ Response response = scrape("GET", "");
+ assertThat(response.status).isEqualTo(200);
+ assertContentType(
+ "text/plain; version=0.0.4; charset=utf-8", response.getHeader("Content-Type"));
+
+ String expected =
+ """
+ # HELP active_connections Active connections
+ # TYPE active_connections gauge
+ active_connections{pool="primary",type="read"} 30.0
+ active_connections{pool="replica",type="write"} 10.0
+ active_connections{protocol="http",region="us-east"} 42.0
+ active_connections{protocol="http",region="us-west"} 38.0
+ active_connections{protocol="https",region="eu-west"} 55.0
+ # HELP http_requests_total Total HTTP requests by status
+ # TYPE http_requests_total counter
+ http_requests_total{endpoint="/api",status="error"} 5.0
+ http_requests_total{endpoint="/health",status="error"} 2.0
+ http_requests_total{method="GET",status="success"} 150.0
+ http_requests_total{method="POST",status="success"} 45.0
+ # HELP unique_metric_bytes_total A unique metric for reference
+ # TYPE unique_metric_bytes_total counter
+ unique_metric_bytes_total 1024.0
+ """;
+
+ assertThat(response.stringBody()).isEqualTo(expected);
+ }
+
+ @Test
+ void testDuplicateMetricsInOpenMetricsTextFormat() throws IOException {
+ start();
+ Response response =
+ scrape("GET", "", "Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8");
+ assertThat(response.status).isEqualTo(200);
+ assertContentType(
+ "application/openmetrics-text; version=1.0.0; charset=utf-8",
+ response.getHeader("Content-Type"));
+
+ // OpenMetrics format should have UNIT for unique_metric_bytes (base name without _total)
+ String expected =
+ """
+ # TYPE active_connections gauge
+ # HELP active_connections Active connections
+ active_connections{pool="primary",type="read"} 30.0
+ active_connections{pool="replica",type="write"} 10.0
+ active_connections{protocol="http",region="us-east"} 42.0
+ active_connections{protocol="http",region="us-west"} 38.0
+ active_connections{protocol="https",region="eu-west"} 55.0
+ # TYPE http_requests counter
+ # HELP http_requests Total HTTP requests by status
+ http_requests_total{endpoint="/api",status="error"} 5.0
+ http_requests_total{endpoint="/health",status="error"} 2.0
+ http_requests_total{method="GET",status="success"} 150.0
+ http_requests_total{method="POST",status="success"} 45.0
+ # TYPE unique_metric_bytes counter
+ # UNIT unique_metric_bytes bytes
+ # HELP unique_metric_bytes A unique metric for reference
+ unique_metric_bytes_total 1024.0
+ # EOF
+ """;
+
+ assertThat(response.stringBody()).isEqualTo(expected);
+ }
+
+ @Test
+ void testDuplicateMetricsInPrometheusProtobufFormat() throws IOException {
+ start();
+ Response response =
+ scrape(
+ "GET",
+ "",
+ "Accept",
+ "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;"
+ + " encoding=delimited");
+ assertThat(response.status).isEqualTo(200);
+ assertContentType(
+ "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;"
+ + " encoding=delimited",
+ response.getHeader("Content-Type"));
+
+ List metrics = response.protoBody();
+
+ assertThat(metrics).hasSize(3);
+
+ // Metrics are sorted by name
+ assertThat(metrics.get(0).getName()).isEqualTo("active_connections");
+ assertThat(metrics.get(1).getName()).isEqualTo("http_requests_total");
+ assertThat(metrics.get(2).getName()).isEqualTo("unique_metric_bytes_total");
+
+ // Verify active_connections has all 5 data points merged
+ Metrics.MetricFamily activeConnections = metrics.get(0);
+ assertThat(activeConnections.getType()).isEqualTo(Metrics.MetricType.GAUGE);
+ assertThat(activeConnections.getHelp()).isEqualTo("Active connections");
+ assertThat(activeConnections.getMetricList()).hasSize(5);
+
+ // Verify http_requests_total has all 4 data points merged
+ Metrics.MetricFamily httpRequests = metrics.get(1);
+ assertThat(httpRequests.getType()).isEqualTo(Metrics.MetricType.COUNTER);
+ assertThat(httpRequests.getHelp()).isEqualTo("Total HTTP requests by status");
+ assertThat(httpRequests.getMetricList()).hasSize(4);
+
+ // Verify each data point has the expected labels
+ boolean foundSuccessGet = false;
+ boolean foundSuccessPost = false;
+ boolean foundErrorApi = false;
+ boolean foundErrorHealth = false;
+
+ for (Metrics.Metric metric : httpRequests.getMetricList()) {
+ List labels = metric.getLabelList();
+ if (hasLabel(labels, "status", "success") && hasLabel(labels, "method", "GET")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(150.0);
+ foundSuccessGet = true;
+ } else if (hasLabel(labels, "status", "success") && hasLabel(labels, "method", "POST")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(45.0);
+ foundSuccessPost = true;
+ } else if (hasLabel(labels, "status", "error") && hasLabel(labels, "endpoint", "/api")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(5.0);
+ foundErrorApi = true;
+ } else if (hasLabel(labels, "status", "error") && hasLabel(labels, "endpoint", "/health")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(2.0);
+ foundErrorHealth = true;
+ }
+ }
+
+ assertThat(foundSuccessGet).isTrue();
+ assertThat(foundSuccessPost).isTrue();
+ assertThat(foundErrorApi).isTrue();
+ assertThat(foundErrorHealth).isTrue();
+
+ Metrics.MetricFamily uniqueMetric = metrics.get(2);
+ assertThat(uniqueMetric.getType()).isEqualTo(Metrics.MetricType.COUNTER);
+ assertThat(uniqueMetric.getMetricList()).hasSize(1);
+ assertThat(uniqueMetric.getMetric(0).getCounter().getValue()).isEqualTo(1024.0);
+ }
+
+ @Test
+ void testDuplicateMetricsWithNameFilter() throws IOException {
+ start();
+ // Only scrape http_requests_total
+ Response response = scrape("GET", nameParam());
+ assertThat(response.status).isEqualTo(200);
+
+ String body = response.stringBody();
+
+ assertThat(body)
+ .contains("http_requests_total{method=\"GET\",status=\"success\"} 150.0")
+ .contains("http_requests_total{endpoint=\"/api\",status=\"error\"} 5.0");
+
+ // Should NOT contain active_connections or unique_metric_total
+ assertThat(body).doesNotContain("active_connections").doesNotContain("unique_metric_total");
+ }
+
+ private boolean hasLabel(List labels, String name, String value) {
+ return labels.stream()
+ .anyMatch(label -> label.getName().equals(name) && label.getValue().equals(value));
+ }
+
+ private String nameParam() {
+ return "name[]=" + "http_requests_total";
+ }
+}
diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java
index 1ab3f3237..e9bcc2ee7 100644
--- a/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java
+++ b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java
@@ -5,11 +5,12 @@
import com.google.common.io.Resources;
import io.prometheus.client.it.common.ExporterTest;
-import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
+import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_33_5.Metrics;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.List;
+import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@@ -21,7 +22,7 @@ public ExporterIT(String sampleApp) throws IOException, URISyntaxException {
}
@Test
- public void testOpenMetricsTextFormat() throws IOException {
+ void testOpenMetricsTextFormat() throws IOException {
start();
Response response =
scrape("GET", "", "Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8");
@@ -43,7 +44,7 @@ public void testOpenMetricsTextFormat() throws IOException {
}
@Test
- public void testPrometheusTextFormat() throws IOException {
+ void testPrometheusTextFormat() throws IOException {
start();
Response response = scrape("GET", "");
assertThat(response.status).isEqualTo(200);
@@ -63,7 +64,7 @@ public void testPrometheusTextFormat() throws IOException {
}
@Test
- public void testPrometheusProtobufFormat() throws IOException {
+ void testPrometheusProtobufFormat() throws IOException {
start();
Response response =
scrape(
@@ -101,15 +102,26 @@ public void testPrometheusProtobufDebugFormat(String format, String expected) th
assertThat(response.status).isEqualTo(200);
assertContentType(
"text/plain;charset=utf-8", response.getHeader("Content-Type").replace(" ", ""));
- assertThat(response.stringBody().trim())
- .isEqualTo(
- Resources.toString(Resources.getResource(expected), UTF_8)
- .trim()
- .replace("", sampleApp));
+
+ String actualResponse = response.stringBody().trim();
+ String expectedResponse =
+ Resources.toString(Resources.getResource(expected), UTF_8)
+ .trim()
+ .replace("", sampleApp);
+
+ if ("prometheus-protobuf".equals(format)) {
+ assertThat(actualResponse)
+ .matches(
+ Pattern.quote(expectedResponse)
+ .replace("", "\\E\\d+\\Q")
+ .replace("", "\\E\\d+\\Q"));
+ } else {
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+ }
}
@Test
- public void testCompression() throws IOException {
+ void testCompression() throws IOException {
start();
Response response =
scrape(
@@ -137,7 +149,7 @@ public void testCompression() throws IOException {
}
@Test
- public void testErrorHandling() throws IOException {
+ void testErrorHandling() throws IOException {
start("error");
Response response = scrape("GET", "");
assertThat(response.status).isEqualTo(500);
@@ -145,7 +157,7 @@ public void testErrorHandling() throws IOException {
}
@Test
- public void testHeadRequest() throws IOException {
+ void testHeadRequest() throws IOException {
start();
Response fullResponse = scrape("GET", "");
int size = fullResponse.body.length;
@@ -157,7 +169,7 @@ public void testHeadRequest() throws IOException {
}
@Test
- public void testDebug() throws IOException {
+ void testDebug() throws IOException {
start();
Response response = scrape("GET", "debug=openmetrics");
assertThat(response.status).isEqualTo(200);
@@ -168,7 +180,7 @@ public void testDebug() throws IOException {
}
@Test
- public void testNameFilter() throws IOException {
+ void testNameFilter() throws IOException {
start();
Response response =
scrape(
@@ -187,7 +199,7 @@ public void testNameFilter() throws IOException {
}
@Test
- public void testEmptyResponseOpenMetrics() throws IOException {
+ void testEmptyResponseOpenMetrics() throws IOException {
start();
Response response =
scrape(
@@ -205,7 +217,7 @@ public void testEmptyResponseOpenMetrics() throws IOException {
}
@Test
- public void testEmptyResponseText() throws IOException {
+ void testEmptyResponseText() throws IOException {
start();
Response response = scrape("GET", nameParam("none_existing"));
assertThat(response.status).isEqualTo(200);
@@ -219,7 +231,7 @@ public void testEmptyResponseText() throws IOException {
}
@Test
- public void testEmptyResponseProtobuf() throws IOException {
+ void testEmptyResponseProtobuf() throws IOException {
start();
Response response =
scrape(
@@ -237,7 +249,7 @@ public void testEmptyResponseProtobuf() throws IOException {
}
@Test
- public void testEmptyResponseGzipOpenMetrics() throws IOException {
+ void testEmptyResponseGzipOpenMetrics() throws IOException {
start();
Response response =
scrape(
@@ -253,7 +265,7 @@ public void testEmptyResponseGzipOpenMetrics() throws IOException {
}
@Test
- public void testEmptyResponseGzipText() throws IOException {
+ void testEmptyResponseGzipText() throws IOException {
start();
Response response = scrape("GET", nameParam("none_existing"), "Accept-Encoding", "gzip");
assertThat(response.status).isEqualTo(200);
@@ -266,7 +278,7 @@ private String nameParam(String name) {
}
@Test
- public void testDebugUnknown() throws IOException {
+ void testDebugUnknown() throws IOException {
start();
Response response = scrape("GET", "debug=unknown");
assertThat(response.status).isEqualTo(500);
diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt
index 1d7603c1b..06f19b85c 100644
--- a/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt
+++ b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt
@@ -37,6 +37,10 @@ type: COUNTER
metric {
counter {
value: 17.0
+ created_timestamp {
+ seconds:
+ nanos:
+ }
}
}
diff --git a/integration-tests/it-exporter/it-no-protobuf-test/pom.xml b/integration-tests/it-exporter/it-no-protobuf-test/pom.xml
index 98a58f84e..1a17b83dc 100644
--- a/integration-tests/it-exporter/it-no-protobuf-test/pom.xml
+++ b/integration-tests/it-exporter/it-no-protobuf-test/pom.xml
@@ -6,7 +6,7 @@
io.prometheusit-exporter
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-no-protobuf-test
diff --git a/integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java b/integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java
index cd534dcb9..9b041795e 100644
--- a/integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java
+++ b/integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java
@@ -14,7 +14,7 @@ public NoProtobufIT() throws IOException, URISyntaxException {
}
@Test
- public void testPrometheusProtobufDebugFormat() throws IOException {
+ void testPrometheusProtobufDebugFormat() throws IOException {
start();
assertThat(scrape("GET", "debug=text").status).isEqualTo(200);
// protobuf is not supported
diff --git a/integration-tests/it-exporter/pom.xml b/integration-tests/it-exporter/pom.xml
index 3dcb27f28..08386d8b0 100644
--- a/integration-tests/it-exporter/pom.xml
+++ b/integration-tests/it-exporter/pom.xml
@@ -6,7 +6,7 @@
io.prometheusintegration-tests
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-exporter
@@ -21,6 +21,7 @@
it-exporter-servlet-tomcat-sampleit-exporter-servlet-jetty-sampleit-exporter-httpserver-sample
+ it-exporter-duplicate-metrics-sampleit-exporter-no-protobufit-exporter-testit-no-protobuf-test
diff --git a/integration-tests/it-pushgateway/pom.xml b/integration-tests/it-pushgateway/pom.xml
index b0b0bdc33..a70ea5458 100644
--- a/integration-tests/it-pushgateway/pom.xml
+++ b/integration-tests/it-pushgateway/pom.xml
@@ -6,7 +6,7 @@
io.prometheusintegration-tests
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTit-pushgateway
@@ -48,7 +48,7 @@
com.jayway.jsonpathjson-path
- 2.9.0
+ 2.10.0test
diff --git a/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java b/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java
index beb83d7d9..3d31129f1 100644
--- a/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java
+++ b/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java
@@ -22,7 +22,7 @@
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.MountableFile;
-public class PushGatewayIT {
+class PushGatewayIT {
private GenericContainer> sampleAppContainer;
private GenericContainer> pushGatewayContainer;
@@ -30,9 +30,13 @@ public class PushGatewayIT {
private Volume sampleAppVolume;
@BeforeEach
- public void setUp() throws IOException, URISyntaxException {
+ void setUp() throws IOException, URISyntaxException {
Network network = Network.newNetwork();
sampleAppVolume = Volume.create("it-pushgateway").copy("pushgateway-test-app.jar");
+ String javaVersion = System.getenv("TEST_JAVA_VERSION");
+ if (javaVersion == null || javaVersion.isEmpty()) {
+ javaVersion = "25";
+ }
pushGatewayContainer =
new GenericContainer<>("prom/pushgateway:v1.8.0")
.withExposedPorts(9091)
@@ -41,7 +45,7 @@ public void setUp() throws IOException, URISyntaxException {
.withLogConsumer(LogConsumer.withPrefix("pushgateway"))
.waitingFor(Wait.forListeningPort());
sampleAppContainer =
- new GenericContainer<>("openjdk:17")
+ new GenericContainer<>("eclipse-temurin:" + javaVersion)
.withFileSystemBind(sampleAppVolume.getHostPath(), "/app", BindMode.READ_ONLY)
.withNetwork(network)
.withWorkingDirectory("/app")
@@ -56,7 +60,7 @@ public void setUp() throws IOException, URISyntaxException {
}
@AfterEach
- public void tearDown() throws IOException {
+ void tearDown() throws IOException {
prometheusContainer.stop();
pushGatewayContainer.stop();
sampleAppContainer.stop();
@@ -66,7 +70,7 @@ public void tearDown() throws IOException {
final OkHttpClient client = new OkHttpClient();
@Test
- public void testSimple() throws IOException, InterruptedException {
+ void testSimple() throws IOException, InterruptedException {
pushGatewayContainer.start();
sampleAppContainer
.withCommand(
@@ -86,7 +90,7 @@ public void testSimple() throws IOException, InterruptedException {
}
@Test
- public void testTextFormat() throws IOException, InterruptedException {
+ void testTextFormat() throws IOException, InterruptedException {
pushGatewayContainer.start();
sampleAppContainer
.withCommand(
@@ -106,7 +110,7 @@ public void testTextFormat() throws IOException, InterruptedException {
}
@Test
- public void testBasicAuth() throws IOException, InterruptedException {
+ void testBasicAuth() throws IOException, InterruptedException {
pushGatewayContainer
.withCopyFileToContainer(
MountableFile.forClasspathResource("/pushgateway-basicauth.yaml"),
@@ -131,7 +135,7 @@ public void testBasicAuth() throws IOException, InterruptedException {
}
@Test
- public void testSsl() throws InterruptedException, IOException {
+ void testSsl() throws InterruptedException, IOException {
pushGatewayContainer
.withCopyFileToContainer(
MountableFile.forClasspathResource("/pushgateway-ssl.yaml"),
@@ -156,7 +160,7 @@ public void testSsl() throws InterruptedException, IOException {
}
@Test
- public void testProtobuf() throws IOException, InterruptedException {
+ void testProtobuf() throws IOException, InterruptedException {
pushGatewayContainer.start();
sampleAppContainer
.withCommand(
diff --git a/integration-tests/it-spring-boot-smoke-test/pom.xml b/integration-tests/it-spring-boot-smoke-test/pom.xml
index 8a180a60f..ee653fbb4 100644
--- a/integration-tests/it-spring-boot-smoke-test/pom.xml
+++ b/integration-tests/it-spring-boot-smoke-test/pom.xml
@@ -8,21 +8,21 @@
org.springframework.bootspring-boot-starter-parent
- 3.5.3
+ 4.0.3io.prometheusit-spring-boot-smoke-test
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTIntegration Test - Spring Smoke Tests
Spring Smoke Tests
- 17
- 5.13.4
+ 25
+ 6.0.3
@@ -89,80 +89,108 @@
-
- org.graalvm.buildtools
- native-maven-plugin
-
-
-
-
- --initialize-at-build-time=org.junit.jupiter.api.DisplayNameGenerator$IndicativeSentences
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$ClassInfo
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$LifecycleMethods
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.DynamicDescendantFilter$Mode
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ExclusiveResourceCollector$1
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor$MethodInfo
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.discovery.ClassSelectorResolver$DummyClassTemplateInvocationContext
-
-
- --initialize-at-build-time=org.junit.platform.engine.support.store.NamespacedHierarchicalStore$EvaluatedValue
-
- --initialize-at-build-time=org.junit.platform.launcher.core.DiscoveryIssueNotifier
-
-
- --initialize-at-build-time=org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider
-
-
- --initialize-at-build-time=org.junit.platform.launcher.core.LauncherDiscoveryResult$EngineResultInfo
-
-
- --initialize-at-build-time=org.junit.platform.suite.engine.SuiteTestDescriptor$LifecycleMethods
-
-
-
-
- org.springframework.bootspring-boot-maven-plugin
-
- com.diffplug.spotless
- spotless-maven-plugin
- 2.46.1
-
-
-
-
-
-
-
- verify
-
- check
-
-
-
-
+
+
+ java17-plus
+
+ [17,)
+
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+
+
+
+
+ --initialize-at-build-time=org.junit.jupiter.api.DisplayNameGenerator$IndicativeSentences
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$ClassInfo
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$LifecycleMethods
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.DynamicDescendantFilter$Mode
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ExclusiveResourceCollector$1
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor$MethodInfo
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.discovery.ClassSelectorResolver$DummyClassTemplateInvocationContext
+
+
+ --initialize-at-build-time=org.junit.platform.engine.support.store.NamespacedHierarchicalStore$EvaluatedValue
+
+ --initialize-at-build-time=org.junit.platform.launcher.core.DiscoveryIssueNotifier
+
+
+ --initialize-at-build-time=org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider
+
+
+ --initialize-at-build-time=org.junit.platform.launcher.core.LauncherDiscoveryResult$EngineResultInfo
+
+
+ --initialize-at-build-time=org.junit.platform.suite.engine.SuiteTestDescriptor$LifecycleMethods
+
+
+ --initialize-at-build-time=org.junit.platform.commons.logging.LoggerFactory$DelegatingLogger
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.execution.ConditionEvaluator
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.execution.InterceptingExecutableInvoker
+
+
+ --initialize-at-build-time=org.junit.jupiter.api.extension.ConditionEvaluationResult
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.execution.InvocationInterceptorChain
+
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 3.2.1
+
+
+
+
+
+
+
+ verify
+
+ check
+
+
+
+
+
+
+
+
+
diff --git a/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java
index 26f555847..fed9fba6d 100644
--- a/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java
+++ b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java
@@ -3,24 +3,22 @@
import static org.assertj.core.api.Assertions.assertThat;
import io.prometheus.client.it.common.ExporterTest;
-import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
+import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_33_5.Metrics;
import java.io.IOException;
-import java.net.URL;
+import java.net.URI;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
-@AutoConfigureObservability
class ApplicationTest {
@Test
- public void testPrometheusProtobufFormat() throws IOException {
+ void testPrometheusProtobufFormat() throws IOException {
ExporterTest.Response response =
ExporterTest.scrape(
"GET",
- new URL("http://localhost:8080/actuator/prometheus"),
+ URI.create("http://localhost:8080/actuator/prometheus"),
"Accept",
"application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;"
+ " encoding=delimited");
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index bca697c1e..f0ab29299 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -6,7 +6,7 @@
io.prometheusclient_java
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTintegration-tests
@@ -19,6 +19,7 @@
true
+ true
@@ -51,12 +52,12 @@
commons-iocommons-io
- 2.20.0
+ 2.21.0org.testcontainersjunit-jupiter
- 1.21.3
+ 1.21.4test
diff --git a/lychee.toml b/lychee.toml
deleted file mode 100644
index 599f2de7a..000000000
--- a/lychee.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-max_retries = 6
-exclude_loopback = true
-cache = true
-
-base = "https://prometheus.github.io"
-exclude_path = ["docs/themes"]
-exclude = [
- '^https://github\.com/prometheus/client_java/settings',
- '#',
- 'CONTRIBUTING.md',
- 'LICENSE',
- 'MAINTAINERS.md'
-]
-
-
diff --git a/mise.native.toml b/mise.native.toml
deleted file mode 100644
index 67ad3940f..000000000
--- a/mise.native.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[tools]
-java = "graalvm-community-24.0.1"
-
-[tasks.test]
-depends = "build"
-run = "../../mvnw test -PnativeTest"
-dir = "integration-tests/it-spring-boot-smoke-test"
-
diff --git a/mise.toml b/mise.toml
index 79eba2876..ebf962bc4 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,10 +1,16 @@
[tools]
-"cargo:zizmor" = "1.9.0"
-"go:github.com/gohugoio/hugo" = "v0.148.1"
-"go:github.com/grafana/oats" = "0.4.0"
-java = "temurin-21.0.8+9.0.LTS"
-lychee = "0.19.1"
-protoc = "31.1"
+"go:github.com/gohugoio/hugo" = "v0.156.0"
+"go:github.com/grafana/oats" = "0.6.1"
+java = "temurin-25.0.2+10.0.LTS"
+lychee = "0.23.0"
+node = "24.13.1"
+"npm:renovate" = "43.8.5"
+protoc = "33.5"
+
+[env]
+RENOVATE_TRACKED_DEPS_EXCLUDE="github-actions,github-runners"
+# renovate: datasource=docker depName=ghcr.io/super-linter/super-linter
+SUPER_LINTER_VERSION="slim-v8.5.0@sha256:857dcc3f0bf5dd065fdeed1ace63394bb2004238a5ef02910ea23d9bcd8fd2b8"
[tasks.ci]
description = "CI Build"
@@ -16,13 +22,20 @@ env.PROTO_GENERATION = "true"
description = "format source code"
run = "./mvnw spotless:apply"
+[tasks.clean]
+description = "clean all modules"
+run = "./mvnw clean"
+
[tasks.compile]
description = "bare compile, ignoring formatting and linters"
run = "./mvnw install -DskipTests -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
[tasks.generate]
description = "bare compile, ignoring formatting and linters"
-run = "./mvnw install -DskipTests -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
+run = [
+ "mise use --pin protoc@latest",
+ "./mvnw clean install -DskipTests -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
+]
env.PROTO_GENERATION = "true"
[tasks.test]
@@ -34,43 +47,43 @@ description = "run all tests"
run = "./mvnw verify"
[tasks.build]
-description = "build all modules wihthout tests"
-run = "./mvnw install -DskipTests"
+description = "build all modules without tests"
+run = "./mvnw install -DskipTests -Dcoverage.skip=true"
-[tasks.lint]
-run = "scripts/super-linter.sh"
+# Shared lint tasks from flint (https://github.com/grafana/flint)
+[tasks."lint:super-linter"]
+description = "Run Super-Linter on the repository"
+file = "https://raw.githubusercontent.com/grafana/flint/5bb3726cfe3305072457c0c4fa85dce5ca154680/tasks/lint/super-linter.sh" # v0.6.0
-[tasks.lint-links]
-run = "lychee --include-fragments ."
+[tasks."lint:links"]
+description = "Lint links"
+file = "https://raw.githubusercontent.com/grafana/flint/5bb3726cfe3305072457c0c4fa85dce5ca154680/tasks/lint/links.sh" # v0.6.0
-[tasks.lint-gh-actions]
-run = "zizmor .github/"
+[tasks."lint:renovate-deps"]
+description = "Verify renovate-tracked-deps.json is up to date"
+file = "https://raw.githubusercontent.com/grafana/flint/5bb3726cfe3305072457c0c4fa85dce5ca154680/tasks/lint/renovate-deps.py" # v0.6.0
-[tasks.lint-bom]
-run = "scripts/lint-bom.sh"
+[tasks."lint"]
+description = "Run all lints"
+depends = ["lint:super-linter", "lint:links", "lint:bom", "lint:renovate-deps"]
-[tasks.lint-rest]
-description = "All lints not covered by super linter"
-depends = ["lint-links", "lint-gh-actions", "lint-bom"]
+[tasks.fix]
+description = "Auto-fix lint issues"
+run = "AUTOFIX=true mise run lint"
[tasks.acceptance-test]
description = "Run OATs acceptance tests"
depends = "build"
run = "oats -timeout 5m examples/"
-[tasks.set-version]
-run = './scripts/set-version.sh {{arg(name="version")}}'
-
[tasks.javadoc]
+description = "Generate Javadoc"
run = [
"./mvnw -B clean compile javadoc:javadoc javadoc:aggregate -P 'javadoc,!default'",
"rm -rf ./docs/static/api",
"mv ./target/reports/apidocs ./docs/static/api && echo && echo 'ls ./docs/static/api' && ls ./docs/static/api"
]
-[tasks.set-gh-pages-version]
-run = "./scripts/set-release-version-github-pages.sh"
-
[tasks.gh-pages-dev]
description = "Build GitHub pages for dev"
run = "hugo server -D"
@@ -78,7 +91,7 @@ dir = "docs"
[tasks.build-gh-pages]
description = "Build GitHub pages"
-depends = ["javadoc", "set-gh-pages-version"]
+depends = ["javadoc", "set-release-version-github-pages"]
# For maximum backward compatibility with Hugo modules
env = { HUGO_ENVIRONMENT = "production", HUGO_ENV = "production" }
dir = "docs"
@@ -87,12 +100,23 @@ run = [
"echo 'ls ./public/api' && ls ./public/api"
]
-[tasks.build-release]
-description = "Build release"
-run = "./scripts/build-release.sh"
-env.TAG = "1.4.0-SNAPSHOT"
-
-[settings]
-# to get lock file support and for go backend
-experimental = true
-
+[tasks."benchmark:quick"]
+description = "Run benchmarks with reduced iterations (quick smoke test, ~10 min)"
+run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 1 -wi 1 -i 3'"
+
+[tasks."benchmark:ci"]
+description = "Run benchmarks with CI configuration (3 forks, 3 warmup, 5 measurement iterations (~60 min total)"
+run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 3 -wi 3 -i 5'"
+
+[tasks."benchmark:ci-json"]
+description = "Run benchmarks with CI configuration and JSON output (for workflow/testing)"
+run = """
+./mvnw -pl benchmarks -am -DskipTests clean package
+JMH_ARGS="${JMH_ARGS:--f 3 -wi 3 -i 5}"
+echo "Running benchmarks with args: $JMH_ARGS"
+java -jar ./benchmarks/target/benchmarks.jar -rf json -rff benchmark-results.json $JMH_ARGS
+"""
+
+[tasks."benchmark:generate-summary"]
+description = "Generate summary from existing benchmark-results.json"
+run = "python3 ./.mise/tasks/generate_benchmark_summary.py"
diff --git a/mvnw b/mvnw
index 6fdd4d2b2..bd8896bf2 100755
--- a/mvnw
+++ b/mvnw
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
-# Apache Maven Wrapper startup batch script, version 3.3.2
+# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
@@ -36,101 +36,104 @@ set -euf
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
- [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
- native_path() { cygpath --path --windows "$1"; }
- ;;
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
- # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
- if [ -n "${JAVA_HOME-}" ]; then
- if [ -x "$JAVA_HOME/jre/sh/java" ]; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- JAVACCMD="$JAVA_HOME/jre/sh/javac"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- JAVACCMD="$JAVA_HOME/bin/javac"
-
- if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
- echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
- echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
- return 1
- fi
- fi
- else
- JAVACMD="$(
- 'set' +e
- 'unset' -f command 2>/dev/null
- 'command' -v java
- )" || :
- JAVACCMD="$(
- 'set' +e
- 'unset' -f command 2>/dev/null
- 'command' -v javac
- )" || :
-
- if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
- echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
- return 1
- fi
- fi
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
}
# hash string like Java String::hashCode
hash_string() {
- str="${1:-}" h=0
- while [ -n "$str" ]; do
- char="${str%"${str#?}"}"
- h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
- str="${str#?}"
- done
- printf %x\\n $h
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
- printf %s\\n "$1" >&2
- exit 1
+ printf %s\\n "$1" >&2
+ exit 1
}
trim() {
- # MWRAPPER-139:
- # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
- # Needed for removing poorly interpreted newline sequences when running in more
- # exotic environments such as mingw bash on Windows.
- printf "%s" "${1}" | tr -d '[:space:]'
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
}
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
- case "${key-}" in
- distributionUrl) distributionUrl=$(trim "${value-}") ;;
- distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
- esac
-done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
-[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
- MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
- case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
- *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
- :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
- :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
- :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
- *)
- echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
- distributionPlatform=linux-amd64
- ;;
- esac
- distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
- ;;
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
-*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
@@ -143,13 +146,13 @@ MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
- unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
- exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
- verbose "found existing MAVEN_HOME at $MAVEN_HOME"
- exec_maven "$@"
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
fi
case "${distributionUrl-}" in
@@ -159,10 +162,10 @@ esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
- clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
- trap clean HUP INT TERM EXIT
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
else
- die "cannot create temp dir"
+ die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
@@ -174,8 +177,8 @@ verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
- distributionUrl="${distributionUrl%.zip}.tar.gz"
- distributionUrlName="${distributionUrl##*/}"
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
@@ -189,71 +192,104 @@ has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
- verbose "Found wget ... using wget"
- wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
- verbose "Found curl ... using curl"
- curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
- verbose "Falling back to use Java to download"
- javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
- targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
- cat >"$javaSource" <<-END
- public class Downloader extends java.net.Authenticator
- {
- protected java.net.PasswordAuthentication getPasswordAuthentication()
- {
- return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
- }
- public static void main( String[] args ) throws Exception
- {
- setDefault( new Downloader() );
- java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
- }
- }
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
END
- # For Cygwin/MinGW, switch paths to Windows format before running javac and java
- verbose " - Compiling Downloader.java ..."
- "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
- verbose " - Running Downloader.java ..."
- "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
- distributionSha256Result=false
- if [ "$MVN_CMD" = mvnd.sh ]; then
- echo "Checksum validation is not supported for maven-mvnd." >&2
- echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
- exit 1
- elif command -v sha256sum >/dev/null; then
- if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
- distributionSha256Result=true
- fi
- elif command -v shasum >/dev/null; then
- if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
- distributionSha256Result=true
- fi
- else
- echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
- echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
- exit 1
- fi
- if [ $distributionSha256Result = false ]; then
- echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
- echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
- exit 1
- fi
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
fi
# unzip and move
if command -v unzip >/dev/null; then
- unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
- tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
fi
-printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
-mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
index 249bdf382..5761d9489 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -1,149 +1,189 @@
-<# : batch portion
-@REM ----------------------------------------------------------------------------
-@REM Licensed to the Apache Software Foundation (ASF) under one
-@REM or more contributor license agreements. See the NOTICE file
-@REM distributed with this work for additional information
-@REM regarding copyright ownership. The ASF licenses this file
-@REM to you under the Apache License, Version 2.0 (the
-@REM "License"); you may not use this file except in compliance
-@REM with the License. You may obtain a copy of the License at
-@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
-@REM
-@REM Unless required by applicable law or agreed to in writing,
-@REM software distributed under the License is distributed on an
-@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-@REM KIND, either express or implied. See the License for the
-@REM specific language governing permissions and limitations
-@REM under the License.
-@REM ----------------------------------------------------------------------------
-
-@REM ----------------------------------------------------------------------------
-@REM Apache Maven Wrapper startup batch script, version 3.3.2
-@REM
-@REM Optional ENV vars
-@REM MVNW_REPOURL - repo url base for downloading maven distribution
-@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
-@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
-@REM ----------------------------------------------------------------------------
-
-@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
-@SET __MVNW_CMD__=
-@SET __MVNW_ERROR__=
-@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
-@SET PSModulePath=
-@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
- IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
-)
-@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
-@SET __MVNW_PSMODULEP_SAVE=
-@SET __MVNW_ARG0_NAME__=
-@SET MVNW_USERNAME=
-@SET MVNW_PASSWORD=
-@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
-@echo Cannot start maven from wrapper >&2 && exit /b 1
-@GOTO :EOF
-: end batch / begin powershell #>
-
-$ErrorActionPreference = "Stop"
-if ($env:MVNW_VERBOSE -eq "true") {
- $VerbosePreference = "Continue"
-}
-
-# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
-$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
-if (!$distributionUrl) {
- Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
-}
-
-switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
- "maven-mvnd-*" {
- $USE_MVND = $true
- $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
- $MVN_CMD = "mvnd.cmd"
- break
- }
- default {
- $USE_MVND = $false
- $MVN_CMD = $script -replace '^mvnw','mvn'
- break
- }
-}
-
-# apply MVNW_REPOURL and calculate MAVEN_HOME
-# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
-if ($env:MVNW_REPOURL) {
- $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
- $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
-}
-$distributionUrlName = $distributionUrl -replace '^.*/',''
-$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
-$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
-if ($env:MAVEN_USER_HOME) {
- $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
-}
-$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
-$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
-
-if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
- Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
- Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
- exit $?
-}
-
-if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
- Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
-}
-
-# prepare tmp dir
-$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
-$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
-$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
-trap {
- if ($TMP_DOWNLOAD_DIR.Exists) {
- try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
- catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
- }
-}
-
-New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
-
-# Download and Install Apache Maven
-Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
-Write-Verbose "Downloading from: $distributionUrl"
-Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
-
-$webclient = New-Object System.Net.WebClient
-if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
- $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
-}
-[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
-$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
-
-# If specified, validate the SHA-256 sum of the Maven distribution zip file
-$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
-if ($distributionSha256Sum) {
- if ($USE_MVND) {
- Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
- }
- Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
- if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
- Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
- }
-}
-
-# unzip and move
-Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
-Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
-try {
- Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
-} catch {
- if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
- Write-Error "fail to move MAVEN_HOME"
- }
-} finally {
- try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
- catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
-}
-
-Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.4
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+
+$MAVEN_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
index 941f6df2b..c166f0648 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,11 +8,10 @@
io.prometheusclient_java_parent
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTprometheus-metrics-parent/pom.xml
- 1.4.0-SNAPSHOTclient_javaPrometheus Metrics Library
@@ -23,11 +22,12 @@
UTF-8--module-name-need-to-be-overridden--
- 4.31.1
- 33.4.8-jre
- 5.13.4
- 2.16.0-alpha
+ 4.33.5
+ 33.5.0-jre
+ 6.0.3
+ 2.25.0-alpha8
+ 250.70falsefalse
@@ -60,6 +60,7 @@
prometheus-metrics-instrumentation-dropwizardprometheus-metrics-instrumentation-guavaprometheus-metrics-simpleclient-bridge
+ prometheus-metrics-otel-support
@@ -69,6 +70,67 @@
3.0.2provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit-jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit-jupiter.version}
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.21.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.27.7
+ test
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.17
+ test
+
+
+ org.junit-pioneer
+ junit-pioneer
+ 2.3.0
+ test
+
+
+ org.awaitility
+ awaitility
+ 4.3.0
+ test
+
+
+ org.wiremock
+ wiremock
+ 3.13.2
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+
+
+
@@ -82,19 +144,19 @@
maven-resources-plugin
- 3.3.1
+ 3.4.0maven-compiler-plugin
- 3.14.0
+ 3.15.0maven-surefire-plugin
- 3.5.3
+ 3.5.4maven-jar-plugin
- 3.4.2
+ 3.5.0maven-deploy-plugin
@@ -111,23 +173,23 @@
maven-shade-plugin
- 3.6.0
+ 3.6.1maven-failsafe-plugin
- 3.5.3
+ 3.5.4maven-dependency-plugin
- 3.8.1
+ 3.10.0maven-javadoc-plugin
- 3.11.2
+ 3.12.0maven-enforcer-plugin
- 3.6.1
+ 3.6.2org.codehaus.mojo
@@ -137,7 +199,7 @@
org.codehaus.mojoexec-maven-plugin
- 3.5.1
+ 3.6.3
@@ -167,12 +229,14 @@
org.jacocojacoco-maven-plugin
- 0.8.13
+ 0.8.14${coverage.skip}**/generated/****/*BlockingRejectedExecutionHandler*
+ **/*AllocationCountingNotificationListener*
+ **/*MapperConfig*
@@ -203,6 +267,11 @@
COVEREDRATIO${jacoco.line-coverage}
+
+ BRANCH
+ COVEREDRATIO
+ 0.50
+
@@ -252,56 +321,62 @@
${java.version}${java.version}${java.version}
- 17
- 17
- 17
+ ${test.java.version}
+ ${test.java.version}
+ ${test.java.version}true-Xlint:all,-serial,-processing,-options${warnings}--should-stop=ifError=FLOW-XDcompilePolicy=simple
-
- -Xplugin:ErrorProne
- -Xep:AlmostJavadoc:OFF
- -Xep:MissingSummary:OFF
- -Xep:LongDoubleConversion:OFF
- -Xep:StringSplitter:OFF
- -XepExcludedPaths:.*/generated/.*
-
-
-
- com.google.errorprone
- error_prone_core
- 2.40.0
-
-
-
org.codehaus.mojoversions-maven-plugin
- 2.18.0
+ 2.21.0file://${project.basedir}/version-rules.xml
+
+ maven-javadoc-plugin
+
+ ${javadoc.skip}
+
+
+
+
+
+ org.junit
+ junit-bom
+ ${junit-jupiter.version}
+ pom
+ import
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-bom-alpha
+ ${otel.instrumentation.version}
+ pom
+ import
+
+
+ io.opentelemetry
+ opentelemetry-proto
+ 1.7.1-alpha
+ test
+
+
+
+
-
-
- maven-project-info-reports-plugin
- 3.9.0
- maven-javadoc-plugin
@@ -325,102 +400,15 @@
- default
+ examples-and-integration-tests
- true
+ [25,)examplesbenchmarksintegration-tests
-
-
-
- org.junit
- junit-bom
- ${junit-jupiter.version}
- pom
- import
-
-
- io.opentelemetry.instrumentation
- opentelemetry-instrumentation-bom-alpha
- ${otel.instrumentation.version}
- pom
- import
-
-
- io.opentelemetry
- opentelemetry-proto
- 1.7.1-alpha
- test
-
-
-
-
-
-
- org.junit.jupiter
- junit-jupiter
- ${junit-jupiter.version}
- test
-
-
- org.junit.jupiter
- junit-jupiter-params
- ${junit-jupiter.version}
- test
-
-
- org.mockito
- mockito-core
- 5.18.0
- test
-
-
- org.assertj
- assertj-core
- 3.27.3
- test
-
-
- com.google.guava
- guava
- ${guava.version}
- test
-
-
- org.slf4j
- slf4j-simple
- 2.0.17
- test
-
-
- org.junit-pioneer
- junit-pioneer
- 2.3.0
- test
-
-
- org.awaitility
- awaitility
- 4.3.0
- test
-
-
- org.wiremock
- wiremock
- 3.13.1
- test
-
-
- org.hamcrest
- hamcrest-core
-
-
-
- javadoc
@@ -429,6 +417,7 @@
maven-javadoc-plugin
+ ${javadoc.skip}UTF-8UTF-8true
@@ -442,6 +431,45 @@
+
+ errorprone
+
+ [21,)
+
+
+
+
+ maven-compiler-plugin
+
+
+ -XDaddTypeAnnotationsToSymbol=true
+
+ -Xplugin:ErrorProne
+ -Xep:AlmostJavadoc:OFF
+ -Xep:MissingSummary:OFF
+ -Xep:LongDoubleConversion:OFF
+ -Xep:StringSplitter:OFF
+ -XepExcludedPaths:(.*/generated/.*|.*/src/test/java/.*|.*/examples/.*|.*/integration-tests/.*)
+ -XepOpt:NullAway:AnnotatedPackages=io.prometheus.metrics
+
+
+
+
+ com.google.errorprone
+ error_prone_core
+ 2.47.0
+
+
+ com.uber.nullaway
+ nullaway
+ 0.13.1
+
+
+
+
+
+
+ release
diff --git a/prometheus-metrics-bom/pom.xml b/prometheus-metrics-bom/pom.xml
index 242710819..167ea522e 100644
--- a/prometheus-metrics-bom/pom.xml
+++ b/prometheus-metrics-bom/pom.xml
@@ -6,7 +6,7 @@
io.prometheusclient_java_parent
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOT../prometheus-metrics-parent/pom.xml
@@ -119,6 +119,12 @@
prometheus-metrics-model${project.version}
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ ${project.version}
+ pom
+ io.prometheusprometheus-metrics-simpleclient-bridge
diff --git a/prometheus-metrics-config/pom.xml b/prometheus-metrics-config/pom.xml
index 4546c3dc9..494282cc7 100644
--- a/prometheus-metrics-config/pom.xml
+++ b/prometheus-metrics-config/pom.xml
@@ -6,7 +6,7 @@
io.prometheusclient_java
- 1.4.0-SNAPSHOT
+ 1.6.0-SNAPSHOTprometheus-metrics-config
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/EscapingScheme.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/EscapingScheme.java
new file mode 100644
index 000000000..1cd037bf3
--- /dev/null
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/EscapingScheme.java
@@ -0,0 +1,85 @@
+package io.prometheus.metrics.config;
+
+import javax.annotation.Nullable;
+
+public enum EscapingScheme {
+ /** NO_ESCAPING indicates that a name will not be escaped. */
+ ALLOW_UTF8("allow-utf-8"),
+
+ /** UNDERSCORE_ESCAPING replaces all legacy-invalid characters with underscores. */
+ UNDERSCORE_ESCAPING("underscores"),
+
+ /**
+ * DOTS_ESCAPING is similar to UNDERSCORE_ESCAPING, except that dots are converted to `_dot_` and
+ * pre-existing underscores are converted to `__`.
+ */
+ DOTS_ESCAPING("dots"),
+
+ /**
+ * VALUE_ENCODING_ESCAPING prepends the name with `U__` and replaces all invalid characters with
+ * the Unicode value, surrounded by underscores. Single underscores are replaced with double
+ * underscores.
+ */
+ VALUE_ENCODING_ESCAPING("values");
+
+ private static final String ESCAPING_KEY = "escaping";
+
+ /** Default escaping scheme for names when not specified. */
+ public static final EscapingScheme DEFAULT = UNDERSCORE_ESCAPING;
+
+ public final String getValue() {
+ return value;
+ }
+
+ private final String value;
+
+ EscapingScheme(String value) {
+ this.value = value;
+ }
+
+ /**
+ * fromAcceptHeader returns an EscapingScheme depending on the Accept header. Iff the header
+ * contains an escaping=allow-utf-8 term, it will select NO_ESCAPING. If a valid "escaping" term
+ * exists, that will be used. Otherwise, the global default will be returned.
+ */
+ public static EscapingScheme fromAcceptHeader(@Nullable String acceptHeader) {
+ if (acceptHeader != null) {
+ for (String p : acceptHeader.split(";")) {
+ String[] toks = p.split("=");
+ if (toks.length != 2) {
+ continue;
+ }
+ String key = toks[0].trim();
+ String value = toks[1].trim();
+ if (key.equals(ESCAPING_KEY)) {
+ try {
+ return EscapingScheme.forString(value);
+ } catch (IllegalArgumentException e) {
+ // If the escaping parameter is unknown, ignore it.
+ return DEFAULT;
+ }
+ }
+ }
+ }
+ return DEFAULT;
+ }
+
+ static EscapingScheme forString(String value) {
+ switch (value) {
+ case "allow-utf-8":
+ return ALLOW_UTF8;
+ case "underscores":
+ return UNDERSCORE_ESCAPING;
+ case "dots":
+ return DOTS_ESCAPING;
+ case "values":
+ return VALUE_ENCODING_ESCAPING;
+ default:
+ throw new IllegalArgumentException("Unknown escaping scheme: " + value);
+ }
+ }
+
+ public String toHeaderFormat() {
+ return "; " + ESCAPING_KEY + "=" + value;
+ }
+}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java
index 017d67909..765d33ac5 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java
@@ -1,23 +1,23 @@
package io.prometheus.metrics.config;
-import java.util.Map;
+import javax.annotation.Nullable;
/** Properties starting with io.prometheus.exemplars */
public class ExemplarsProperties {
private static final String PREFIX = "io.prometheus.exemplars";
- private static final String MIN_RETENTION_PERIOD_SECONDS = "minRetentionPeriodSeconds";
- private static final String MAX_RETENTION_PERIOD_SECONDS = "maxRetentionPeriodSeconds";
- private static final String SAMPLE_INTERVAL_MILLISECONDS = "sampleIntervalMilliseconds";
+ private static final String MIN_RETENTION_PERIOD_SECONDS = "min_retention_period_seconds";
+ private static final String MAX_RETENTION_PERIOD_SECONDS = "max_retention_period_seconds";
+ private static final String SAMPLE_INTERVAL_MILLISECONDS = "sample_interval_milliseconds";
- private final Integer minRetentionPeriodSeconds;
- private final Integer maxRetentionPeriodSeconds;
- private final Integer sampleIntervalMilliseconds;
+ @Nullable private final Integer minRetentionPeriodSeconds;
+ @Nullable private final Integer maxRetentionPeriodSeconds;
+ @Nullable private final Integer sampleIntervalMilliseconds;
private ExemplarsProperties(
- Integer minRetentionPeriodSeconds,
- Integer maxRetentionPeriodSeconds,
- Integer sampleIntervalMilliseconds) {
+ @Nullable Integer minRetentionPeriodSeconds,
+ @Nullable Integer maxRetentionPeriodSeconds,
+ @Nullable Integer sampleIntervalMilliseconds) {
this.minRetentionPeriodSeconds = minRetentionPeriodSeconds;
this.maxRetentionPeriodSeconds = maxRetentionPeriodSeconds;
this.sampleIntervalMilliseconds = sampleIntervalMilliseconds;
@@ -28,6 +28,7 @@ private ExemplarsProperties(
*
*
Default see {@code ExemplarSamplerConfig.DEFAULT_MIN_RETENTION_PERIOD_SECONDS}
*/
+ @Nullable
public Integer getMinRetentionPeriodSeconds() {
return minRetentionPeriodSeconds;
}
@@ -37,6 +38,7 @@ public Integer getMinRetentionPeriodSeconds() {
*
*
Default see {@code ExemplarSamplerConfig.DEFAULT_MAX_RETENTION_PERIOD_SECONDS}
*/
+ @Nullable
public Integer getMaxRetentionPeriodSeconds() {
return maxRetentionPeriodSeconds;
}
@@ -48,22 +50,23 @@ public Integer getMaxRetentionPeriodSeconds() {
*
*
Default see {@code ExemplarSamplerConfig.DEFAULT_SAMPLE_INTERVAL_MILLISECONDS}
*/
+ @Nullable
public Integer getSampleIntervalMilliseconds() {
return sampleIntervalMilliseconds;
}
/**
- * Note that this will remove entries from {@code properties}. This is because we want to know if
- * there are unused properties remaining after all properties have been loaded.
+ * Note that this will remove entries from {@code propertySource}. This is because we want to know
+ * if there are unused properties remaining after all properties have been loaded.
*/
- static ExemplarsProperties load(Map
-
+
io.prometheus
- prometheus-metrics-exposition-formats
+ prometheus-metrics-exposition-formats-no-protobuf${project.version}test
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/datapoints/DistributionDataPoint.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/datapoints/DistributionDataPoint.java
index 82e42a892..0f2a072de 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/datapoints/DistributionDataPoint.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/datapoints/DistributionDataPoint.java
@@ -19,6 +19,12 @@
*/
public interface DistributionDataPoint extends DataPoint, TimerApi {
+ /** Get the count of observations. */
+ long getCount();
+
+ /** Get the sum of all observed values. */
+ double getSum();
+
/** Observe {@code value}. */
void observe(double value);
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSampler.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSampler.java
index 15b0355cd..1219b2a09 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSampler.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSampler.java
@@ -1,5 +1,7 @@
package io.prometheus.metrics.core.exemplars;
+import static java.util.Objects.requireNonNull;
+
import io.prometheus.metrics.core.util.Scheduler;
import io.prometheus.metrics.model.snapshots.Exemplar;
import io.prometheus.metrics.model.snapshots.Exemplars;
@@ -10,6 +12,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.LongSupplier;
+import javax.annotation.Nullable;
/**
* The ExemplarSampler selects Spans as exemplars.
@@ -37,6 +40,8 @@ public class ExemplarSampler {
// to be overwritten by automatic exemplar sampling. exemplars.length == customExemplars.length
private final AtomicBoolean acceptingNewExemplars = new AtomicBoolean(true);
private final AtomicBoolean acceptingNewCustomExemplars = new AtomicBoolean(true);
+
+ @Nullable
private final SpanContext
spanContext; // may be null, in that case SpanContextSupplier.getSpanContext() is used.
@@ -52,7 +57,7 @@ public ExemplarSampler(ExemplarSamplerConfig config) {
* io.prometheus.metrics.tracer.initializer.SpanContextSupplier#getSpanContext()
* SpanContextSupplier.getSpanContext()} is called to find a span context.
*/
- public ExemplarSampler(ExemplarSamplerConfig config, SpanContext spanContext) {
+ public ExemplarSampler(ExemplarSamplerConfig config, @Nullable SpanContext spanContext) {
this.config = config;
this.exemplars = new Exemplar[config.getNumberOfExemplars()];
this.customExemplars = new Exemplar[exemplars.length];
@@ -113,10 +118,13 @@ public void observeWithExemplar(double value, Labels labels) {
private long doObserve(double value) {
if (exemplars.length == 1) {
return doObserveSingleExemplar(value);
- } else if (config.getHistogramClassicUpperBounds() != null) {
- return doObserveWithUpperBounds(value);
} else {
- return doObserveWithoutUpperBounds(value);
+ double[] classicUpperBounds = config.getHistogramClassicUpperBounds();
+ if (classicUpperBounds != null) {
+ return doObserveWithUpperBounds(value, classicUpperBounds);
+ } else {
+ return doObserveWithoutUpperBounds(value);
+ }
}
}
@@ -140,11 +148,10 @@ private long doObserveSingleExemplar(double amount, Labels labels) {
return 0;
}
- private long doObserveWithUpperBounds(double value) {
+ private long doObserveWithUpperBounds(double value, double[] classicUpperBounds) {
long now = System.currentTimeMillis();
- double[] upperBounds = config.getHistogramClassicUpperBounds();
- for (int i = 0; i < upperBounds.length; i++) {
- if (value <= upperBounds[i]) {
+ for (int i = 0; i < classicUpperBounds.length; i++) {
+ if (value <= classicUpperBounds[i]) {
Exemplar previous = exemplars[i];
if (previous == null
|| now - previous.getTimestampMillis() > config.getMinRetentionPeriodMillis()) {
@@ -185,11 +192,11 @@ private long doObserveWithoutUpperBounds(double value) {
if (nullIndex >= 0) {
return updateExemplar(nullIndex, value, now);
}
- if (now - smallest.getTimestampMillis() > config.getMinRetentionPeriodMillis()
+ if (now - requireNonNull(smallest).getTimestampMillis() > config.getMinRetentionPeriodMillis()
&& value < smallest.getValue()) {
return updateExemplar(smallestIndex, value, now);
}
- if (now - largest.getTimestampMillis() > config.getMinRetentionPeriodMillis()
+ if (now - requireNonNull(largest).getTimestampMillis() > config.getMinRetentionPeriodMillis()
&& value > largest.getValue()) {
return updateExemplar(largestIndex, value, now);
}
@@ -215,18 +222,21 @@ private long doObserveWithoutUpperBounds(double value) {
private long doObserveWithExemplar(double amount, Labels labels) {
if (customExemplars.length == 1) {
return doObserveSingleExemplar(amount, labels);
- } else if (config.getHistogramClassicUpperBounds() != null) {
- return doObserveWithExemplarWithUpperBounds(amount, labels);
} else {
- return doObserveWithExemplarWithoutUpperBounds(amount, labels);
+ double[] classicUpperBounds = config.getHistogramClassicUpperBounds();
+ if (classicUpperBounds != null) {
+ return doObserveWithExemplarWithUpperBounds(amount, labels, classicUpperBounds);
+ } else {
+ return doObserveWithExemplarWithoutUpperBounds(amount, labels);
+ }
}
}
- private long doObserveWithExemplarWithUpperBounds(double value, Labels labels) {
+ private long doObserveWithExemplarWithUpperBounds(
+ double value, Labels labels, double[] classicUpperBounds) {
long now = System.currentTimeMillis();
- double[] upperBounds = config.getHistogramClassicUpperBounds();
- for (int i = 0; i < upperBounds.length; i++) {
- if (value <= upperBounds[i]) {
+ for (int i = 0; i < classicUpperBounds.length; i++) {
+ if (value <= classicUpperBounds[i]) {
Exemplar previous = customExemplars[i];
if (previous == null
|| now - previous.getTimestampMillis() > config.getMinRetentionPeriodMillis()) {
@@ -260,7 +270,8 @@ private long doObserveWithExemplarWithoutUpperBounds(double amount, Labels label
}
if (nullPos != -1) {
return updateCustomExemplar(nullPos, amount, labels, now);
- } else if (now - oldest.getTimestampMillis() > config.getMinRetentionPeriodMillis()) {
+ } else if (now - requireNonNull(oldest).getTimestampMillis()
+ > config.getMinRetentionPeriodMillis()) {
return updateCustomExemplar(oldestPos, amount, labels, now);
} else {
return 0;
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerConfig.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerConfig.java
index 7acfbec22..5bf642e7e 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerConfig.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerConfig.java
@@ -3,6 +3,7 @@
import io.prometheus.metrics.config.ExemplarsProperties;
import io.prometheus.metrics.config.PrometheusProperties;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
public class ExemplarSamplerConfig {
@@ -18,11 +19,13 @@ public class ExemplarSamplerConfig {
private final long minRetentionPeriodMillis;
private final long maxRetentionPeriodMillis;
private final long sampleIntervalMillis;
+
+ @Nullable
private final double[] histogramClassicUpperBounds; // null unless it's a classic histogram
- private final int
- numberOfExemplars; // if histogramClassicUpperBounds != null, then numberOfExemplars ==
- // histogramClassicUpperBounds.length
+ // if histogramClassicUpperBounds != null,
+ // then numberOfExemplars == histogramClassicUpperBounds.length
+ private final int numberOfExemplars;
/**
* Constructor for all metric types except classic histograms.
@@ -49,7 +52,9 @@ public ExemplarSamplerConfig(
}
private ExemplarSamplerConfig(
- ExemplarsProperties properties, int numberOfExemplars, double[] histogramClassicUpperBounds) {
+ ExemplarsProperties properties,
+ int numberOfExemplars,
+ @Nullable double[] histogramClassicUpperBounds) {
this(
TimeUnit.SECONDS.toMillis(
getOrDefault(
@@ -68,7 +73,7 @@ private ExemplarSamplerConfig(
long maxRetentionPeriodMillis,
long sampleIntervalMillis,
int numberOfExemplars,
- double[] histogramClassicUpperBounds) {
+ @Nullable double[] histogramClassicUpperBounds) {
this.minRetentionPeriodMillis = minRetentionPeriodMillis;
this.maxRetentionPeriodMillis = maxRetentionPeriodMillis;
this.sampleIntervalMillis = sampleIntervalMillis;
@@ -110,11 +115,11 @@ private void validate() {
}
}
- private static T getOrDefault(T result, T defaultValue) {
+ private static T getOrDefault(@Nullable T result, T defaultValue) {
return result != null ? result : defaultValue;
}
- /** May be {@code null}. */
+ @Nullable
public double[] getHistogramClassicUpperBounds() {
return histogramClassicUpperBounds;
}
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Buffer.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Buffer.java
index d4ff33a37..1c47f867c 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Buffer.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Buffer.java
@@ -18,7 +18,15 @@
class Buffer {
private static final long bufferActiveBit = 1L << 63;
- private final AtomicLong observationCount = new AtomicLong(0);
+ // Tracking observation counts requires an AtomicLong for coordination between recording and
+ // collecting. AtomicLong does much worse under contention than the LongAdder instances used
+ // elsewhere to hold aggregated state. To improve, we stripe the AtomicLong into N instances,
+ // where N is the number of available processors. Each record operation chooses the appropriate
+ // instance to use based on the modulo of its thread id and N. This is a more naive / simple
+ // implementation compared to the striping used under the hood in java.util.concurrent classes
+ // like LongAdder - contention and hot spots can still occur if recording thread ids happen to
+ // resolve to the same index. Further improvement is possible.
+ private final AtomicLong[] stripedObservationCounts;
private double[] observationBuffer = new double[0];
private int bufferPos = 0;
private boolean reset = false;
@@ -27,8 +35,17 @@ class Buffer {
ReentrantLock runLock = new ReentrantLock();
Condition bufferFilled = appendLock.newCondition();
+ Buffer() {
+ stripedObservationCounts = new AtomicLong[Runtime.getRuntime().availableProcessors()];
+ for (int i = 0; i < stripedObservationCounts.length; i++) {
+ stripedObservationCounts[i] = new AtomicLong(0);
+ }
+ }
+
boolean append(double value) {
- long count = observationCount.incrementAndGet();
+ int index = Math.abs((int) Thread.currentThread().getId()) % stripedObservationCounts.length;
+ AtomicLong observationCountForThread = stripedObservationCounts[index];
+ long count = observationCountForThread.incrementAndGet();
if ((count & bufferActiveBit) == 0) {
return false; // sign bit not set -> buffer not active.
} else {
@@ -69,7 +86,10 @@ T run(
runLock.lock();
try {
// Signal that the buffer is active.
- Long expectedCount = observationCount.getAndAdd(bufferActiveBit);
+ long expectedCount = 0L;
+ for (AtomicLong observationCount : stripedObservationCounts) {
+ expectedCount += observationCount.getAndAdd(bufferActiveBit);
+ }
while (!complete.apply(expectedCount)) {
// Wait until all in-flight threads have added their observations to the histogram /
@@ -81,14 +101,18 @@ T run(
result = createResult.get();
// Signal that the buffer is inactive.
- int expectedBufferSize;
+ long expectedBufferSize = 0;
if (reset) {
- expectedBufferSize =
- (int) ((observationCount.getAndSet(0) & ~bufferActiveBit) - expectedCount);
+ for (AtomicLong observationCount : stripedObservationCounts) {
+ expectedBufferSize += observationCount.getAndSet(0) & ~bufferActiveBit;
+ }
reset = false;
} else {
- expectedBufferSize = (int) (observationCount.addAndGet(bufferActiveBit) - expectedCount);
+ for (AtomicLong observationCount : stripedObservationCounts) {
+ expectedBufferSize += observationCount.addAndGet(bufferActiveBit);
+ }
}
+ expectedBufferSize -= expectedCount;
appendLock.lock();
try {
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java
index 8ed9b0001..c5f2f1cff 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java
@@ -5,6 +5,7 @@
import io.prometheus.metrics.core.datapoints.CounterDataPoint;
import io.prometheus.metrics.core.exemplars.ExemplarSampler;
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
import io.prometheus.metrics.model.snapshots.Exemplar;
import io.prometheus.metrics.model.snapshots.Labels;
@@ -13,6 +14,7 @@
import java.util.List;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.Nullable;
/**
* Counter metric.
@@ -32,13 +34,13 @@
public class Counter extends StatefulMetric
implements CounterDataPoint {
- private final boolean exemplarsEnabled;
- private final ExemplarSamplerConfig exemplarSamplerConfig;
+ @Nullable private final ExemplarSamplerConfig exemplarSamplerConfig;
private Counter(Builder builder, PrometheusProperties prometheusProperties) {
super(builder);
MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties);
- exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
+ boolean exemplarsEnabled =
+ getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
if (exemplarsEnabled) {
exemplarSamplerConfig =
new ExemplarSamplerConfig(prometheusProperties.getExemplarProperties(), 1);
@@ -92,13 +94,13 @@ protected CounterSnapshot collect(List labels, List metricDat
}
@Override
- protected boolean isExemplarsEnabled() {
- return exemplarsEnabled;
+ public MetricType getMetricType() {
+ return MetricType.COUNTER;
}
@Override
protected DataPoint newDataPoint() {
- if (isExemplarsEnabled()) {
+ if (exemplarSamplerConfig != null) {
return new DataPoint(new ExemplarSampler(exemplarSamplerConfig));
} else {
return new DataPoint(null);
@@ -112,7 +114,7 @@ static String stripTotalSuffix(String name) {
return name;
}
- class DataPoint implements CounterDataPoint {
+ static class DataPoint implements CounterDataPoint {
private final DoubleAdder doubleValue = new DoubleAdder();
// LongAdder is 20% faster than DoubleAdder. So let's use the LongAdder for long observations,
@@ -120,9 +122,11 @@ class DataPoint implements CounterDataPoint {
// we will be using the LongAdder and get the best performance.
private final LongAdder longValue = new LongAdder();
private final long createdTimeMillis = System.currentTimeMillis();
- private final ExemplarSampler exemplarSampler; // null if isExemplarsEnabled() is false
- private DataPoint(ExemplarSampler exemplarSampler) {
+ @Nullable
+ private final ExemplarSampler exemplarSampler; // null if exemplarSamplerConfig is null
+
+ private DataPoint(@Nullable ExemplarSampler exemplarSampler) {
this.exemplarSampler = exemplarSampler;
}
@@ -139,7 +143,7 @@ public long getLongValue() {
@Override
public void inc(long amount) {
validateAndAdd(amount);
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observe((double) amount);
}
}
@@ -147,7 +151,7 @@ public void inc(long amount) {
@Override
public void inc(double amount) {
validateAndAdd(amount);
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observe(amount);
}
}
@@ -155,7 +159,7 @@ public void inc(double amount) {
@Override
public void incWithExemplar(long amount, Labels labels) {
validateAndAdd(amount);
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observeWithExemplar((double) amount, labels);
}
}
@@ -163,7 +167,7 @@ public void incWithExemplar(long amount, Labels labels) {
@Override
public void incWithExemplar(double amount, Labels labels) {
validateAndAdd(amount);
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observeWithExemplar(amount, labels);
}
}
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java
index f8c1b162c..3a818c004 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/CounterWithCallback.java
@@ -1,11 +1,13 @@
package io.prometheus.metrics.core.metrics;
import io.prometheus.metrics.config.PrometheusProperties;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
+import javax.annotation.Nullable;
/**
* Example:
@@ -31,10 +33,10 @@ public interface Callback {
private CounterWithCallback(Builder builder) {
super(builder);
- this.callback = builder.callback;
- if (callback == null) {
+ if (builder.callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
+ this.callback = builder.callback;
}
@Override
@@ -49,6 +51,11 @@ public CounterSnapshot collect() {
return new CounterSnapshot(getMetadata(), dataPoints);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.COUNTER;
+ }
+
public static Builder builder() {
return new Builder(PrometheusProperties.get());
}
@@ -60,7 +67,7 @@ public static Builder builder(PrometheusProperties properties) {
public static class Builder
extends CallbackMetric.Builder {
- private Consumer callback;
+ @Nullable private Consumer callback;
public Builder callback(Consumer callback) {
this.callback = callback;
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Gauge.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Gauge.java
index 4ba5d5ed3..8b1f31409 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Gauge.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Gauge.java
@@ -5,6 +5,7 @@
import io.prometheus.metrics.core.datapoints.GaugeDataPoint;
import io.prometheus.metrics.core.exemplars.ExemplarSampler;
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.Exemplar;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
import io.prometheus.metrics.model.snapshots.Labels;
@@ -12,6 +13,7 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
+import javax.annotation.Nullable;
/**
* Gauge metric.
@@ -39,13 +41,13 @@
public class Gauge extends StatefulMetric
implements GaugeDataPoint {
- private final boolean exemplarsEnabled;
- private final ExemplarSamplerConfig exemplarSamplerConfig;
+ @Nullable private final ExemplarSamplerConfig exemplarSamplerConfig;
private Gauge(Builder builder, PrometheusProperties prometheusProperties) {
super(builder);
MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties);
- exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
+ boolean exemplarsEnabled =
+ getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
if (exemplarsEnabled) {
exemplarSamplerConfig =
new ExemplarSamplerConfig(prometheusProperties.getExemplarProperties(), 1);
@@ -93,25 +95,26 @@ protected GaugeSnapshot collect(List labels, List metricData)
return new GaugeSnapshot(getMetadata(), dataPointSnapshots);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.GAUGE;
+ }
+
@Override
protected DataPoint newDataPoint() {
- if (isExemplarsEnabled()) {
+ if (exemplarSamplerConfig != null) {
return new DataPoint(new ExemplarSampler(exemplarSamplerConfig));
} else {
return new DataPoint(null);
}
}
- @Override
- protected boolean isExemplarsEnabled() {
- return exemplarsEnabled;
- }
-
- class DataPoint implements GaugeDataPoint {
+ static class DataPoint implements GaugeDataPoint {
- private final ExemplarSampler exemplarSampler; // null if isExemplarsEnabled() is false
+ @Nullable
+ private final ExemplarSampler exemplarSampler; // null if exemplarSamplerConfig is null
- private DataPoint(ExemplarSampler exemplarSampler) {
+ private DataPoint(@Nullable ExemplarSampler exemplarSampler) {
this.exemplarSampler = exemplarSampler;
}
@@ -121,7 +124,7 @@ private DataPoint(ExemplarSampler exemplarSampler) {
public void inc(double amount) {
long next =
value.updateAndGet(l -> Double.doubleToRawLongBits(Double.longBitsToDouble(l) + amount));
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observe(Double.longBitsToDouble(next));
}
}
@@ -130,7 +133,7 @@ public void inc(double amount) {
public void incWithExemplar(double amount, Labels labels) {
long next =
value.updateAndGet(l -> Double.doubleToRawLongBits(Double.longBitsToDouble(l) + amount));
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observeWithExemplar(Double.longBitsToDouble(next), labels);
}
}
@@ -138,7 +141,7 @@ public void incWithExemplar(double amount, Labels labels) {
@Override
public void set(double value) {
this.value.set(Double.doubleToRawLongBits(value));
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observe(value);
}
}
@@ -151,7 +154,7 @@ public double get() {
@Override
public void setWithExemplar(double value, Labels labels) {
this.value.set(Double.doubleToRawLongBits(value));
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observeWithExemplar(value, labels);
}
}
@@ -162,7 +165,7 @@ private GaugeSnapshot.GaugeDataPointSnapshot collect(Labels labels) {
// If there are multiple Exemplars (by default it's just one), use the oldest
// so that we don't violate min age.
Exemplar oldest = null;
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
for (Exemplar exemplar : exemplarSampler.collect()) {
if (oldest == null || exemplar.getTimestampMillis() < oldest.getTimestampMillis()) {
oldest = exemplar;
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/GaugeWithCallback.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/GaugeWithCallback.java
index 8b2d7a0ba..88aee225f 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/GaugeWithCallback.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/GaugeWithCallback.java
@@ -1,11 +1,13 @@
package io.prometheus.metrics.core.metrics;
import io.prometheus.metrics.config.PrometheusProperties;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
+import javax.annotation.Nullable;
/**
* Example:
@@ -36,10 +38,10 @@ public interface Callback {
private GaugeWithCallback(Builder builder) {
super(builder);
- this.callback = builder.callback;
- if (callback == null) {
+ if (builder.callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
+ this.callback = builder.callback;
}
@Override
@@ -53,6 +55,11 @@ public GaugeSnapshot collect() {
return new GaugeSnapshot(getMetadata(), dataPoints);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.GAUGE;
+ }
+
public static Builder builder() {
return new Builder(PrometheusProperties.get());
}
@@ -64,7 +71,7 @@ public static Builder builder(PrometheusProperties properties) {
public static class Builder
extends CallbackMetric.Builder {
- private Consumer callback;
+ @Nullable private Consumer callback;
public Builder callback(Consumer callback) {
this.callback = callback;
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java
index 327867fd0..930d9e67e 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java
@@ -7,6 +7,7 @@
import io.prometheus.metrics.core.exemplars.ExemplarSampler;
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig;
import io.prometheus.metrics.core.util.Scheduler;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
import io.prometheus.metrics.model.snapshots.Exemplars;
import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
@@ -24,6 +25,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.Nullable;
/**
* Histogram metric. Example usage:
@@ -68,8 +70,7 @@ public class Histogram extends StatefulMetric nativeBucketsForPositiveValues =
@@ -195,10 +206,10 @@ public class DataPoint implements DistributionDataPoint {
private volatile long createdTimeMillis = System.currentTimeMillis();
private final Buffer buffer = new Buffer();
private volatile boolean resetDurationExpired = false;
- private final ExemplarSampler exemplarSampler;
+ @Nullable private final ExemplarSampler exemplarSampler;
private DataPoint() {
- if (exemplarsEnabled) {
+ if (exemplarSamplerConfig != null) {
exemplarSampler = new ExemplarSampler(exemplarSamplerConfig);
} else {
exemplarSampler = null;
@@ -210,6 +221,16 @@ private DataPoint() {
maybeScheduleNextReset();
}
+ @Override
+ public double getSum() {
+ return sum.sum();
+ }
+
+ @Override
+ public long getCount() {
+ return count.sum();
+ }
+
@Override
public void observe(double value) {
if (Double.isNaN(value)) {
@@ -219,7 +240,7 @@ public void observe(double value) {
if (!buffer.append(value)) {
doObserve(value, false);
}
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observe(value);
}
}
@@ -233,7 +254,7 @@ public void observeWithExemplar(double value, Labels labels) {
if (!buffer.append(value)) {
doObserve(value, false);
}
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observeWithExemplar(value, labels);
}
}
@@ -629,6 +650,11 @@ protected HistogramSnapshot collect(List labels, List metricD
return new HistogramSnapshot(getMetadata(), data);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.HISTOGRAM;
+ }
+
@Override
protected DataPoint newDataPoint() {
return new DataPoint();
@@ -674,14 +700,14 @@ public static class Builder extends StatefulMetric.BuilderDefault is no reset.
*/
public Builder nativeResetDuration(long duration, TimeUnit unit) {
- // TODO: reset interval isn't tested yet
if (duration <= 0) {
throw new IllegalArgumentException(duration + ": value > 0 expected");
}
- nativeResetDurationSeconds = unit.toSeconds(duration);
+ long seconds = unit.toSeconds(duration);
+ if (seconds == 0) {
+ throw new IllegalArgumentException(
+ duration
+ + " "
+ + unit
+ + ": duration must be at least 1 second. Sub-second durations are not supported.");
+ }
+ nativeResetDurationSeconds = seconds;
return this;
}
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java
index aac918943..011f0bb73 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java
@@ -1,6 +1,7 @@
package io.prometheus.metrics.core.metrics;
import io.prometheus.metrics.config.PrometheusProperties;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.InfoSnapshot;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.Unit;
@@ -9,6 +10,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
+import javax.annotation.Nullable;
/**
* Info metric. Example:
@@ -104,6 +106,11 @@ public InfoSnapshot collect() {
return new InfoSnapshot(getMetadata(), data);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.INFO;
+ }
+
public static Builder builder() {
return new Builder(PrometheusProperties.get());
}
@@ -144,7 +151,7 @@ public Builder name(String name) {
/** Throws an {@link UnsupportedOperationException} because Info metrics cannot have a unit. */
@Override
- public Builder unit(Unit unit) {
+ public Builder unit(@Nullable Unit unit) {
if (unit != null) {
throw new UnsupportedOperationException("Info metrics cannot have a unit.");
}
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java
index 9b213a85d..12c48c51d 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java
@@ -1,12 +1,16 @@
package io.prometheus.metrics.core.metrics;
import io.prometheus.metrics.config.PrometheusProperties;
+import io.prometheus.metrics.model.snapshots.Label;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.MetricMetadata;
import io.prometheus.metrics.model.snapshots.PrometheusNaming;
import io.prometheus.metrics.model.snapshots.Unit;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
/**
* Almost all metrics have fixed metadata, i.e. the metric name is known when the metric is created.
@@ -26,11 +30,15 @@ protected MetricWithFixedMetadata(Builder, ?> builder) {
this.labelNames = Arrays.copyOf(builder.labelNames, builder.labelNames.length);
}
- protected MetricMetadata getMetadata() {
+ @Override
+ public MetricMetadata getMetadata() {
return metadata;
}
- private String makeName(String name, Unit unit) {
+ private String makeName(@Nullable String name, @Nullable Unit unit) {
+ if (name == null) {
+ throw new IllegalArgumentException("Missing required field: name is null");
+ }
if (unit != null) {
if (!name.endsWith("_" + unit) && !name.endsWith("." + unit)) {
name += "_" + unit;
@@ -44,12 +52,24 @@ public String getPrometheusName() {
return metadata.getPrometheusName();
}
+ @Override
+ public Set getLabelNames() {
+ Set names = new HashSet<>();
+ for (String labelName : labelNames) {
+ names.add(PrometheusNaming.prometheusName(labelName));
+ }
+ for (Label label : constLabels) {
+ names.add(PrometheusNaming.prometheusName(label.getName()));
+ }
+ return names;
+ }
+
public abstract static class Builder, M extends MetricWithFixedMetadata>
extends Metric.Builder {
- protected String name;
- private Unit unit;
- private String help;
+ @Nullable private String name;
+ @Nullable private Unit unit;
+ @Nullable private String help;
private String[] labelNames = new String[0];
protected Builder(List illegalLabelNames, PrometheusProperties properties) {
@@ -65,7 +85,7 @@ public B name(String name) {
return self();
}
- public B unit(Unit unit) {
+ public B unit(@Nullable Unit unit) {
this.unit = unit;
return self();
}
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SlidingWindow.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SlidingWindow.java
index 5360e3349..e56134d5d 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SlidingWindow.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SlidingWindow.java
@@ -15,8 +15,16 @@
*
It is implemented in a generic way so that 3rd party libraries can use it for implementing
* sliding windows.
*
- *
TODO: The current implementation is {@code synchronized}. There is likely room for
- * optimization.
+ *
Thread Safety: This class uses coarse-grained {@code synchronized} methods for
+ * simplicity and correctness. All public methods ({@link #current()} and {@link #observe(double)})
+ * are synchronized, which ensures thread-safe access to the ring buffer and rotation logic.
+ *
+ *
Performance Note: The synchronized approach may cause contention under high-frequency
+ * observations.
+ *
+ *
However, given that Summary metrics are less commonly used (Histogram is generally preferred),
+ * and the observation frequency is typically lower than Counter increments, the current
+ * implementation provides an acceptable trade-off between simplicity and performance.
*/
public class SlidingWindow {
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java
index a04ddb274..740183f31 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java
@@ -2,15 +2,16 @@
import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
-import io.prometheus.metrics.config.MetricsProperties;
import io.prometheus.metrics.config.PrometheusProperties;
import io.prometheus.metrics.core.datapoints.StateSetDataPoint;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
+import javax.annotation.Nullable;
/**
* StateSet metric. Example:
@@ -53,14 +54,11 @@
public class StateSet extends StatefulMetric
implements StateSetDataPoint {
- private final boolean exemplarsEnabled;
private final String[] names;
- private StateSet(Builder builder, PrometheusProperties prometheusProperties) {
+ private StateSet(Builder builder, String[] names) {
super(builder);
- MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties);
- exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
- this.names = builder.names; // builder.names is already a validated copy
+ this.names = names;
for (String name : names) {
if (this.getMetadata().getPrometheusName().equals(prometheusName(name))) {
throw new IllegalArgumentException(
@@ -87,6 +85,11 @@ protected StateSetSnapshot collect(List labels, List metricDa
return new StateSetSnapshot(getMetadata(), data);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.STATESET;
+ }
+
@Override
public void setTrue(String state) {
getNoLabels().setTrue(state);
@@ -102,11 +105,6 @@ protected DataPoint newDataPoint() {
return new DataPoint();
}
- @Override
- protected boolean isExemplarsEnabled() {
- return exemplarsEnabled;
- }
-
class DataPoint implements StateSetDataPoint {
private final boolean[] values = new boolean[names.length];
@@ -144,7 +142,7 @@ public static Builder builder(PrometheusProperties config) {
public static class Builder extends StatefulMetric.Builder {
- private String[] names;
+ @Nullable private String[] names;
private Builder(PrometheusProperties config) {
super(Collections.emptyList(), config);
@@ -170,7 +168,7 @@ public StateSet build() {
if (names == null) {
throw new IllegalStateException("State names are required when building a StateSet.");
}
- return new StateSet(this, properties);
+ return new StateSet(this, names);
}
@Override
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java
index d4be7ccc6..386e92292 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StatefulMetric.java
@@ -10,8 +10,10 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
+import javax.annotation.Nullable;
/**
* There are two kinds of metrics:
@@ -28,13 +30,14 @@
* because in Java synchronous and asynchronous usually refers to multi-threading, but
* this has nothing to do with multi-threading.
*/
-abstract class StatefulMetric extends MetricWithFixedMetadata {
+public abstract class StatefulMetric
+ extends MetricWithFixedMetadata {
/** Map label values to data points. */
private final ConcurrentHashMap, T> data = new ConcurrentHashMap<>();
/** Shortcut for data.get(Collections.emptyList()) */
- private volatile T noLabels;
+ @Nullable private volatile T noLabels;
protected StatefulMetric(Builder, ?> builder) {
super(builder);
@@ -156,23 +159,26 @@ protected T getNoLabels() {
return noLabels;
}
+ /**
+ * Metric properties in effect by order of precedence with the highest precedence first. If a
+ * {@code MetricProperties} is configured for the metric name it has higher precedence than the
+ * builder configuration. A special case is the setting {@link Builder#withoutExemplars()} via the
+ * builder, which cannot be overridden by any configuration.
+ */
protected MetricsProperties[] getMetricProperties(
Builder, ?> builder, PrometheusProperties prometheusProperties) {
+ List properties = new ArrayList<>();
+ if (Objects.equals(builder.exemplarsEnabled, false)) {
+ properties.add(MetricsProperties.builder().exemplarsEnabled(false).build());
+ }
String metricName = getMetadata().getName();
if (prometheusProperties.getMetricProperties(metricName) != null) {
- return new MetricsProperties[] {
- prometheusProperties.getMetricProperties(metricName), // highest precedence
- builder.toProperties(), // second-highest precedence
- prometheusProperties.getDefaultMetricProperties(), // third-highest precedence
- builder.getDefaultProperties() // fallback
- };
- } else {
- return new MetricsProperties[] {
- builder.toProperties(), // highest precedence
- prometheusProperties.getDefaultMetricProperties(), // second-highest precedence
- builder.getDefaultProperties() // fallback
- };
+ properties.add(prometheusProperties.getMetricProperties(metricName));
}
+ properties.add(builder.toProperties());
+ properties.add(prometheusProperties.getDefaultMetricProperties());
+ properties.add(builder.getDefaultProperties()); // fallback
+ return properties.toArray(new MetricsProperties[0]);
}
protected
P getConfigProperty(
@@ -188,12 +194,10 @@ protected
P getConfigProperty(
"Missing default config. This is a bug in the Prometheus metrics core library.");
}
- protected abstract boolean isExemplarsEnabled();
-
abstract static class Builder, M extends StatefulMetric, ?>>
extends MetricWithFixedMetadata.Builder {
- protected Boolean exemplarsEnabled;
+ @Nullable protected Boolean exemplarsEnabled;
protected Builder(List illegalLabelNames, PrometheusProperties config) {
super(illegalLabelNames, config);
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Summary.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Summary.java
index ddb3eb7f2..47b6e2a9c 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Summary.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Summary.java
@@ -1,10 +1,13 @@
package io.prometheus.metrics.core.metrics;
+import static java.util.Objects.requireNonNull;
+
import io.prometheus.metrics.config.MetricsProperties;
import io.prometheus.metrics.config.PrometheusProperties;
import io.prometheus.metrics.core.datapoints.DistributionDataPoint;
import io.prometheus.metrics.core.exemplars.ExemplarSampler;
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.Exemplars;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.Quantile;
@@ -16,6 +19,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAdder;
+import javax.annotation.Nullable;
/**
* Summary metric. Example:
@@ -42,21 +46,25 @@ public class Summary extends StatefulMetric quantiles; // May be empty, but cannot be null.
+
private final long maxAgeSeconds;
private final int ageBuckets;
- private final boolean exemplarsEnabled;
- private final ExemplarSamplerConfig exemplarSamplerConfig;
+ @Nullable private final ExemplarSamplerConfig exemplarSamplerConfig;
private Summary(Builder builder, PrometheusProperties prometheusProperties) {
super(builder);
MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties);
- this.exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
- this.quantiles = Collections.unmodifiableList(makeQuantiles(properties));
- this.maxAgeSeconds = getConfigProperty(properties, MetricsProperties::getSummaryMaxAgeSeconds);
- this.ageBuckets =
- getConfigProperty(properties, MetricsProperties::getSummaryNumberOfAgeBuckets);
- this.exemplarSamplerConfig =
- new ExemplarSamplerConfig(prometheusProperties.getExemplarProperties(), 4);
+ quantiles = Collections.unmodifiableList(makeQuantiles(properties));
+ maxAgeSeconds = getConfigProperty(properties, MetricsProperties::getSummaryMaxAgeSeconds);
+ ageBuckets = getConfigProperty(properties, MetricsProperties::getSummaryNumberOfAgeBuckets);
+ boolean exemplarsEnabled =
+ getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
+ if (exemplarsEnabled) {
+ exemplarSamplerConfig =
+ new ExemplarSamplerConfig(prometheusProperties.getExemplarProperties(), 4);
+ } else {
+ exemplarSamplerConfig = null;
+ }
}
private List makeQuantiles(MetricsProperties[] properties) {
@@ -78,8 +86,13 @@ private List makeQuantiles(MetricsProperties[] propertie
}
@Override
- protected boolean isExemplarsEnabled() {
- return exemplarsEnabled;
+ public double getSum() {
+ return getNoLabels().getSum();
+ }
+
+ @Override
+ public long getCount() {
+ return getNoLabels().getCount();
}
@Override
@@ -106,6 +119,11 @@ protected SummarySnapshot collect(List labels, List metricDat
return new SummarySnapshot(getMetadata(), data);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.SUMMARY;
+ }
+
@Override
protected DataPoint newDataPoint() {
return new DataPoint();
@@ -115,14 +133,16 @@ public class DataPoint implements DistributionDataPoint {
private final LongAdder count = new LongAdder();
private final DoubleAdder sum = new DoubleAdder();
- private final SlidingWindow quantileValues;
+ @Nullable private final SlidingWindow quantileValues;
private final Buffer buffer = new Buffer();
- private final ExemplarSampler exemplarSampler;
+ @Nullable private final ExemplarSampler exemplarSampler;
private final long createdTimeMillis = System.currentTimeMillis();
private DataPoint() {
- if (quantiles.size() > 0) {
+ if (quantiles.isEmpty()) {
+ quantileValues = null;
+ } else {
CKMSQuantiles.Quantile[] quantilesArray = quantiles.toArray(new CKMSQuantiles.Quantile[0]);
quantileValues =
new SlidingWindow<>(
@@ -131,16 +151,24 @@ private DataPoint() {
CKMSQuantiles::insert,
maxAgeSeconds,
ageBuckets);
- } else {
- quantileValues = null;
}
- if (exemplarsEnabled) {
+ if (exemplarSamplerConfig != null) {
exemplarSampler = new ExemplarSampler(exemplarSamplerConfig);
} else {
exemplarSampler = null;
}
}
+ @Override
+ public double getSum() {
+ return sum.sum();
+ }
+
+ @Override
+ public long getCount() {
+ return count.sum();
+ }
+
@Override
public void observe(double value) {
if (Double.isNaN(value)) {
@@ -149,7 +177,7 @@ public void observe(double value) {
if (!buffer.append(value)) {
doObserve(value);
}
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observe(value);
}
}
@@ -162,7 +190,7 @@ public void observeWithExemplar(double value, Labels labels) {
if (!buffer.append(value)) {
doObserve(value);
}
- if (isExemplarsEnabled()) {
+ if (exemplarSampler != null) {
exemplarSampler.observeWithExemplar(value, labels);
}
}
@@ -180,7 +208,13 @@ private void doObserve(double amount) {
private SummarySnapshot.SummaryDataPointSnapshot collect(Labels labels) {
return buffer.run(
expectedCount -> count.sum() == expectedCount,
- // TODO Exemplars (are hard-coded as empty in the line below)
+ // Note: Exemplars are currently hard-coded as empty for Summary metrics.
+ // While exemplars are sampled during observe() and observeWithExemplar() calls
+ // via the exemplarSampler field, they are not included in the snapshot to maintain
+ // consistency with the buffering mechanism. The buffer.run() ensures atomic
+ // collection of count, sum, and quantiles. Adding exemplars would require
+ // coordination between the buffer and exemplarSampler, which could impact
+ // performance. Consider using Histogram instead if exemplars are needed.
() ->
new SummarySnapshot.SummaryDataPointSnapshot(
count.sum(),
@@ -201,7 +235,8 @@ private Quantiles makeQuantiles() {
for (int i = 0; i < getQuantiles().size(); i++) {
CKMSQuantiles.Quantile quantile = getQuantiles().get(i);
quantiles[i] =
- new Quantile(quantile.quantile, quantileValues.current().get(quantile.quantile));
+ new Quantile(
+ quantile.quantile, requireNonNull(quantileValues).current().get(quantile.quantile));
}
return Quantiles.of(quantiles);
}
@@ -224,8 +259,8 @@ public static class Builder extends StatefulMetric.Builder quantiles = new ArrayList<>();
- private Long maxAgeSeconds;
- private Integer ageBuckets;
+ @Nullable private Long maxAgeSeconds;
+ @Nullable private Integer ageBuckets;
private Builder(PrometheusProperties properties) {
super(Collections.singletonList("quantile"), properties);
@@ -326,10 +361,15 @@ protected MetricsProperties toProperties() {
quantileErrors[i] = this.quantiles.get(i).epsilon;
}
}
- return MetricsProperties.builder()
+ MetricsProperties.Builder builder = MetricsProperties.builder();
+ if (quantiles != null) {
+ builder.summaryQuantiles(quantiles);
+ }
+ if (quantileErrors != null) {
+ builder.summaryQuantileErrors(quantileErrors);
+ }
+ return builder
.exemplarsEnabled(exemplarsEnabled)
- .summaryQuantiles(quantiles)
- .summaryQuantileErrors(quantileErrors)
.summaryNumberOfAgeBuckets(ageBuckets)
.summaryMaxAgeSeconds(maxAgeSeconds)
.build();
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SummaryWithCallback.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SummaryWithCallback.java
index dbe61b2ce..fa823e68e 100644
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SummaryWithCallback.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SummaryWithCallback.java
@@ -1,6 +1,7 @@
package io.prometheus.metrics.core.metrics;
import io.prometheus.metrics.config.PrometheusProperties;
+import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.Exemplars;
import io.prometheus.metrics.model.snapshots.Quantiles;
import io.prometheus.metrics.model.snapshots.SummarySnapshot;
@@ -8,6 +9,7 @@
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
+import javax.annotation.Nullable;
/**
* Example:
@@ -44,10 +46,10 @@ public interface Callback {
private SummaryWithCallback(Builder builder) {
super(builder);
- this.callback = builder.callback;
- if (callback == null) {
+ if (builder.callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
+ this.callback = builder.callback;
}
@Override
@@ -62,6 +64,11 @@ public SummarySnapshot collect() {
return new SummarySnapshot(getMetadata(), dataPoints);
}
+ @Override
+ public MetricType getMetricType() {
+ return MetricType.SUMMARY;
+ }
+
public static Builder builder() {
return new Builder(PrometheusProperties.get());
}
@@ -73,7 +80,7 @@ public static Builder builder(PrometheusProperties properties) {
public static class Builder
extends CallbackMetric.Builder {
- private Consumer callback;
+ @Nullable private Consumer callback;
public Builder callback(Consumer callback) {
this.callback = callback;
diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerTest.java
index 059929c22..8ba1370da 100644
--- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerTest.java
+++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/ExemplarSamplerTest.java
@@ -52,24 +52,24 @@ public void markCurrentSpanAsExemplar() {}
}
@Test
- public void testCustomExemplarsBuckets() throws Exception {
+ void testCustomExemplarsBuckets() throws Exception {
// TODO
}
private io.prometheus.metrics.tracer.common.SpanContext origContext;
@BeforeEach
- public void setUp() {
+ void setUp() {
origContext = SpanContextSupplier.getSpanContext();
}
@AfterEach
- public void tearDown() {
+ void tearDown() {
SpanContextSupplier.setSpanContext(origContext);
}
@Test
- public void testIsSampled() throws Exception {
+ void testIsSampled() throws Exception {
SpanContext context = new SpanContext();
context.isSampled = false;
ExemplarSampler sampler = new ExemplarSampler(makeConfig(), context);
@@ -79,7 +79,7 @@ public void testIsSampled() throws Exception {
}
@Test
- public void testDefaultConfigHasFourExemplars() throws Exception {
+ void testDefaultConfigHasFourExemplars() throws Exception {
ExemplarSampler sampler = new ExemplarSampler(makeConfig(), new SpanContext());
Thread.sleep(tick); // t = 1 tick
sampler.observe(0.3);
@@ -96,7 +96,7 @@ public void testDefaultConfigHasFourExemplars() throws Exception {
}
@Test
- public void testEmptyBuckets() throws Exception {
+ void testEmptyBuckets() throws Exception {
ExemplarSampler sampler =
new ExemplarSampler(makeConfig(Double.POSITIVE_INFINITY), new SpanContext());
Thread.sleep(tick); // t = 1 tick
@@ -108,7 +108,7 @@ public void testEmptyBuckets() throws Exception {
}
@Test
- public void testDefaultExemplarsBuckets() throws Exception {
+ void testDefaultExemplarsBuckets() throws Exception {
ExemplarSampler sampler =
new ExemplarSampler(
makeConfig(0.2, 0.4, 0.6, 0.8, 1.0, Double.POSITIVE_INFINITY), new SpanContext());
@@ -136,12 +136,12 @@ public void testDefaultExemplarsBuckets() throws Exception {
}
@Test
- public void testCustomExemplarsNoBuckets() throws Exception {
+ void testCustomExemplarsNoBuckets() throws Exception {
// TODO
}
@Test
- public void testDefaultExemplarsNoBuckets() throws Exception {
+ void testDefaultExemplarsNoBuckets() throws Exception {
ExemplarSampler sampler = new ExemplarSampler(makeConfig(), new SpanContext());
Scheduler.awaitInitialization();
Thread.sleep(tick); // t = 1 tick
diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/SpanContextSupplierTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/SpanContextSupplierTest.java
index 6c89b48a5..5ddfb2ad5 100644
--- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/SpanContextSupplierTest.java
+++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/exemplars/SpanContextSupplierTest.java
@@ -51,12 +51,12 @@ public void markCurrentSpanAsExemplar() {}
);
@BeforeEach
- public void setUp() {
+ void setUp() {
origSpanContext = SpanContextSupplier.getSpanContext();
}
@AfterEach
- public void tearDown() {
+ void tearDown() {
SpanContextSupplier.setSpanContext(origSpanContext);
}
@@ -66,7 +66,7 @@ public void tearDown() {
* SpanContextSupplier}.
*/
@Test
- public void testConstructorInjection() {
+ void testConstructorInjection() {
ExemplarsProperties properties = ExemplarsProperties.builder().build();
ExemplarSamplerConfig config = new ExemplarSamplerConfig(properties, 1);
ExemplarSampler exemplarSampler = new ExemplarSampler(config, spanContextA);
@@ -86,7 +86,7 @@ public void testConstructorInjection() {
* ExemplarSampler}).
*/
@Test
- public void testUpdateSpanContext() throws InterruptedException {
+ void testUpdateSpanContext() throws InterruptedException {
ExemplarSampler exemplarSampler = new ExemplarSampler(config);
SpanContextSupplier.setSpanContext(spanContextB);
diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CKMSQuantilesTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CKMSQuantilesTest.java
index bd6b177a4..5a7fd7f48 100644
--- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CKMSQuantilesTest.java
+++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CKMSQuantilesTest.java
@@ -20,13 +20,13 @@ class CKMSQuantilesTest {
private final Quantile qMax = new Quantile(1.0, 0.00);
@Test
- public void testGetOnEmptyValues() {
+ void testGetOnEmptyValues() {
CKMSQuantiles ckms = new CKMSQuantiles(q50, q95, q99);
assertThat(Double.isNaN(ckms.get(q95.quantile))).isTrue();
}
@Test
- public void testGet() {
+ void testGet() {
Random random = new Random(0);
CKMSQuantiles ckms = new CKMSQuantiles(q50, q95, q99);
List input = shuffledValues(100, random);
@@ -37,7 +37,7 @@ public void testGet() {
}
@Test
- public void testBatchInsert() {
+ void testBatchInsert() {
Random random = new Random(1);
testInsertBatch(1, 1, 100, random);
testInsertBatch(1, 10, 100, random);
@@ -87,7 +87,7 @@ private void testInsertBatch(
}
@Test
- public void testGetWithAMillionElements() {
+ void testGetWithAMillionElements() {
Random random = new Random(2);
List input = shuffledValues(1000 * 1000, random);
CKMSQuantiles ckms = new CKMSQuantiles(q50, q95, q99);
@@ -99,7 +99,7 @@ public void testGetWithAMillionElements() {
}
@Test
- public void testMin() {
+ void testMin() {
Random random = new Random(3);
List input = shuffledValues(1000, random);
CKMSQuantiles ckms = new CKMSQuantiles(qMin);
@@ -112,7 +112,7 @@ public void testMin() {
}
@Test
- public void testMax() {
+ void testMax() {
Random random = new Random(4);
List input = shuffledValues(1000, random);
CKMSQuantiles ckms = new CKMSQuantiles(qMax);
@@ -125,7 +125,7 @@ public void testMax() {
}
@Test
- public void testMinMax() {
+ void testMinMax() {
Random random = new Random(5);
List input = shuffledValues(1000, random);
CKMSQuantiles ckms = new CKMSQuantiles(qMin, qMax);
@@ -138,7 +138,7 @@ public void testMinMax() {
}
@Test
- public void testMinAndOthers() {
+ void testMinAndOthers() {
Random random = new Random(6);
List input = shuffledValues(1000, random);
CKMSQuantiles ckms = new CKMSQuantiles(q95, qMin);
@@ -150,7 +150,7 @@ public void testMinAndOthers() {
}
@Test
- public void testMaxAndOthers() {
+ void testMaxAndOthers() {
Random random = new Random(7);
List input = shuffledValues(10000, random);
CKMSQuantiles ckms = new CKMSQuantiles(q50, q95, qMax);
@@ -162,7 +162,7 @@ public void testMaxAndOthers() {
}
@Test
- public void testMinMaxAndOthers() {
+ void testMinMaxAndOthers() {
Random random = new Random(8);
List input = shuffledValues(10000, random);
CKMSQuantiles ckms = new CKMSQuantiles(qMin, q50, q95, q99, qMax);
@@ -174,7 +174,7 @@ public void testMinMaxAndOthers() {
}
@Test
- public void testExactQuantile() {
+ void testExactQuantile() {
Random random = new Random(9);
List input = shuffledValues(10000, random);
CKMSQuantiles ckms = new CKMSQuantiles(new Quantile(0.95, 0));
@@ -187,7 +187,7 @@ public void testExactQuantile() {
}
@Test
- public void testExactAndOthers() {
+ void testExactAndOthers() {
Random random = new Random(10);
List input = shuffledValues(10000, random);
CKMSQuantiles ckms = new CKMSQuantiles(q50, new Quantile(0.95, 0), q99);
@@ -200,7 +200,7 @@ public void testExactAndOthers() {
}
@Test
- public void testExactAndMin() {
+ void testExactAndMin() {
Random random = new Random(11);
List input = shuffledValues(10000, random);
CKMSQuantiles ckms = new CKMSQuantiles(qMin, q50, new Quantile(0.95, 0));
@@ -213,7 +213,7 @@ public void testExactAndMin() {
}
@Test
- public void testMaxEpsilon() {
+ void testMaxEpsilon() {
Random random = new Random(12);
List input = shuffledValues(10000, random);
// epsilon == 1 basically gives you random results, but it should still not throw an exception.
@@ -225,7 +225,7 @@ public void testMaxEpsilon() {
}
@Test
- public void testGetGaussian() {
+ void testGetGaussian() {
RandomGenerator rand = new JDKRandomGenerator();
rand.setSeed(0);
@@ -284,7 +284,7 @@ public void testGetGaussian() {
}
@Test
- public void testIllegalArgumentException() {
+ void testIllegalArgumentException() {
try {
new Quantile(-1, 0);
} catch (IllegalArgumentException e) {
diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java
index b5ca3be15..b6d6779d7 100644
--- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java
+++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java
@@ -5,10 +5,14 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.data.Offset.offset;
+import io.prometheus.metrics.config.EscapingScheme;
+import io.prometheus.metrics.config.MetricsProperties;
+import io.prometheus.metrics.config.PrometheusProperties;
import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfigTestUtil;
-import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
+import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_33_5.Metrics;
import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl;
import io.prometheus.metrics.expositionformats.internal.ProtobufUtil;
+import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
import io.prometheus.metrics.model.snapshots.Exemplar;
import io.prometheus.metrics.model.snapshots.Label;
@@ -21,6 +25,8 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
class CounterTest {
@@ -31,7 +37,7 @@ class CounterTest {
private SpanContext origSpanContext;
@BeforeEach
- public void setUp() throws NoSuchFieldException, IllegalAccessException {
+ void setUp() throws NoSuchFieldException, IllegalAccessException {
noLabels = Counter.builder().name("nolabels").build();
labels =
Counter.builder().name("labels").help("help").unit(Unit.SECONDS).labelNames("l").build();
@@ -43,7 +49,7 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException {
}
@AfterEach
- public void tearDown() {
+ void tearDown() {
SpanContextSupplier.setSpanContext(origSpanContext);
}
@@ -66,7 +72,7 @@ private int getNumberOfLabels(Counter counter) {
}
@Test
- public void testIncrement() {
+ void testIncrement() {
noLabels.inc();
assertThat(getValue(noLabels)).isCloseTo(1.0, offset(.001));
noLabels.inc(2);
@@ -78,20 +84,20 @@ public void testIncrement() {
}
@Test
- public void testNegativeIncrementFails() {
+ void testNegativeIncrementFails() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> noLabels.inc(-1))
.withMessage("Negative increment -1 is illegal for Counter metrics.");
}
@Test
- public void testEmptyCountersHaveNoLabels() {
+ void testEmptyCountersHaveNoLabels() {
assertThat(getNumberOfLabels(noLabels)).isOne();
assertThat(getNumberOfLabels(labels)).isZero();
}
@Test
- public void testLabels() {
+ void testLabels() {
assertThat(getNumberOfLabels(labels)).isZero();
labels.labelValues("a").inc();
assertThat(getNumberOfLabels(labels)).isOne();
@@ -102,26 +108,25 @@ public void testLabels() {
assertThat(getValue(labels, "l", "b")).isCloseTo(3.0, offset(.001));
}
- @Test
- public void testTotalStrippedFromName() {
- for (String name :
- new String[] {
- "my_counter_total", "my.counter.total",
- "my_counter_seconds_total", "my.counter.seconds.total",
- "my_counter", "my.counter",
- "my_counter_seconds", "my.counter.seconds"
- }) {
- Counter counter = Counter.builder().name(name).unit(Unit.SECONDS).build();
- Metrics.MetricFamily protobufData =
- new PrometheusProtobufWriterImpl().convert(counter.collect());
- assertThat(ProtobufUtil.shortDebugString(protobufData))
- .isEqualTo(
- "name: \"my_counter_seconds_total\" type: COUNTER metric { counter { value: 0.0 } }");
- }
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "my_counter_total",
+ "my_counter_seconds_total",
+ "my_counter",
+ "my_counter_seconds",
+ })
+ void testTotalStrippedFromName(String name) {
+ Counter counter = Counter.builder().name(name).unit(Unit.SECONDS).build();
+ Metrics.MetricFamily protobufData =
+ new PrometheusProtobufWriterImpl().convert(counter.collect(), EscapingScheme.ALLOW_UTF8);
+ assertThat(ProtobufUtil.shortDebugString(protobufData))
+ .matches(
+ "^name: \"my_counter_seconds_total\" type: COUNTER metric \\{ counter \\{ value: 0.0 created_timestamp \\{ seconds: \\d+ nanos: \\d+ } } }$");
}
@Test
- public void testSnapshotComplete() {
+ void testSnapshotComplete() {
long before = System.currentTimeMillis();
Counter counter =
Counter.builder()
@@ -176,7 +181,7 @@ public void testSnapshotComplete() {
}
@Test
- public void testIncWithExemplar() throws Exception {
+ void testIncWithExemplar() throws Exception {
noLabels.incWithExemplar(Labels.of("key", "value"));
assertExemplar(noLabels, 1.0, "key", "value");
@@ -198,7 +203,7 @@ private void assertExemplar(Counter counter, double value, String... labels) {
}
@Test
- public void testExemplarSampler() throws Exception {
+ void testExemplarSampler() throws Exception {
Exemplar exemplar1 = Exemplar.builder().value(2.0).traceId("abc").spanId("123").build();
Exemplar exemplar2 = Exemplar.builder().value(1.0).traceId("def").spanId("456").build();
Exemplar exemplar3 = Exemplar.builder().value(1.0).traceId("123").spanId("abc").build();
@@ -316,14 +321,21 @@ void incWithExemplar2() {
}
@Test
- public void testExemplarSamplerDisabled() {
- Counter counter =
- Counter.builder()
- // .withExemplarSampler((inc, prev) -> {throw new RuntimeException("unexpected call to
- // exemplar sampler");})
- .name("count_total")
- .withoutExemplars()
+ void testExemplarSamplerDisabled() {
+ Counter counter = Counter.builder().name("count_total").withoutExemplars().build();
+ counter.incWithExemplar(3.0, Labels.of("a", "b"));
+ assertThat(getData(counter).getExemplar()).isNull();
+ counter.inc(2.0);
+ assertThat(getData(counter).getExemplar()).isNull();
+ }
+
+ @Test
+ void testExemplarSamplerDisabled_enabledByDefault() {
+ PrometheusProperties properties =
+ PrometheusProperties.builder()
+ .defaultMetricsProperties(MetricsProperties.builder().exemplarsEnabled(true).build())
.build();
+ Counter counter = Counter.builder(properties).name("count_total").withoutExemplars().build();
counter.incWithExemplar(3.0, Labels.of("a", "b"));
assertThat(getData(counter).getExemplar()).isNull();
counter.inc(2.0);
@@ -331,7 +343,20 @@ public void testExemplarSamplerDisabled() {
}
@Test
- public void testConstLabelsFirst() {
+ void testExemplarSamplerDisabledInBuilder_enabledByPropertiesOnMetric() {
+ PrometheusProperties properties =
+ PrometheusProperties.builder()
+ .putMetricProperty("count", MetricsProperties.builder().exemplarsEnabled(true).build())
+ .build();
+ Counter counter = Counter.builder(properties).name("count_total").withoutExemplars().build();
+ counter.incWithExemplar(3.0, Labels.of("a", "b"));
+ assertThat(getData(counter).getExemplar()).isNull();
+ counter.inc(2.0);
+ assertThat(getData(counter).getExemplar()).isNull();
+ }
+
+ @Test
+ void testConstLabelsFirst() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(
() ->
@@ -343,7 +368,7 @@ public void testConstLabelsFirst() {
}
@Test
- public void testConstLabelsSecond() {
+ void testConstLabelsSecond() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(
() ->
@@ -353,4 +378,17 @@ public void testConstLabelsSecond() {
.constLabels(Labels.of("const_a", "const_b"))
.build());
}
+
+ @Test
+ void testLabelNormalizationInRegistration() {
+ PrometheusRegistry registry = new PrometheusRegistry();
+
+ Counter.builder().name("requests").labelNames("request.count").register(registry);
+
+ // request.count and request_count normalize to the same name
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(
+ () -> Counter.builder().name("requests").labelNames("request_count").register(registry))
+ .withMessageContaining("duplicate metric name with identical label schema");
+ }
}
diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterWithCallbackTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterWithCallbackTest.java
index 47c5f5c57..2907a8a02 100644
--- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterWithCallbackTest.java
+++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterWithCallbackTest.java
@@ -13,7 +13,7 @@
class CounterWithCallbackTest {
@Test
- public void testCounter() {
+ void testCounter() {
final AtomicInteger value = new AtomicInteger(1);
List labelValues = Arrays.asList("v1", "v2");
CounterWithCallback counter =
@@ -38,7 +38,7 @@ public void testCounter() {
}
@Test
- public void testCounterNoCallback() {
+ void testCounterNoCallback() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(
() -> CounterWithCallback.builder().name("counter").labelNames("l1", "l2").build());
diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CustomBucketsHistogramTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CustomBucketsHistogramTest.java
new file mode 100644
index 000000000..347f775cf
--- /dev/null
+++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CustomBucketsHistogramTest.java
@@ -0,0 +1,470 @@
+package io.prometheus.metrics.core.metrics;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.Offset.offset;
+
+import io.prometheus.metrics.config.EscapingScheme;
+import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
+import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_33_5.Metrics;
+import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl;
+import io.prometheus.metrics.model.snapshots.ClassicHistogramBucket;
+import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
+import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
+import io.prometheus.metrics.model.snapshots.Labels;
+import io.prometheus.metrics.model.snapshots.MetricSnapshots;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Comprehensive tests to verify that client_java supports native histograms with custom buckets
+ * (NHCB).
+ *
+ *
According to the Prometheus specification
+ * (https://prometheus.io/docs/specs/native_histograms/), native histograms with custom buckets
+ * (schema -53) are exposed as classic histograms with custom bucket boundaries. Prometheus servers
+ * can then convert these to NHCB upon ingestion when configured with
+ * convert_classic_histograms_to_nhcb.
+ *
+ *
These tests verify that:
+ *
+ *
+ *
Histograms with custom bucket boundaries can be created
+ *
Custom buckets are properly exposed in both text and protobuf formats
+ *
Both classic-only and dual (classic+native) histograms work with custom buckets
+ *
Various custom bucket configurations (linear, exponential, arbitrary) work correctly
+ *
\n"
+ "The name[] parameter can be used by the Prometheus server for scraping. "
+ "Add the following snippet to your scrape job configuration in "
@@ -50,13 +53,17 @@ public DefaultHandler() {
+ "The Prometheus Java metrics library supports a debug query parameter "
+ "for viewing the different formats in a Web browser:\n"
+ "
%s?debug=text: ", metrics, metricsPath)
+ "View Prometheus text format (this is the default when accessing the "
- + "/metrics endpoint with a Web browser).