diff --git a/.circleci/config.yml b/.circleci/config.yml index 33e250dec5..ea8637084d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,31 +1,38 @@ version: 2.1 executors: + circle-jdk23-executor: + working_directory: ~/micrometer + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + resource_class: medium+ + docker: + - image: cimg/openjdk:23.0.1 circle-jdk-executor: working_directory: ~/micrometer environment: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' resource_class: medium+ docker: - - image: cimg/openjdk:21.0.2 + - image: cimg/openjdk:21.0.5 circle-jdk17-executor: working_directory: ~/micrometer environment: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' resource_class: medium+ docker: - - image: cimg/openjdk:17.0.11 + - image: cimg/openjdk:17.0.13 circle-jdk11-executor: working_directory: ~/micrometer environment: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' resource_class: medium+ docker: - - image: cimg/openjdk:11.0.22 + - image: cimg/openjdk:11.0.25 machine-executor: working_directory: ~/micrometer machine: - image: ubuntu-2204:2024.05.1 + image: ubuntu-2404:2024.08.1 commands: gradlew-build: @@ -60,6 +67,11 @@ commands: path: ~/micrometer/test-results/ jobs: + build-jdk23: + executor: circle-jdk23-executor + steps: + - gradlew-build + build: executor: circle-jdk-executor steps: @@ -107,13 +119,17 @@ workflows: - build - build-jdk11 - build-jdk17 + - build-jdk23 - concurrency-tests - docker-tests - deploy: + context: + - deploy requires: - build - build-jdk11 - build-jdk17 + - build-jdk23 - concurrency-tests - docker-tests filters: @@ -141,6 +157,12 @@ workflows: ignore: /.*/ tags: only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ + - build-jdk23: + filters: + branches: + ignore: /.*/ + tags: + only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ - concurrency-tests: filters: branches: @@ -154,10 +176,13 @@ workflows: tags: only: /^v\d+\.\d+\.\d+(-(RC|M)\d+)?$/ - deploy: + context: + - deploy requires: - build - build-jdk11 - build-jdk17 + - build-jdk23 - concurrency-tests - docker-tests filters: diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 5318e55b7e..0000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.lockfile linguist-generated \ No newline at end of file diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 0000000000..0c4b142e9a --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e888ab2f46..04d505f247 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,8 +26,8 @@ updates: directory: "/" schedule: interval: monthly - target-branch: "1.12.x" - milestone: 211 + target-branch: "1.13.x" + milestone: 233 ignore: # metrics are better with https://github.com/Netflix/Hystrix/pull/1568 introduced # in hystrix 1.5.12, but Netflix re-released 1.5.11 as 1.5.18 late in 2018. @@ -43,8 +43,8 @@ updates: directory: "/" schedule: interval: monthly - target-branch: "1.13.x" - milestone: 233 + target-branch: "1.14.x" + milestone: 250 ignore: # metrics are better with https://github.com/Netflix/Hystrix/pull/1568 introduced # in hystrix 1.5.12, but Netflix re-released 1.5.11 as 1.5.18 late in 2018. diff --git a/.github/workflows/post-release-workflow.yml b/.github/workflows/post-release-workflow.yml new file mode 100644 index 0000000000..6717114491 --- /dev/null +++ b/.github/workflows/post-release-workflow.yml @@ -0,0 +1,112 @@ +name: Post Release Workflow + +on: + workflow_dispatch: # Enables manual trigger + +jobs: + generate-release-notes: + name: Generate Release Notes + runs-on: ubuntu-latest + + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Download Changelog Generator + run: | + curl -L -o github-changelog-generator.jar https://github.com/spring-io/github-changelog-generator/releases/download/v0.0.11/github-changelog-generator.jar + + - name: Generate release notes + id: generate_notes + run: | + java -jar github-changelog-generator.jar \ + ${GITHUB_REF_NAME#v} \ + changelog.md \ + --changelog.repository="${{ github.repository }}" \ + --github.token="${{ secrets.GITHUB_TOKEN }}" + + - name: Run script to process Markdown file + run: python .github/workflows/process_changelog.py + + - name: Update release text + run: | + echo -e "::Info::Original changelog\n\n" + cat changelog.md + + echo -e "\n\n" + echo -e "::Info::Processed changelog\n\n" + cat changelog-output.md + gh release edit ${{ github.ref_name }} --notes-file changelog-output.md + env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + + close-milestone: + name: Close Milestone + runs-on: ubuntu-latest + needs: generate-release-notes + steps: + - name: Close milestone + run: | + # Extract version without 'v' prefix + milestone_name=${GITHUB_REF_NAME#v} + + echo "Closing milestone: $milestone_name" + + # List milestones and find the ID + milestone_id=$(gh api "/repos/${{ github.repository }}/milestones?state=open" \ + --jq ".[] | select(.title == \"$milestone_name\").number") + + if [ -z "$milestone_id" ]; then + echo "::error::Milestone '$milestone_name' not found" + exit 1 + fi + + # Close the milestone + gh api --method PATCH "/repos/${{ github.repository }}/milestones/$milestone_id" \ + -f state=closed + + echo "Successfully closed milestone: $milestone_name" + env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + + notify: + name: Send Notifications + runs-on: ubuntu-latest + needs: close-milestone + + steps: + - name: Announce Release on `Spring-Releases` space + run: | + milestone_name=${GITHUB_REF_NAME#v} + curl --location --request POST '${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }}' \ + --header 'Content-Type: application/json' \ + --data-raw "{ text: \"${{ github.event.repository.name }}-announcing ${milestone_name}\"}" + + - name: Post on Bluesky + env: + BSKY_IDENTIFIER: ${{ secrets.BLUESKY_HANDLE }} + BSKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} + run: | + # First get the session token + SESSION_TOKEN=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \ + -H "Content-Type: application/json" \ + -d "{\"identifier\":\"$BSKY_IDENTIFIER\",\"password\":\"$BSKY_PASSWORD\"}" | \ + jq -r .accessJwt) + + # Create post content + VERSION=${GITHUB_REF_NAME#v} + POST_TEXT="${{ github.event.repository.name }} ${VERSION} has been released!\n\nCheck out the changelog: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}" + + # Create the post + curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SESSION_TOKEN}" \ + -d "{ + \"repo\": \"$BSKY_IDENTIFIER\", + \"collection\": \"app.bsky.feed.post\", + \"record\": { + \"\$type\": \"app.bsky.feed.post\", + \"text\": \"$POST_TEXT\", + \"createdAt\": \"$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")\" + } + }" diff --git a/.github/workflows/process_changelog.py b/.github/workflows/process_changelog.py new file mode 100644 index 0000000000..584618406f --- /dev/null +++ b/.github/workflows/process_changelog.py @@ -0,0 +1,148 @@ +import re +import subprocess + +input_file = "changelog.md" +output_file = "changelog-output.md" + +def fetch_test_and_optional_dependencies(): + # Fetch the list of all subprojects + result = subprocess.run( + ["./gradlew", "projects"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + subprojects = [] + for line in result.stdout.splitlines(): + match = re.match(r".*Project (':.+')", line) + if match: + subprojects.append(match.group(1).strip("'")) + + print(f"Found the following subprojects\n\n {subprojects}\n\n") + test_optional_dependencies = set() + implementation_dependencies = set() + + print("Will fetch non transitive dependencies for all subprojects...") + # Run dependencies task for all subprojects in a single Gradle command + if subprojects: + dependencies_command = ["./gradlew"] + [f"{subproject}:dependencies" for subproject in subprojects] + result = subprocess.run( + dependencies_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + in_test_section = False + in_optional_section = False + in_implementation_section = False + + for line in result.stdout.splitlines(): + if "project :" in line: + continue + + # Detect gradle plugin + if "classpath" in line: + in_optional_section = True + continue + + # Detect test dependencies section + if "testCompileClasspath" in line or "testImplementation" in line: + in_test_section = True + continue + if "runtimeClasspath" in line or line.strip() == "": + in_test_section = False + + # Detect optional dependencies section + if "compileOnly" in line: + in_optional_section = True + continue + if line.strip() == "": + in_optional_section = False + + # Detect implementation dependencies section + if "implementation" in line or "compileClasspath" in line: + in_implementation_section = True + continue + if line.strip() == "": + in_implementation_section = False + + # Parse dependencies explicitly declared with +--- or \--- + match = re.match(r"[\\+|\\\\]--- ([^:]+):([^:]+):([^ ]+)", line) + if match: + group_id, artifact_id, _ = match.groups() + dependency_key = f"{group_id}:{artifact_id}" + if in_test_section or in_optional_section: + test_optional_dependencies.add(dependency_key) + if in_implementation_section: + implementation_dependencies.add(dependency_key) + + # Remove dependencies from test/optional if they are also in implementation + final_exclusions = test_optional_dependencies - implementation_dependencies + + print(f"Dependencies in either test or optional scope to be excluded from changelog processing:\n\n{final_exclusions}\n\n") + return final_exclusions + +def process_dependency_upgrades(lines, exclude_dependencies): + dependencies = {} + regex = re.compile(r"- Bump (.+?) from ([\d\.]+) to ([\d\.]+) \[(#[\d]+)\]\((.+)\)") + for line in lines: + match = regex.match(line) + if match: + unit, old_version, new_version, pr_number, link = match.groups() + if unit not in exclude_dependencies: + if unit not in dependencies: + dependencies[unit] = {"lowest": old_version, "highest": new_version, "pr_number": pr_number, "link": link} + else: + dependencies[unit]["lowest"] = min(dependencies[unit]["lowest"], old_version) + dependencies[unit]["highest"] = max(dependencies[unit]["highest"], new_version) + sorted_units = sorted(dependencies.keys()) + return [f"- Bump {unit} from {dependencies[unit]['lowest']} to {dependencies[unit]['highest']} [{dependencies[unit]['pr_number']}]({dependencies[unit]['link']})" for unit in sorted_units] + +with open(input_file, "r") as file: + lines = file.readlines() + +# Fetch test and optional dependencies from all projects +print("Fetching test and optional dependencies from the project and its subprojects...") +exclude_dependencies = fetch_test_and_optional_dependencies() + +# Step 1: Copy all content until the hammer line +header = [] +dependency_lines = [] +footer = [] +in_dependency_section = False + +print("Parsing changelog until the dependency upgrades section...") + +for line in lines: + if line.startswith("## :hammer: Dependency Upgrades"): + in_dependency_section = True + header.append(line) + header.append("\n") + break + header.append(line) + +print("Parsing dependency upgrade section...") + +# Step 2: Parse dependency upgrades +if in_dependency_section: + for line in lines[len(header):]: + if line.startswith("## :heart: Contributors"): + break + dependency_lines.append(line) + +print("Parsing changelog to find everything after the dependency upgrade section...") +# Find the footer starting from the heart line +footer_start_index = next((i for i, line in enumerate(lines) if line.startswith("## :heart: Contributors")), None) +if footer_start_index is not None: + footer = lines[footer_start_index:] + +print("Processing the dependency upgrades section...") +processed_dependencies = process_dependency_upgrades(dependency_lines, exclude_dependencies) + +print("Writing output...") +# Step 3: Write the output file +with open(output_file, "w") as file: + file.writelines(header) + file.writelines(f"{line}\n" for line in processed_dependencies) + file.writelines("\n") + file.writelines(footer) diff --git a/.gitignore b/.gitignore index c66f719e15..94db3c90b9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,8 @@ bin/ .vscode .DS_Store .java-version + +# jcstress +generated/ +results/ +jcstress-results-*.bin.gz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc782ab88a..36aba587f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,9 +21,10 @@ For example, those with Micrometer knowledge and experience can contribute by: The remainder of this document will focus on guidance for contributing code changes. It will help contributors to build, modify, or test the Micrometer source code. -## Contributor License Agreement +## Include a Signed Off By Trailer -Contributions in the form of source changes require that you fill out and submit the [Contributor License Agreement](https://cla.pivotal.io/sign/pivotal) if you have not done so previously. +All commits must include a *Signed-off-by* trailer at the end of each commit message to indicate that the contributor agrees to the [Developer Certificate of Origin](https://developercertificate.org). +For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring). ## Getting the source @@ -34,10 +35,11 @@ You can use a Git client to clone the source code to your local machine. Micrometer targets Java 8 but requires JDK 11 or later to build. If you are not running Gradle with JDK 11 or later and Gradle cannot detect an existing JDK 17 installation, it will download one. +If you want to build the reference docs, you need to use JDK 17 or later. The Gradle wrapper is provided and should be used for building with a consistent version of Gradle. -The wrapper can be used with a command, for example, `./gradlew check` to build the project and check conventions. +The wrapper can be used with a command, for example, `./gradlew` to build the project and check conventions. ## Importing into an IDE diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/compare/CompareOTLPHistograms.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/compare/CompareOTLPHistograms.java index a5b70cf288..15f5d8f0f5 100644 --- a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/compare/CompareOTLPHistograms.java +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/compare/CompareOTLPHistograms.java @@ -45,12 +45,26 @@ */ @Fork(1) @Measurement(iterations = 2) -@Warmup(iterations = 2) -@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 3) +@BenchmarkMode(Mode.SampleTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) -@Threads(16) +@Threads(2) public class CompareOTLPHistograms { + // disable publishing since we are only benchmarking recording + static OtlpConfig disabledConfig = new OtlpConfig() { + + @Override + public boolean enabled() { + return false; + } + + @Override + public String get(String key) { + return ""; + } + }; + @State(Scope.Thread) public static class Data { @@ -82,7 +96,7 @@ public static class DistributionsWithoutHistogramCumulative { @Setup(Level.Iteration) public void setup() { - registry = new OtlpMeterRegistry(); + registry = new OtlpMeterRegistry(disabledConfig, Clock.SYSTEM); distributionSummary = DistributionSummary.builder("ds").register(registry); timer = Timer.builder("timer").register(registry); } @@ -99,6 +113,11 @@ public static class DistributionsWithoutHistogramDelta { OtlpConfig otlpConfig = new OtlpConfig() { + @Override + public boolean enabled() { + return false; + } + @Override public AggregationTemporality aggregationTemporality() { return AggregationTemporality.DELTA; @@ -142,7 +161,7 @@ public static class ExplicitBucketHistogramCumulative { @Setup(Level.Iteration) public void setup() { - registry = new OtlpMeterRegistry(); + registry = new OtlpMeterRegistry(disabledConfig, Clock.SYSTEM); distributionSummary = DistributionSummary.builder("ds").publishPercentileHistogram().register(registry); timer = Timer.builder("timer").publishPercentileHistogram().register(registry); } @@ -159,6 +178,11 @@ public static class ExplicitBucketHistogramDelta { OtlpConfig otlpConfig = new OtlpConfig() { + @Override + public boolean enabled() { + return false; + } + @Override public AggregationTemporality aggregationTemporality() { return AggregationTemporality.DELTA; @@ -197,6 +221,12 @@ public static class ExponentialHistogramCumulative { MeterRegistry registry; OtlpConfig otlpConfig = new OtlpConfig() { + + @Override + public boolean enabled() { + return false; + } + @Override public HistogramFlavor histogramFlavor() { return HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM; @@ -234,6 +264,11 @@ public static class ExponentialHistogramDelta { OtlpConfig otlpConfig = new OtlpConfig() { + @Override + public boolean enabled() { + return false; + } + @Override public AggregationTemporality aggregationTemporality() { return AggregationTemporality.DELTA; diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/CounterBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/CounterBenchmark.java index 7b12a40aa3..11c995b173 100644 --- a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/CounterBenchmark.java +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/CounterBenchmark.java @@ -52,6 +52,16 @@ public void setup() { counter = registry.counter("counter"); } + @Benchmark + public void baseline() { + // this method was intentionally left blank. + } + + @Benchmark + public Counter retrieve() { + return registry.counter("counter"); + } + @Benchmark public double countSum() { counter.increment(); diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/DefaultLongTaskTimerBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/DefaultLongTaskTimerBenchmark.java new file mode 100644 index 0000000000..9125205bb1 --- /dev/null +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/DefaultLongTaskTimerBenchmark.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ +package io.micrometer.benchmark.core; + +import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.internal.DefaultLongTaskTimer; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 3) +@Measurement(iterations = 3) +public class DefaultLongTaskTimerBenchmark { + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder().include(DefaultLongTaskTimerBenchmark.class.getSimpleName()).build(); + + new Runner(opt).run(); + } + + private static final Random random = new Random(); + + @Param({ "10000", "100000" }) + int activeSampleCount; + + private MockClock clock; + + private DefaultLongTaskTimer longTaskTimer; + + private LongTaskTimer.Sample randomSample; + + @Setup(Level.Invocation) + public void setup() { + clock = new MockClock(); + longTaskTimer = new DefaultLongTaskTimer( + new Meter.Id("ltt", Tags.empty(), TimeUnit.MILLISECONDS.toString().toLowerCase(Locale.ROOT), null, + Meter.Type.LONG_TASK_TIMER), + clock, TimeUnit.MILLISECONDS, DistributionStatisticConfig.DEFAULT, false); + int randomIndex = random.nextInt(activeSampleCount); + // start some samples for benchmarking start/stop with active samples + IntStream.range(0, activeSampleCount).forEach(offset -> { + clock.add(offset, TimeUnit.MILLISECONDS); + LongTaskTimer.Sample sample = longTaskTimer.start(); + if (offset == randomIndex) + randomSample = sample; + }); + clock.add(1, TimeUnit.MILLISECONDS); + } + + @Benchmark + public LongTaskTimer.Sample start() { + return longTaskTimer.start(); + } + + @Benchmark + public long stopRandom() { + return randomSample.stop(); + } + +} diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/GaugeBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/GaugeBenchmark.java new file mode 100644 index 0000000000..4098f7afe4 --- /dev/null +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/GaugeBenchmark.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ +package io.micrometer.benchmark.core; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Fork(1) +@State(Scope.Benchmark) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class GaugeBenchmark { + + private MeterRegistry registry; + + @Setup + public void setup() { + registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + registry.gauge("test.gauge", new AtomicInteger()); + // emits warn because of double registration + registry.gauge("test.gauge", new AtomicInteger()); + // emits debug because of double registration and keeps emitting debug from now on + registry.gauge("test.gauge", new AtomicInteger()); + } + + @Benchmark + public AtomicInteger baseline() { + return new AtomicInteger(); + } + + @Benchmark + public AtomicInteger gaugeReRegistrationWithoutBuilder() { + return registry.gauge("test.gauge", new AtomicInteger()); + } + + @Benchmark + public Gauge gaugeReRegistrationWithBuilder() { + return Gauge.builder("test.gauge", new AtomicInteger(), AtomicInteger::doubleValue).register(registry); + } + + public static void main(String[] args) throws RunnerException { + new Runner(new OptionsBuilder().include(GaugeBenchmark.class.getSimpleName()).build()).run(); + } + +} diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/KeyValuesMergeBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/KeyValuesMergeBenchmark.java index 29a31cb85e..bc443d8aea 100644 --- a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/KeyValuesMergeBenchmark.java +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/KeyValuesMergeBenchmark.java @@ -34,7 +34,7 @@ public class KeyValuesMergeBenchmark { static final KeyValues left = KeyValues.of("key", "value", "key2", "value2", "key6", "value6", "key7", "value7", "key8", "value8", "keyA", "valueA", "keyC", "valueC", "keyE", "valueE", "keyF", "valueF", "keyG", "valueG", - "keyG", "valueG", "keyG", "valueG", "keyH", "valueH"); + "keyH", "valueH"); static final KeyValues right = KeyValues.of("key", "value", "key1", "value1", "key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5", "keyA", "valueA", "keyB", "valueB", "keyD", "valueD"); diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/MeterRemovalBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/MeterRemovalBenchmark.java new file mode 100644 index 0000000000..1f046bd926 --- /dev/null +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/MeterRemovalBenchmark.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ +package io.micrometer.benchmark.core; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.profile.GCProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Fork(1) +@Warmup(iterations = 2) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class MeterRemovalBenchmark { + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder().include(MeterRemovalBenchmark.class.getSimpleName()) + .addProfiler(GCProfiler.class) + .build(); + + new Runner(opt).run(); + } + + @Param({ "10000", "100000" }) + int meterCount; + + MeterRegistry registry = new SimpleMeterRegistry(); + + @Setup + public void setup() { + for (int i = 0; i < meterCount; i++) { + registry.counter("counter", "key", String.valueOf(i)); + } + } + + /** + * Benchmark the time to remove one meter from a registry with many meters. This uses + * the single shot mode because otherwise it would measure the time to remove a meter + * not in the registry after the first call, and that is not what we want to measure. + */ + @Benchmark + @Warmup(iterations = 100) + @Measurement(iterations = 500) + @BenchmarkMode(Mode.SingleShotTime) + public Meter remove() { + return registry.remove(registry.counter("counter", "key", String.valueOf(meterCount / 2))); + } + +} diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/ObservationKeyValuesBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/ObservationKeyValuesBenchmark.java new file mode 100644 index 0000000000..2eaad8a9a9 --- /dev/null +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/ObservationKeyValuesBenchmark.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ + +package io.micrometer.benchmark.core; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Threads(4) +public class ObservationKeyValuesBenchmark { + + private static final KeyValues KEY_VALUES = KeyValues.of("key1", "value1", "key2", "value2", "key3", "value3", + "key4", "value4", "key5", "value5"); + + private final ObservationRegistry registry = ObservationRegistry.create(); + + private final Observation.Context context = new TestContext(); + + private final Observation observation = Observation.createNotStarted("jmh", () -> context, registry); + + @Benchmark + @Group("contended") + @GroupThreads(1) + public Observation contendedWrite() { + return write(); + } + + @Benchmark + @Group("contended") + @GroupThreads(1) + public KeyValues contendedRead() { + return read(); + } + + @Benchmark + @Threads(1) + public Observation uncontendedWrite() { + return write(); + } + + @Benchmark + @Threads(1) + public KeyValues uncontendedRead() { + return read(); + } + + private Observation write() { + return observation.lowCardinalityKeyValues(KEY_VALUES); + } + + private KeyValues read() { + return observation.getContext().getLowCardinalityKeyValues(); + } + + public static void main(String[] args) throws RunnerException { + Options options = new OptionsBuilder().include(ObservationKeyValuesBenchmark.class.getSimpleName()) + .warmupIterations(3) + .measurementIterations(5) + .mode(Mode.SampleTime) + .forks(1) + .build(); + + new Runner(options).run(); + } + + static class TestContext extends Observation.Context { + + } + +} diff --git a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java index 3e0774c522..3d1c1f53c8 100644 --- a/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java +++ b/benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/TagsMergeBenchmark.java @@ -33,8 +33,8 @@ public class TagsMergeBenchmark { static final Tags left = Tags.of("key", "value", "key2", "value2", "key6", "value6", "key7", "value7", "key8", - "value8", "keyA", "valueA", "keyC", "valueC", "keyE", "valueE", "keyF", "valueF", "keyG", "valueG", "keyG", - "valueG", "keyG", "valueG", "keyH", "valueH"); + "value8", "keyA", "valueA", "keyC", "valueC", "keyE", "valueE", "keyF", "valueF", "keyG", "valueG", "keyH", + "valueH"); static final Tags right = Tags.of("key", "value", "key1", "value1", "key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5", "keyA", "valueA", "keyB", "valueB", "keyD", "valueD"); diff --git a/benchmarks/benchmarks-core/src/jmh/resources/logback.xml b/benchmarks/benchmarks-core/src/jmh/resources/logback.xml new file mode 100644 index 0000000000..f2b1939e72 --- /dev/null +++ b/benchmarks/benchmarks-core/src/jmh/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/build.gradle b/build.gradle index 2792a2f248..6fe5d7dc15 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,12 @@ buildscript { classpath libs.plugin.japicmp classpath libs.plugin.downloadTask classpath libs.plugin.spotless - classpath libs.plugin.bnd + + if (javaLanguageVersion.asInt() < 17) { + classpath libs.plugin.bnd + } else { + classpath libs.plugin.bndForJava17 + } constraints { classpath(libs.asmForPlugins) { @@ -281,7 +286,7 @@ subprojects { } plugins.withId('org.jetbrains.kotlin.jvm') { - // We disble the kotlinSourcesJar task since it conflicts with the sourcesJar task of the Java plugin + // We disable the kotlinSourcesJar task since it conflicts with the sourcesJar task of the Java plugin // See: https://github.com/micrometer-metrics/micrometer/issues/5151 // See: https://youtrack.jetbrains.com/issue/KT-54207/Kotlin-has-two-sources-tasks-kotlinSourcesJar-and-sourcesJar-that-archives-sources-to-the-same-artifact kotlinSourcesJar.enabled = false @@ -343,7 +348,7 @@ subprojects { check.dependsOn("testModules") - if (!(project.name in ['micrometer-test-aspectj-ltw', 'micrometer-test-aspectj-ctw', 'micrometer-java21'])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp + if (!(project.name in [])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp apply plugin: 'me.champeau.gradle.japicmp' apply plugin: 'de.undercouch.download' @@ -386,13 +391,13 @@ subprojects { ignoreMissingClasses = true includeSynthetic = true - // TODO remove MetricsDSLContext from excludes in 1.14 when comparing against 1.13 - classExcludes = [ 'io.micrometer.core.instrument.binder.db.MetricsDSLContext' ] + classExcludes = [] compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ] packageExcludes = ['io.micrometer.shaded.*', 'io.micrometer.statsd.internal'] - fieldExcludes = ['io.micrometer.core.instrument.binder.jdk.DefaultHttpClientObservationConvention#INSTANCE'] + fieldExcludes = [] + methodExcludes = ['io.micrometer.registry.otlp.AggregationTemporality#toOtlpAggregationTemporality(io.micrometer.registry.otlp.AggregationTemporality)'] onlyIf { compatibleVersion != 'SKIP' } } @@ -433,7 +438,7 @@ nexusPublishing { } wrapper { - gradleVersion = '8.10.2' + gradleVersion = '8.12' } defaultTasks 'build' diff --git a/concurrency-tests/build.gradle b/concurrency-tests/build.gradle index da81f9abca..4b8e58b883 100644 --- a/concurrency-tests/build.gradle +++ b/concurrency-tests/build.gradle @@ -3,15 +3,19 @@ plugins { } dependencies { + // Comment out the local project references and reference an old version to compare jcstress results across versions + // Make sure to use a consistent version for all micrometer dependencies implementation project(":micrometer-core") -// implementation("io.micrometer:micrometer-core:1.12.4") - implementation project(":micrometer-test") +// implementation("io.micrometer:micrometer-core:1.14.1") implementation project(":micrometer-registry-prometheus") +// implementation("io.micrometer:micrometer-registry-prometheus:1.14.1") + implementation project(":micrometer-registry-otlp") + runtimeOnly(libs.logbackLatest) } jcstress { - libs.jcstressCore + jcstressDependency libs.jcstressCore.get().toString() // This affects how long and thorough testing will be // In order of increasing stress: sanity, quick, default, tough, stress diff --git a/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/Base2ExponentialHistogramConcurrencyTests.java b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/Base2ExponentialHistogramConcurrencyTests.java new file mode 100644 index 0000000000..7c690fb15a --- /dev/null +++ b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/Base2ExponentialHistogramConcurrencyTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ +package io.micrometer.concurrencytests; + +import static org.openjdk.jcstress.annotations.Expect.*; +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; +import static org.openjdk.jcstress.annotations.Expect.FORBIDDEN; + +import org.openjdk.jcstress.annotations.*; +import org.openjdk.jcstress.infra.results.*; +import io.micrometer.registry.otlp.internal.Base2ExponentialHistogram; +import io.micrometer.registry.otlp.internal.CumulativeBase2ExponentialHistogram; + +public class Base2ExponentialHistogramConcurrencyTests { + + @JCStressTest + @Outcome(id = "null, 5, 2", expect = ACCEPTABLE, desc = "Read after all writes") + @Outcome(id = "null, 20, 0", expect = ACCEPTABLE, desc = "Read before write") + @Outcome(id = { "null, 20, 1" }, expect = ACCEPTABLE, desc = "Reading after single " + "write") + @Outcome( + id = { "class java.lang.ArrayIndexOutOfBoundsException, 20, 0", + "class java.lang.ArrayIndexOutOfBoundsException, 20, 1" }, + expect = FORBIDDEN, desc = "Exception in recording thread") + @Outcome( + id = "class java.lang.ArrayIndexOutOfBoundsException, class java.lang.ArrayIndexOutOfBoundsException, null", + expect = FORBIDDEN, desc = "Exception in both reading and writing threads") + @Outcome(id = "null, class java.lang.ArrayIndexOutOfBoundsException, null", expect = FORBIDDEN, + desc = "Exception in reading thread") + @Outcome(expect = UNKNOWN) + @State + public static class RescalingAndConcurrentReading { + + Base2ExponentialHistogram exponentialHistogram = new CumulativeBase2ExponentialHistogram(20, 40, 0, null); + + @Actor + public void actor1(LLL_Result r) { + try { + exponentialHistogram.recordDouble(2); + } + catch (Exception e) { + r.r1 = e.getClass(); + } + } + + @Actor + public void actor2(LLL_Result r) { + try { + exponentialHistogram.recordDouble(4); + } + catch (Exception e) { + r.r1 = e.getClass(); + } + } + + @Actor + public void actor3(LLL_Result r) { + try { + exponentialHistogram.takeSnapshot(2, 6, 4); + r.r2 = exponentialHistogram.getLatestExponentialHistogramSnapshot().scale(); + r.r3 = exponentialHistogram.getLatestExponentialHistogramSnapshot() + .positive() + .bucketCounts() + .stream() + .mapToLong(Long::longValue) + .sum(); + } + catch (Exception e) { + r.r2 = e.getClass(); + } + } + + } + +} diff --git a/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/MeterRegistryConcurrencyTest.java b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/MeterRegistryConcurrencyTest.java index 39bdde41a7..320c6b12db 100644 --- a/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/MeterRegistryConcurrencyTest.java +++ b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/MeterRegistryConcurrencyTest.java @@ -15,7 +15,6 @@ */ package io.micrometer.concurrencytests; -import io.micrometer.core.Issue; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -154,7 +153,7 @@ public void arbiter(LL_Result r) { * iteration could happen at the same time a new meter is being registered, thus added * to the preFilterIdToMeterMap, modifying it while iterating over its KeySet. */ - @Issue("gh-5489") + // @Issue("gh-5489") @JCStressTest @Outcome(id = "OK", expect = Expect.ACCEPTABLE, desc = "No exception") @Outcome(expect = Expect.FORBIDDEN, desc = "Exception thrown") diff --git a/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/ObservationContextConcurrencyTest.java b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/ObservationContextConcurrencyTest.java new file mode 100644 index 0000000000..c0a3dd638c --- /dev/null +++ b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/ObservationContextConcurrencyTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ + +package io.micrometer.concurrencytests; + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LL_Result; + +import java.util.UUID; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; +import static org.openjdk.jcstress.annotations.Expect.FORBIDDEN; + +public class ObservationContextConcurrencyTest { + + @JCStressTest + @State + @Outcome(id = "No exception, No exception", expect = ACCEPTABLE) + @Outcome(expect = FORBIDDEN) + public static class ConsistentKeyValuesGetAdd { + + private final Observation.Context context = new TestContext(); + + private final String uuid = UUID.randomUUID().toString(); + + @Actor + public void get(LL_Result r) { + try { + context.getHighCardinalityKeyValues(); + r.r1 = "No exception"; + } + catch (Exception e) { + r.r1 = e.getClass().getSimpleName(); + } + } + + @Actor + public void add(LL_Result r) { + try { + context.addHighCardinalityKeyValue(KeyValue.of("uuid", uuid)); + r.r2 = "No exception"; + } + catch (Exception e) { + r.r2 = e.getClass().getSimpleName(); + } + } + + } + + @JCStressTest + @State + @Outcome(id = "No exception, No exception", expect = ACCEPTABLE) + @Outcome(expect = FORBIDDEN) + public static class ConsistentKeyValuesGetRemove { + + private final Observation.Context context = new TestContext(); + + public ConsistentKeyValuesGetRemove() { + context.addLowCardinalityKeyValue(KeyValue.of("keep", "donotremoveme")); + context.addLowCardinalityKeyValue(KeyValue.of("remove", "removeme")); + } + + @Actor + public void get(LL_Result r) { + try { + context.getAllKeyValues(); + r.r1 = "No exception"; + } + catch (Exception e) { + r.r1 = e.getClass().getSimpleName(); + } + } + + @Actor + public void remove(LL_Result r) { + try { + context.removeLowCardinalityKeyValue("remove"); + r.r2 = "No exception"; + } + catch (Exception e) { + r.r2 = e.getClass().getSimpleName(); + } + } + + } + + static class TestContext extends Observation.Context { + + } + +} diff --git a/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/PrometheusMeterRegistryConcurrencyTest.java b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/PrometheusMeterRegistryConcurrencyTest.java index bfe4c5d255..a315e2a8a7 100644 --- a/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/PrometheusMeterRegistryConcurrencyTest.java +++ b/concurrency-tests/src/jcstress/java/io/micrometer/concurrencytests/PrometheusMeterRegistryConcurrencyTest.java @@ -15,7 +15,6 @@ */ package io.micrometer.concurrencytests; -import io.micrometer.core.Issue; import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.LongTaskTimer; import io.micrometer.core.instrument.LongTaskTimer.Sample; @@ -38,7 +37,7 @@ */ public class PrometheusMeterRegistryConcurrencyTest { - @Issue("#5193") + // @Issue("#5193") @JCStressTest @State @Outcome(id = "true", expect = ACCEPTABLE, desc = "Successful scrape") @@ -67,7 +66,7 @@ public void scrape(Z_Result r) { } - @Issue("#5193") + // @Issue("#5193") @JCStressTest @State @Outcome(id = "true", expect = ACCEPTABLE, desc = "Successful scrape") @@ -96,7 +95,7 @@ public void scrape(Z_Result r) { } - @Issue("#5193") + // @Issue("#5193") @JCStressTest @State @Outcome(id = "true", expect = ACCEPTABLE, desc = "Successful scrape") diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 6fab4c2039..4355e81191 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -46,6 +46,22 @@ + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index f0e925571f..af17221548 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -32,7 +32,7 @@ ** xref:implementations/new-relic.adoc[New Relic] ** xref:implementations/otlp.adoc[OpenTelemetry Protocol (OTLP)] ** xref:implementations/prometheus.adoc[Prometheus] -** xref:implementations/signalFx.adoc[SignalFx] +** xref:implementations/signalFx.adoc[SignalFx] (deprecated) ** xref:implementations/stackdriver.adoc[Stackdriver] ** xref:implementations/statsD.adoc[statsD] ** xref:implementations/wavefront.adoc[Wavefront] diff --git a/docs/modules/ROOT/pages/concepts/counters.adoc b/docs/modules/ROOT/pages/concepts/counters.adoc index 567fdfc3d4..1a552f4384 100644 --- a/docs/modules/ROOT/pages/concepts/counters.adoc +++ b/docs/modules/ROOT/pages/concepts/counters.adoc @@ -45,6 +45,85 @@ Counter counter = Counter .register(registry); ---- + +== The `@Counted` Annotation + +The `micrometer-core` module contains a `@Counted` annotation that frameworks can use to add counting support to either specific types of methods such as those serving web request endpoints or, more generally, to all methods. + +Also, an incubating AspectJ aspect is included in `micrometer-core`. You can use it in your application either through compile/load time AspectJ weaving or through framework facilities that interpret AspectJ aspects and proxy targeted methods in some other way, such as Spring AOP. Here is a sample Spring AOP configuration: + +[source,java] +---- +@Configuration +public class CountedConfiguration { + @Bean + public CountedAspect countedAspect(MeterRegistry registry) { + return new CountedAspect(registry); + } +} +---- + +Applying `CountedAspect` makes `@Counted` usable on any arbitrary method in an AspectJ proxied instance, as the following example shows: + +[source,java] +---- +@Service +public class ExampleService { + + @Counted + public void sync() { + // @Counted will record the number of executions of this method + ... + } + + @Async + @Counted + public CompletableFuture async() { + // @Counted will record the number of executions of this method + return CompletableFuture.supplyAsync(...); + } + +} +---- + +=== @MeterTag on Method Parameters + +To support using the `@MeterTag` annotation on method parameters, you need to configure the `@CountedAspect` to add the `CountedMeterTagAnnotationHandler`. + +[source,java,subs=+attributes] +----- +include::{include-java}/metrics/CountedAspectTest.java[tags=resolvers,indent=0] + +include::{include-java}/metrics/CountedAspectTest.java[tags=meter_tag_annotation_handler,indent=0] +----- + +Let's assume that we have the following interface. + +[source,java,subs=+attributes] +----- +include::{include-java}/metrics/CountedAspectTest.java[tags=interface,indent=0] +----- + +When its implementations would be called with different arguments (remember that the implementation needs to be annotated with `@Counted` annotation too), the following counters would be created: + +[source,java,subs=+attributes] +----- +// Example for returning on the parameter +include::{include-java}/metrics/CountedAspectTest.java[tags=example_value_to_string,indent=0] + +// Example for calling the provided on the parameter +include::{include-java}/metrics/CountedAspectTest.java[tags=example_value_resolver,indent=0] + +// Example for calling the provided +include::{include-java}/metrics/CountedAspectTest.java[tags=example_value_spel,indent=0] + +// Example for using multiple @MeterTag annotations on the same parameter +// @MeterTags({ @MeterTag(...), @MeterTag(...) }) can be also used +include::{include-java}/metrics/CountedAspectTest.java[tags=example_multi_annotations,indent=0] +----- + +NOTE: `CountedAspect` doesn't support meta-annotations with `@Counted`. + == Function-tracking Counters Micrometer also provides a more infrequently used counter pattern that tracks a monotonically increasing function (a function that stays the same or increases over time but never decreases). Some monitoring systems, such as Prometheus, push cumulative values for counters to the backend, but others publish the rate at which a counter is incrementing over the push interval. By employing this pattern, you let the Micrometer implementation for your monitoring system choose whether to rate-normalize the counter, and your counter remains portable across different types of monitoring systems. diff --git a/docs/modules/ROOT/pages/concepts/gauges.adoc b/docs/modules/ROOT/pages/concepts/gauges.adoc index 48010a6d93..2970dc3b8a 100644 --- a/docs/modules/ROOT/pages/concepts/gauges.adoc +++ b/docs/modules/ROOT/pages/concepts/gauges.adoc @@ -42,7 +42,7 @@ Note that, in this form, unlike other meter types, you do not get a reference to This pattern should be less common than the `DoubleFunction` form. Remember that frequent setting of the observed `Number` results in a lot of intermediate values that never get published. Only the _instantaneous value_ of the gauge at publish time is ever sent to the monitoring system. -WARNING: Attempting to construct a gauge with a primitive number or one of its `java.lang` object forms is always incorrect. These numbers are immutable. Thus, the gauge cannot ever be changed. Attempting to "`re-register`" the gauge with a different number does not work, as the registry maintains only one meter for each unique combination of name and tags. +WARNING: Attempting to construct a gauge with a primitive number or one of its `java.lang` object forms is always incorrect. These numbers are immutable. Thus, the gauge cannot ever be changed. Attempting to "re-register" the gauge with a different number does not work, as the registry maintains only one meter for each unique combination of name and tags. "Re-registering" a gauge can happen indirectly for example as the result of a `MeterFilter` modifying the name and/or the tags of two different gauges so that they will be the same after the filter is applied. == Gauge Fluent Builder diff --git a/docs/modules/ROOT/pages/concepts/meter-filters.adoc b/docs/modules/ROOT/pages/concepts/meter-filters.adoc index 5f07d24749..e365e0f56a 100644 --- a/docs/modules/ROOT/pages/concepts/meter-filters.adoc +++ b/docs/modules/ROOT/pages/concepts/meter-filters.adoc @@ -98,6 +98,17 @@ new MeterFilter() { This filter adds a name prefix and an additional tag conditionally to meters starting with a name of `test`. +[IMPORTANT] +.`MeterFilter` implementations of `map` should be "static" +==== +The `id` parameter is the only dynamic input that changes over the lifecycle of the `MeterFilter` on which they should depend. +Use cases where dynamic behavior is desired, such as defining tags based on the context of a request etc., should be implemented in the instrumentation itself rather than in a `MeterFilter`. +For example, see `MongoMetricsCommandListener` and the `MongoCommandTagsProvider` it takes in a constructor argument as well as the default implementation `DefaultMongoCommandTagsProvider`. +See also xref:../observation/components.adoc#micrometer-observation-predicates-filters[ObservationFilter] which allows dynamic implementations. +==== + +=== Convenience Methods + `MeterFilter` provides convenience builders for many common transformation cases: * `commonTags(Iterable)`: Adds a set of tags to all metrics. Adding common tags for application name, host, region, and others is a highly recommended practice. diff --git a/docs/modules/ROOT/pages/concepts/meter-provider.adoc b/docs/modules/ROOT/pages/concepts/meter-provider.adoc index a5dfc55f01..9a7ac7068f 100644 --- a/docs/modules/ROOT/pages/concepts/meter-provider.adoc +++ b/docs/modules/ROOT/pages/concepts/meter-provider.adoc @@ -44,6 +44,6 @@ Result result = job.execute(); sample.stop(timerProvider.withTags("status", result.status())); <2> ---- <1> Definition of the `MeterProvider` for `Timer` with all the "static" fields necessary. Please note the `withRegistry` method call. -<2> Definition of the dynamic tags. Note that only those tags are defined here that are dynamic and everying else is defined where the `MeterProvider` is created. The `withTags` method returns a `Timer` that is created using the tags defined in `withTags` plus everything else that is defined by the `MeterProvider`. +<2> Definition of the dynamic tags. Note that only those tags are defined here that are dynamic and everything else is defined where the `MeterProvider` is created. The `withTags` method returns a `Timer` that is created using the tags defined in `withTags` plus everything else that is defined by the `MeterProvider`. This and the previous example produce the same output, the only difference is the amount of boilerplate in your code and the amount of builder objects created in the heap. diff --git a/docs/modules/ROOT/pages/concepts/timers.adoc b/docs/modules/ROOT/pages/concepts/timers.adoc index 697083df3b..d5efd25519 100644 --- a/docs/modules/ROOT/pages/concepts/timers.adoc +++ b/docs/modules/ROOT/pages/concepts/timers.adoc @@ -73,8 +73,6 @@ Note how we do not decide the timer to which to accumulate the sample until it i The `micrometer-core` module contains a `@Timed` annotation that frameworks can use to add timing support to either specific types of methods such as those serving web request endpoints or, more generally, to all methods. -WARNING: Micrometer's Spring Boot configuration does _not_ recognize `@Timed` on arbitrary methods. - Also, an incubating AspectJ aspect is included in `micrometer-core`. You can use it in your application either through compile/load time AspectJ weaving or through framework facilities that interpret AspectJ aspects and proxy targeted methods in some other way, such as Spring AOP. Here is a sample Spring AOP configuration: [source,java] @@ -114,6 +112,8 @@ public class ExampleService { } ---- +NOTE: `TimedAspect` doesn't support meta-annotations with `@Timed`. + === @MeterTag on Method Parameters To support using the `@MeterTag` annotation on method parameters, you need to configure the `@TimedAspect` to add the `MeterTagAnnotationHandler`. @@ -144,6 +144,10 @@ include::{include-java}/metrics/TimedAspectTest.java[tags=example_value_resolver // Example for calling the provided include::{include-java}/metrics/TimedAspectTest.java[tags=example_value_spel,indent=0] + +// Example for using multiple @MeterTag annotations on the same parameter +// @MeterTags({ @MeterTag(...), @MeterTag(...) }) can be also used +include::{include-java}/metrics/TimedAspectTest.java[tags=example_multi_annotations,indent=0] ----- == Function-tracking Timers diff --git a/docs/modules/ROOT/pages/contextpropagation.adoc b/docs/modules/ROOT/pages/contextpropagation.adoc deleted file mode 100644 index 2f592471f1..0000000000 --- a/docs/modules/ROOT/pages/contextpropagation.adoc +++ /dev/null @@ -1,6 +0,0 @@ -[[context-propagation-support]] -= Context Propagation support - -https://github.com/micrometer-metrics/context-propagation[Context Propagation] is a library that assists with context propagation across different types of context -mechanisms such as `ThreadLocal`, Reactor https://projectreactor.io/docs/core/release/reference/#context[Context] -and others. diff --git a/docs/modules/ROOT/pages/implementations/_install.adoc b/docs/modules/ROOT/pages/implementations/_install.adoc index b6a8a92579..acd27ac936 100644 --- a/docs/modules/ROOT/pages/implementations/_install.adoc +++ b/docs/modules/ROOT/pages/implementations/_install.adoc @@ -1,20 +1,29 @@ [id=installing-micrometer-registry-{system}] == Installing micrometer-registry-{system} -For Gradle, add the following implementation: +It is recommended to use the BOM provided by Micrometer (or your framework if any), you can see how to configure it xref:../installing.adoc[here]. The examples below assume you are using a BOM. + +=== Gradle + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,groovy,subs=+attributes] ---- -implementation 'io.micrometer:micrometer-registry-{system}:latest.release' +implementation 'io.micrometer:micrometer-registry-{system}' ---- -For Maven, add the following dependency: +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +=== Maven + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,xml,subs=+attributes] ---- io.micrometer micrometer-registry-{system} - ${micrometer.version} ---- + +NOTE: The version is not needed for this dependency since it is defined by the BOM. diff --git a/docs/modules/ROOT/pages/implementations/datadog.adoc b/docs/modules/ROOT/pages/implementations/datadog.adoc index a7207bf0ee..be4554cfd6 100644 --- a/docs/modules/ROOT/pages/implementations/datadog.adoc +++ b/docs/modules/ROOT/pages/implementations/datadog.adoc @@ -11,26 +11,35 @@ If you can choose between the two, the API approach is far more efficient. NOTE: If you encounter a rate limit problem with the Datadog API approach, try the DogStatsD approach or one of https://docs.datadoghq.com/metrics/guide/micrometer/[alternatives that are described in the Datadog documentation]. +It is recommended to use the BOM provided by Micrometer (or your framework if any), you can see how to configure it xref:../installing.adoc[here]. The examples below assume you are using a BOM. + === Direct to Datadog API Approach -For Gradle, add the following implementation: +==== Gradle + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,groovy] ---- -implementation 'io.micrometer:micrometer-registry-datadog:latest.release' +implementation 'io.micrometer:micrometer-registry-datadog' ---- -For Maven, add the following dependency: +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +==== Maven + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,xml] ---- io.micrometer micrometer-registry-datadog - ${micrometer.version} ---- +NOTE: The version is not needed for this dependency since it is defined by the BOM. + Metrics are rate-aggregated and pushed to `datadoghq` on a periodic interval. Rate aggregation performed by the registry yields datasets that are similar to those produced by `dogstatsd`. [source, java] @@ -76,24 +85,31 @@ For example, for the `US5` site, the correct API endpoint is `https://api.us5.da === Through DogStatsD Approach -For Gradle, add the following implementation: +==== Gradle + +After the BOM is xref:../installing.adoc[configured], add the following dependency: -[source,groovy,subs=+attributes] +[source,groovy] ---- -implementation 'io.micrometer:micrometer-registry-statsd:latest.release' +implementation 'io.micrometer:micrometer-registry-statsd' ---- -For Maven, add the following dependency: +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +==== Maven + +After the BOM is xref:../installing.adoc[configured]d, add the following dependency: -[source,xml,subs=+attributes] +[source,xml] ---- io.micrometer micrometer-registry-statsd - ${micrometer.version} ---- +NOTE: The version is not needed for this dependency since it is defined by the BOM. + Metrics are immediately shipped to DogStatsD using Datadog's flavor of the StatsD line protocol. `java-dogstatsd-client` is _not_ needed on the classpath for this to work, as Micrometer uses its own implementation. [source,java] diff --git a/docs/modules/ROOT/pages/implementations/influx.adoc b/docs/modules/ROOT/pages/implementations/influx.adoc index 84248002fc..d47267e939 100644 --- a/docs/modules/ROOT/pages/implementations/influx.adoc +++ b/docs/modules/ROOT/pages/implementations/influx.adoc @@ -6,30 +6,39 @@ The InfluxData suite of tools supports real-time stream processing and storage o The InfluxMeterRegistry supports the 1.x InfluxDB API as well as the v2 API. -== Configuring +== Installation and Configuration Micrometer supports shipping metrics to InfluxDB directly or through Telegraf through the StatsD registry. +It is recommended to use the BOM provided by Micrometer (or your framework if any), you can see how to configure it xref:../installing.adoc[here]. The examples below assume you are using a BOM. + === Direct to InfluxDB -The following example adds the required library in Gradle: +==== Gradle + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,groovy] ---- -implementation 'io.micrometer:micrometer-registry-influx:latest.release' +implementation 'io.micrometer:micrometer-registry-influx' ---- -The following example adds the required library in Maven: +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +==== Maven + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,xml] ---- io.micrometer micrometer-registry-influx - ${micrometer.version} ---- +NOTE: The version is not needed for this dependency since it is defined by the BOM. + Metrics are rate-aggregated and pushed to InfluxDB on a periodic interval. Rate aggregation performed by the registry yields datasets that are quite similar to those produced by Telegraf. The following example configures a meter registry for InfluxDB: .InfluxDB 1.x configuration example @@ -113,24 +122,31 @@ management.metrics.export.influx: Telegraf is a StatsD agent that expects a modified flavor of the StatsD line protocol. -The following listing adds the relevant library in Gradle: +==== Gradle + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,groovy] ---- -implementation 'io.micrometer:micrometer-registry-statsd:latest.release' +implementation 'io.micrometer:micrometer-registry-statsd' ---- -The following listing adds the relevant library in Maven: +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +==== Maven + +After the BOM is xref:../installing.adoc[configured], add the following dependency: [source,xml] ---- io.micrometer micrometer-registry-statsd - ${micrometer.version} ---- +NOTE: The version is not needed for this dependency since it is defined by the BOM. + Metrics are shipped immediately over UDP to Telegraf by using Telegraf's flavor of the StatsD line protocol: [source,java] diff --git a/docs/modules/ROOT/pages/implementations/otlp.adoc b/docs/modules/ROOT/pages/implementations/otlp.adoc index 6e912e4f16..accb014f1f 100644 --- a/docs/modules/ROOT/pages/implementations/otlp.adoc +++ b/docs/modules/ROOT/pages/implementations/otlp.adoc @@ -41,7 +41,7 @@ management: ---- 1. `url` - The URL to which data is reported. Environment variables `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` and `OTEL_EXPORTER_OTLP_ENDPOINT` are also supported in the default implementation. If a value is not provided, it defaults to `http://localhost:4318/v1/metrics` -2. `batchSize` - number of `Meter` to include in a single payload sent to the backend. The default is 10,000. +2. `batchSize` - number of ``Meter``s to include in a single payload sent to the backend. The default is 10,000. 3. `aggregationTemporality` - https://opentelemetry.io/docs/specs/otel/metrics/data-model/#temporality[Aggregation temporality, window=_blank] determines how the additive quantities are expressed, in relation to time. The environment variable `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` is supported by the default implementation. The supported values are `cumulative` or `delta`. Defaults to `cumulative`. 4. `headers` - Additional headers to send with exported metrics. This can be used for authorization headers. By default, headers are loaded from the config. If that is not set, they can be taken from the environment variables `OTEL_EXPORTER_OTLP_HEADERS` and `OTEL_EXPORTER_OTLP_METRICS_HEADERS`. If a header is set in both the environmental variables, the header in the latter overrides the former. 5. `step` - the interval at which metrics will be published. The environment variable `OTEL_METRIC_EXPORT_INTERVAL` is supported by the default implementation. If a value is not provided, defaults to 1 minute. diff --git a/docs/modules/ROOT/pages/implementations/prometheus.adoc b/docs/modules/ROOT/pages/implementations/prometheus.adoc index 4534e5846c..918902487e 100644 --- a/docs/modules/ROOT/pages/implementations/prometheus.adoc +++ b/docs/modules/ROOT/pages/implementations/prometheus.adoc @@ -85,14 +85,14 @@ By default, the `PrometheusMeterRegistry` `scrape()` method returns the https:// The https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md[OpenMetrics] format can also be produced. To specify the format to be returned, you can pass a content type to the `scrape` method. -For example, to get the OpenMetrics 1.0.0 format scrape, you could use the Prometheus Java client constant for it, as follows in case of the "new" client (`micrometer-registry-prometheus`): +For example, to get the OpenMetrics 1.0.0 format scrape, you could use the Content-Type for it, as follows in case of the "new" client (`micrometer-registry-prometheus`): [source,java] ---- String openMetricsScrape = registry.scrape("application/openmetrics-text"); ---- -if you use the "legacy" client (`micrometer-registry-prometheus-simpleclient`): +If you use the "legacy" client (`micrometer-registry-prometheus-simpleclient`), you could use the Prometheus Java client constant for it: [source,java] ---- @@ -137,7 +137,7 @@ PrometheusMeterRegistry registry = new PrometheusMeterRegistry(config, new Prome <1> You can reuse the "default" properties defined in `PrometheusConfig`. <2> You can set any property from any property source. -Micrometer passes these properties to the Exporters and the Examplar Sampler of the Prometheus client so you can use the https://prometheus.github.io/client_java/config/config/#exporter-properties[exporter]-, and the https://prometheus.github.io/client_java/config/config/#exemplar-properties[exemplar] properties of the Prometheus Client. +Micrometer passes these properties to the Exporters and the Exemplar Sampler of the Prometheus client, so you can use the https://prometheus.github.io/client_java/config/config/#exporter-properties[exporter], and the https://prometheus.github.io/client_java/config/config/#exemplar-properties[exemplar] properties of the Prometheus Client. == Graphing @@ -146,34 +146,10 @@ See the https://prometheus.io/docs/querying/basics[Prometheus docs] for a far mo === Grafana Dashboard -A publicly available Grafana dashboard for Micrometer-sourced JVM and Tomcat metrics is available https://grafana.com/grafana/dashboards/4701-jvm-micrometer/[here]. +There are many third-party Grafana dashboards publicly available on https://grafana.com/grafana/dashboards/?search=micrometer[GrafanaHub]. +See an example https://grafana.com/grafana/dashboards/4701-jvm-micrometer/[here]. -image::implementations/prometheus-dashboard.png[Grafana dashboard for JVM and Tomcat binders] - -The dashboard features: - -* JVM memory -* Process memory (provided by https://github.com/mweirauch/micrometer-jvm-extras[micrometer-jvm-extras]) -* CPU-Usage, Load, Threads, File Descriptors, and Log Events -* JVM Memory Pools (Heap, Non-Heap) -* Garbage Collection -* Classloading -* Direct/Mapped buffer sizes - -Instead of using the `job` tag to distinguish different applications, this dashboard makes use of a common tag called `application`, which is applied to every metric. -You can apply the common tag, as follows: - -[source,java] ----- -registry.config().commonTags("application", "MYAPPNAME"); ----- - -In Spring Boot applications, you can use the https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.metrics.customizing.common-tags[property support for common tags]: - -[source,properties] ----- -management.metrics.tags.application=MYAPPNAME ----- +NOTE: The dashboards are maintained by the community in their external GitHub repositories, so if you have an issue, it should be created in their respective GitHub repository. === Counters @@ -217,10 +193,30 @@ image::implementations/prometheus-long-task-timer.png[Grafana-rendered Prometheu == Limitation on same name with different set of tag keys The `PrometheusMeterRegistry` doesn't allow to create meters having the same name with a different set of tag keys, so you should guarantee that meters having the same name have the same set of tag keys. -Otherwise, subsequent meters having the same name with a different set of tag keys will not be registered silently by default. -You can change the default behavior by registering a meter registration failed listener. -For example, you can register a meter registration failed listener that throws an exception as follows: +Otherwise, subsequent meters having the same name with a different set of tag keys will not be registered. +This means that you should not do things like: + +[source,java] +---- +// Please don't do this +registry.counter("test", "first", "1").increment(); +registry.counter("test", "second", "2").increment(); +---- + +This will result in the following warning and the second `Meter` will not be registered: +[source] +---- +WARNING: The meter (MeterId{name='test', tags=[tag(second=2)]}) registration has failed: Prometheus requires that all meters with the same name have the same set of tag keys. There is already an existing meter named 'test' containing tag keys [first]. The meter you are attempting to register has keys [second]. Note that subsequent logs will be logged at debug level. +---- + +Instead you can do something like this: +[source,java] +---- +registry.counter("test", "first", "1", "second", "none").increment(); +registry.counter("test", "first", "none", "second", "2").increment(); +---- +In addition to the warning, you can register a meter registration failed listener to handle the failure: [source,java] ---- registry.config().onMeterRegistrationFailed((id, reason) -> { @@ -253,7 +249,7 @@ PrometheusMeterRegistry registry = new PrometheusMeterRegistry( registry.counter("test").increment(); System.out.println(registry.scrape("application/openmetrics-text")); ---- -<1> You need to implement `MySpanContext` (`class MySpanContext implements SpanContext { ... }`) or use an implementation that already exists. +<1> You need to implement `SpanContext` (`class MySpanContext implements SpanContext { ... }`) or use an implementation that already exists. But if you use the "legacy" client (`micrometer-registry-prometheus-simpleclient`): [source,java] @@ -267,9 +263,9 @@ PrometheusMeterRegistry registry = new PrometheusMeterRegistry( registry.counter("test").increment(); System.out.println(registry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100)); ---- -<1> You need to implement `MySpanContextSupplier` (`class MySpanContextSupplier implements SpanContextSupplier { ... }`) or use an implementation that already exists. +<1> You need to implement `SpanContextSupplier` (`class MySpanContextSupplier implements SpanContextSupplier { ... }`) or use an implementation that already exists. -If your configuration is correct, you should get something like this, the `# {span_id="321",trace_id="123"} ...` section is the Exempar right after the value: +If your configuration is correct, you should get something like this, the `# {span_id="321",trace_id="123"} ...` section is the Exemplar right after the value: [source] ---- # TYPE test counter diff --git a/docs/modules/ROOT/pages/implementations/signalFx.adoc b/docs/modules/ROOT/pages/implementations/signalFx.adoc index 2ee7e77b94..ebcccf75f3 100644 --- a/docs/modules/ROOT/pages/implementations/signalFx.adoc +++ b/docs/modules/ROOT/pages/implementations/signalFx.adoc @@ -2,6 +2,8 @@ :sectnums: :system: signalfx +CAUTION: This module has been deprecated in favor of the xref:implementations/otlp.adoc[OTLP Registry] because the https://github.com/signalfx/signalfx-java[SignalFX Java client library] that this module depends on has been deprecated. + SignalFx is a dimensional monitoring system SaaS with a full UI that operates on a push model. It has a rich set of alert "`detectors`". include::_install.adoc[] diff --git a/docs/modules/ROOT/pages/implementations/stackdriver.adoc b/docs/modules/ROOT/pages/implementations/stackdriver.adoc index 4c93a3ba03..a76d1004eb 100644 --- a/docs/modules/ROOT/pages/implementations/stackdriver.adoc +++ b/docs/modules/ROOT/pages/implementations/stackdriver.adoc @@ -127,7 +127,3 @@ You can also use https://docs.spring.io/spring-boot/docs/current/reference/html/ spring.application.name=my-application management.metrics.tags.application=${spring.application.name} ---- - -== GraalVM native image compilation - -To compile an application by using `micrometer-registry-stackdriver` to a https://www.graalvm.org/reference-manual/native-image/[native image using GraalVM], add the https://github.com/GoogleCloudPlatform/native-image-support-java[native-image-support-java] library as a dependency. Doing so ensures that the correct native image configuration is available and avoids errors like `Classes that should be initialized at run time got initialized during image building`. diff --git a/docs/modules/ROOT/pages/installing.adoc b/docs/modules/ROOT/pages/installing.adoc index 1c180843bb..a14aec350e 100644 --- a/docs/modules/ROOT/pages/installing.adoc +++ b/docs/modules/ROOT/pages/installing.adoc @@ -4,26 +4,61 @@ Micrometer contains a core library with the instrumentation SPI and an in-memory implementation that does not export data anywhere, a series of modules with implementations for various monitoring systems, and a test module. -To use Micrometer, add the dependency for your monitoring system. +To use Micrometer, add the dependency for your monitoring system. It is recommended to use the BOM provided by Micrometer, you can configure it as follows (you only need to declare it once in your project). -The following example adds Prometheus in Gradle: +NOTE: If you use a framework, it might have dependency management that defines Micrometer versions or imports the Micrometer BOM. You can defer to the framework's dependency management instead of declaring the Micrometer BOM directly in that case. + +== Gradle + +The following example configures the Micrometer BOM in Gradle: + +[source,groovy,subs=+attributes] +---- +implementation platform('io.micrometer:micrometer-bom:{micrometer-version}') +---- + +After the BOM is configured, the following example adds Prometheus in Gradle: [source,groovy] ---- -implementation 'io.micrometer:micrometer-registry-prometheus:latest.release' +implementation 'io.micrometer:micrometer-registry-prometheus' +---- + +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +== Maven + +The following example configures the Micrometer BOM in Maven: + +[source,xml,subs=+attributes] +---- + + + + io.micrometer + micrometer-bom + {micrometer-version} + pom + import + + + ---- -The following example adds Prometheus in Maven: +After the BOM is configured, the following example adds Prometheus in Maven: [source,xml] ---- io.micrometer micrometer-registry-prometheus - ${micrometer.version} ---- +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +== Multiple Monitoring Systems + Through Micrometer's composite meter registry (described in greater detail in xref:/concepts/registry.adoc#_composite_registries["Concepts"]), you can configure more than one registry implementation if you intend to publish metrics to more than one monitoring system. If you have not decided on a monitoring system yet and want only to try out the instrumentation SPI, you can add a dependency on `micrometer-core` instead and configure the `SimpleMeterRegistry`. diff --git a/docs/modules/ROOT/pages/observation/installing.adoc b/docs/modules/ROOT/pages/observation/installing.adoc index 787fc85294..6f3769bcf8 100644 --- a/docs/modules/ROOT/pages/observation/installing.adoc +++ b/docs/modules/ROOT/pages/observation/installing.adoc @@ -1,36 +1,29 @@ [[micrometer-observation-install]] = Installing -Micrometer comes with a Bill of Materials (BOM), which is a project that manages all the project versions for consistency. +It is recommended to use the BOM provided by Micrometer (or your framework if any), you can see how to configure it xref:../installing.adoc[here]. The examples below assume you are using a BOM. -The following example shows the required dependency for Micrometer Observation in Gradle: +== Gradle -[source,groovy,subs=+attributes] +After the BOM is xref:../installing.adoc[configured], add the following dependency: + +[source,groovy] ---- -implementation platform('io.micrometer:micrometer-bom:latest.release') implementation 'io.micrometer:micrometer-observation' ---- -The following example shows the required dependency in Maven: +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +== Maven -[source,xml,subs=+attributes] +After the BOM is xref:../installing.adoc[configured], add the following dependency: + +[source,xml] ---- - - - - io.micrometer - micrometer-bom - ${micrometer.version} - pom - import - - - - - - - io.micrometer - micrometer-observation - - + + io.micrometer + micrometer-observation + ---- + +NOTE: The version is not needed for this dependency since it is defined by the BOM. diff --git a/docs/modules/ROOT/pages/observation/instrumenting.adoc b/docs/modules/ROOT/pages/observation/instrumenting.adoc index c12819ff9d..adc0f8fcad 100644 --- a/docs/modules/ROOT/pages/observation/instrumenting.adoc +++ b/docs/modules/ROOT/pages/observation/instrumenting.adoc @@ -12,7 +12,7 @@ To better convey how you can do instrumentation, we need to distinguish two conc - Context propagation - Creation of Observations -*Context propagation* - We propagate existing context through threads or network. We use the xref:contextpropagation.adoc[Micrometer Context Propagation] library to define the context and to propagate it through threads. We use dedicated `SenderContext` and `ReceiverContext` objects, together with Micrometer Tracing handlers, to create Observations that propagate context over the wire. +*Context propagation* - We propagate existing context through threads or network. We use the https://docs.micrometer.io/context-propagation/reference/[Micrometer Context Propagation] library to define the context and to propagate it through threads. We use dedicated `SenderContext` and `ReceiverContext` objects, together with Micrometer Tracing handlers, to create Observations that propagate context over the wire. *Creation of Observations* - We want to wrap an operation in an Observation to get measurements. We need to know if there previously has been a parent Observation to maintain the parent-child relationship of Observations. diff --git a/docs/modules/ROOT/pages/observation/testing.adoc b/docs/modules/ROOT/pages/observation/testing.adoc index 770ae13962..7aa9e5982c 100644 --- a/docs/modules/ROOT/pages/observation/testing.adoc +++ b/docs/modules/ROOT/pages/observation/testing.adoc @@ -6,16 +6,24 @@ Micrometer Observation comes with the `micrometer-observation-test` module, whic [[micrometer-observation-installing]] == Installing -The following example shows the required dependency in Gradle (assuming that the Micrometer BOM has been added): +It is recommended to use the BOM provided by Micrometer (or your framework if any), you can see how to configure it xref:../installing.adoc[here]. The examples below assume you are using a BOM. -[source,groovy,subs=+attributes] +=== Gradle + +After the BOM is xref:../installing.adoc[configured], add the following dependency: + +[source,groovy] ----- testImplementation 'io.micrometer:micrometer-observation-test' ----- -The following example shows the required dependency in Maven (assuming that the Micrometer BOM has been added): +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +=== Maven -[source,xml,subs=+attributes] +After the BOM is xref:../installing.adoc[configured], add the following dependency: + +[source,xml] ----- io.micrometer @@ -24,6 +32,8 @@ The following example shows the required dependency in Maven (assuming that the ----- +NOTE: The version is not needed for this dependency since it is defined by the BOM. + [[micrometer-observation-runnning]] == Running Observation Unit Tests diff --git a/docs/modules/ROOT/pages/reference/java-httpclient.adoc b/docs/modules/ROOT/pages/reference/java-httpclient.adoc index deb8cff07b..4411b26c75 100644 --- a/docs/modules/ROOT/pages/reference/java-httpclient.adoc +++ b/docs/modules/ROOT/pages/reference/java-httpclient.adoc @@ -2,16 +2,26 @@ Since Java 11, an `HttpClient` is provided as part of the JDK. See https://openjdk.org/groups/net/httpclient/intro.html[this introduction] to it. Micrometer provides instrumentation of this via a `micrometer-java11` module. This module requires Java 11 or later. -For Gradle, add the following implementation: +== Installing -[source,groovy,subs=+attributes] +It is recommended to use the BOM provided by Micrometer (or your framework if any), you can see how to configure it xref:../installing.adoc[here]. The examples below assume you are using a BOM. + +=== Gradle + +After the BOM is xref:../installing.adoc[configured], add the following dependency: + +[source,groovy] ---- implementation 'io.micrometer:micrometer-java11' ---- -For Maven, add the following dependency: +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +=== Maven -[source,xml,subs=+attributes] +After the BOM is xref:../installing.adoc[configured], add the following dependency: + +[source,xml] ---- io.micrometer @@ -19,6 +29,10 @@ For Maven, add the following dependency: ---- +NOTE: The version is not needed for this dependency since it is defined by the BOM. + +== Usage + Create an `HttpClient` as you normally would. For example: [source,java,subs=+attributes] diff --git a/docs/modules/ROOT/pages/reference/jetty.adoc b/docs/modules/ROOT/pages/reference/jetty.adoc index c8b4d7b9a6..fc7eb8194f 100644 --- a/docs/modules/ROOT/pages/reference/jetty.adoc +++ b/docs/modules/ROOT/pages/reference/jetty.adoc @@ -17,7 +17,7 @@ You can collect metrics from a Jetty `Connector` by configuring it with `JettyCo server.setConnectors(new Connector[] { connector }); ---- <1> Register general connection metrics -<2> Registers metrics for bytes in/out on this connector +<2> Register metrics for bytes in/out on this connector Alternatively, you can apply the metrics instrumentation to all connectors on a `Server` as follows: diff --git a/docs/modules/ROOT/pages/reference/jvm.adoc b/docs/modules/ROOT/pages/reference/jvm.adoc index 44978e847e..ac09c6b41e 100644 --- a/docs/modules/ROOT/pages/reference/jvm.adoc +++ b/docs/modules/ROOT/pages/reference/jvm.adoc @@ -10,12 +10,14 @@ new JvmMemoryMetrics().bindTo(registry); <2> new JvmGcMetrics().bindTo(registry); <3> new ProcessorMetrics().bindTo(registry); <4> new JvmThreadMetrics().bindTo(registry); <5> +new JvmThreadDeadlockMetrics().bindTo(registry); <6> ---- <1> Gauges loaded and unloaded classes. <2> Gauges buffer and memory pool utilization. <3> Gauges max and live data size, promotion and allocation rates, and the number of times the GC pauses (or concurrent phase time in the case of CMS). <4> Gauges current CPU total and load average. <5> Gauges thread peak, the number of daemon threads, and live threads. +<6> Gauges the number of threads that are deadlocked. Micrometer also provides a meter binder for `ExecutorService`. You can instrument your `ExecutorService`, as follows: @@ -40,3 +42,16 @@ another. The reported value underestimates the actual total number of steals whe * `executor.queued` (`Gauge`): An estimate of the total number of tasks currently held in queues by worker threads. * `executor.active` (`Gauge`): An estimate of the number of threads that are currently stealing or running tasks. * `executor.running` (`Gauge`): An estimate of the number of worker threads that are not blocked but are waiting to join tasks or for other managed synchronization threads. +* `executor.parallelism` (`Gauge`): The targeted parallelism level of this pool. +* `executor.pool.size` (`Gauge`): The current number of threads in the pool. + +== Java 21 Metrics + +Micrometer provides support for https://openjdk.org/jeps/444[virtual threads] released in Java 21. In order to utilize it, you need to add the `io.micrometer:micrometer-java21` dependency to your classpath to use the binder: + +[source, java] +---- +new VirtualThreadMetrics().bindTo(registry); +---- + +The binder measures the duration (and counts the number of events) of virtual threads being pinned; also counts the number of events when starting or unparking a virtual thread failed. diff --git a/docs/modules/ROOT/pages/reference/netty.adoc b/docs/modules/ROOT/pages/reference/netty.adoc index 51fa9bb80f..e3b4bb0118 100644 --- a/docs/modules/ROOT/pages/reference/netty.adoc +++ b/docs/modules/ROOT/pages/reference/netty.adoc @@ -12,7 +12,7 @@ include::{include-java}/netty/NettyMetricsTests.java[tags=directInstrumentation, ----- Netty infrastructure can be configured in many ways, so you can also instrument lazily at runtime, as resources are used. -The fllowing example shows how to lazily create instrumentation: +The following example shows how to lazily create instrumentation: [source,java,subs=+attributes] ----- diff --git a/docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java b/docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java new file mode 100644 index 0000000000..b1889f0538 --- /dev/null +++ b/docs/src/test/java/io/micrometer/docs/metrics/CountedAspectTest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2024 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ +package io.micrometer.docs.metrics; + +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.core.annotation.Counted; +import io.micrometer.core.aop.CountedAspect; +import io.micrometer.core.aop.CountedMeterTagAnnotationHandler; +import io.micrometer.core.aop.MeterTag; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +class CountedAspectTest { + + // tag::resolvers[] + ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]"; + + // Example of a ValueExpressionResolver that uses Spring Expression Language + ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver(); + + // end::resolvers[] + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsWithText(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + // tag::meter_tag_annotation_handler[] + // Setting the handler on the aspect + countedAspect.setMeterTagAnnotationHandler( + new CountedMeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + // end::meter_tag_annotation_handler[] + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(countedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + // tag::example_value_to_string[] + service.getAnnotationForArgumentToString(15L); + + assertThat(registry.get("method.counted").tag("test", "15").counter().count()).isEqualTo(1); + // end::example_value_to_string[] + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler( + new CountedMeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(countedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + // @formatter:off + // tag::example_value_resolver[] + service.getAnnotationForTagValueResolver("foo"); + + assertThat(registry.get("method.counted") + .tag("test", "Value from myCustomTagValueResolver [foo]") + .counter() + .count()).isEqualTo(1); + // end::example_value_resolver[] + // @formatter:on + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler( + new CountedMeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(countedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + // tag::example_value_spel[] + service.getAnnotationForTagValueExpression("15L"); + + assertThat(registry.get("method.counted").tag("test", "hello characters").counter().count()).isEqualTo(1); + // end::example_value_spel[] + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void multipleMeterTagsWithExpression(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + CountedAspect countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler( + new CountedMeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(countedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + // tag::example_multi_annotations[] + service.getMultipleAnnotationsForTagValueExpression(new DataHolder("zxe", "qwe")); + + assertThat(registry.get("method.counted") + .tag("value1", "value1: zxe") + .tag("value2", "value2: qwe") + .counter() + .count()).isEqualTo(1); + // end::example_multi_annotations[] + } + + enum AnnotatedTestClass { + + CLASS_WITHOUT_INTERFACE(MeterTagClass.class), CLASS_WITH_INTERFACE(MeterTagClassChild.class); + + private final Class clazz; + + AnnotatedTestClass(Class clazz) { + this.clazz = clazz; + } + + @SuppressWarnings("unchecked") + T newInstance() { + try { + return (T) clazz.getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + // tag::interface[] + interface MeterTagClassInterface { + + @Counted + void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test); + + @Counted + void getAnnotationForTagValueExpression( + @MeterTag(key = "test", expression = "'hello' + ' characters'") String test); + + @Counted + void getAnnotationForArgumentToString(@MeterTag("test") Long param); + + @Counted + void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", + expression = "'value2: ' + value2") DataHolder param); + + } + // end::interface[] + + static class MeterTagClass implements MeterTagClassInterface { + + @Counted + @Override + public void getAnnotationForTagValueResolver( + @MeterTag(key = "test", resolver = ValueResolver.class) String test) { + } + + @Counted + @Override + public void getAnnotationForTagValueExpression( + @MeterTag(key = "test", expression = "'hello' + ' characters'") String test) { + } + + @Counted + @Override + public void getAnnotationForArgumentToString(@MeterTag("test") Long param) { + } + + @Counted + @Override + public void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", + expression = "'value2: ' + value2") DataHolder param) { + } + + } + + static class MeterTagClassChild implements MeterTagClassInterface { + + @Counted + @Override + public void getAnnotationForTagValueResolver(String test) { + } + + @Counted + @Override + public void getAnnotationForTagValueExpression(String test) { + } + + @Counted + @Override + public void getAnnotationForArgumentToString(Long param) { + } + + @Counted + @Override + public void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value2", expression = "'value2: ' + value2") DataHolder param) { + } + + } + + static class DataHolder { + + private final String value1; + + private final String value2; + + private DataHolder(String value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + + public String getValue1() { + return value1; + } + + public String getValue2() { + return value2; + } + + } + +} diff --git a/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java b/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java index 59ffe5fae2..7539097ac3 100644 --- a/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java +++ b/docs/src/test/java/io/micrometer/docs/metrics/TimedAspectTest.java @@ -31,158 +31,217 @@ class TimedAspectTest { - static class MeterTagsTests { + // tag::resolvers[] + ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]"; - // tag::resolvers[] - ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]"; + // Example of a ValueExpressionResolver that uses Spring Expression Language + ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver(); - // Example of a ValueExpressionResolver that uses Spring Expression Language - ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver(); + // end::resolvers[] - // end::resolvers[] + @ParameterizedTest + @EnumSource + void meterTagsWithText(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + TimedAspect timedAspect = new TimedAspect(registry); + // tag::meter_tag_annotation_handler[] + // Setting the handler on the aspect + timedAspect.setMeterTagAnnotationHandler( + new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + // end::meter_tag_annotation_handler[] - @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) - void meterTagsWithText(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - // tag::meter_tag_annotation_handler[] - // Setting the handler on the aspect - timedAspect.setMeterTagAnnotationHandler( - new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); - // end::meter_tag_annotation_handler[] + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(timedAspect); - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); + MeterTagClassInterface service = pf.getProxy(); - MeterTagClassInterface service = pf.getProxy(); + // tag::example_value_to_string[] + service.getAnnotationForArgumentToString(15L); - // tag::example_value_to_string[] - service.getAnnotationForArgumentToString(15L); + assertThat(registry.get("method.timed").tag("test", "15").timer().count()).isEqualTo(1); + // end::example_value_to_string[] + } - assertThat(registry.get("method.timed").tag("test", "15").timer().count()).isEqualTo(1); - // end::example_value_to_string[] - } + @ParameterizedTest + @EnumSource + void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + TimedAspect timedAspect = new TimedAspect(registry); + timedAspect.setMeterTagAnnotationHandler( + new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(timedAspect); + + MeterTagClassInterface service = pf.getProxy(); + + // @formatter:off + // tag::example_value_resolver[] + service.getAnnotationForTagValueResolver("foo"); + + assertThat(registry.get("method.timed") + .tag("test", "Value from myCustomTagValueResolver [foo]") + .timer() + .count()).isEqualTo(1); + // end::example_value_resolver[] + // @formatter:on + } - @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) - void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler( - new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + @ParameterizedTest + @EnumSource + void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + TimedAspect timedAspect = new TimedAspect(registry); + timedAspect.setMeterTagAnnotationHandler( + new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(timedAspect); - MeterTagClassInterface service = pf.getProxy(); + MeterTagClassInterface service = pf.getProxy(); - // tag::example_value_resolver[] - service.getAnnotationForTagValueResolver("foo"); + // tag::example_value_spel[] + service.getAnnotationForTagValueExpression("15L"); - assertThat(registry.get("method.timed") - .tag("test", "Value from myCustomTagValueResolver [foo]") - .timer() - .count()).isEqualTo(1); - // end::example_value_resolver[] - } + assertThat(registry.get("method.timed").tag("test", "hello characters").timer().count()).isEqualTo(1); + // end::example_value_spel[] + } - @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) - void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler( - new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void multipleMeterTagsWithExpression(AnnotatedTestClass annotatedClass) { + MeterRegistry registry = new SimpleMeterRegistry(); + TimedAspect timedAspect = new TimedAspect(registry); + timedAspect.setMeterTagAnnotationHandler( + new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver)); - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); + AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); + pf.addAspect(timedAspect); - MeterTagClassInterface service = pf.getProxy(); + MeterTagClassInterface service = pf.getProxy(); - // tag::example_value_spel[] - service.getAnnotationForTagValueExpression("15L"); + // tag::example_multi_annotations[] + service.getMultipleAnnotationsForTagValueExpression(new DataHolder("zxe", "qwe")); - assertThat(registry.get("method.timed").tag("test", "hello characters").timer().count()).isEqualTo(1); - // end::example_value_spel[] - } + assertThat( + registry.get("method.timed").tag("value1", "value1: zxe").tag("value2", "value2: qwe").timer().count()) + .isEqualTo(1); + // end::example_multi_annotations[] + } - enum AnnotatedTestClass { + enum AnnotatedTestClass { - CLASS_WITHOUT_INTERFACE(MeterTagClass.class), CLASS_WITH_INTERFACE(MeterTagClassChild.class); + CLASS_WITHOUT_INTERFACE(MeterTagClass.class), CLASS_WITH_INTERFACE(MeterTagClassChild.class); - private final Class clazz; + private final Class clazz; - AnnotatedTestClass(Class clazz) { - this.clazz = clazz; - } + AnnotatedTestClass(Class clazz) { + this.clazz = clazz; + } - @SuppressWarnings("unchecked") - T newInstance() { - try { - return (T) clazz.getDeclaredConstructor().newInstance(); - } - catch (Exception e) { - throw new RuntimeException(e); - } + @SuppressWarnings("unchecked") + T newInstance() { + try { + return (T) clazz.getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + throw new RuntimeException(e); } - } - // tag::interface[] - interface MeterTagClassInterface { + } + + // tag::interface[] + interface MeterTagClassInterface { + + @Timed + void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test); + + @Timed + void getAnnotationForTagValueExpression( + @MeterTag(key = "test", expression = "'hello' + ' characters'") String test); - @Timed - void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test); + @Timed + void getAnnotationForArgumentToString(@MeterTag("test") Long param); - @Timed - void getAnnotationForTagValueExpression( - @MeterTag(key = "test", expression = "'hello' + ' characters'") String test); + @Timed + void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", + expression = "'value2: ' + value2") DataHolder param); - @Timed - void getAnnotationForArgumentToString(@MeterTag("test") Long param); + } + // end::interface[] + + static class MeterTagClass implements MeterTagClassInterface { + @Timed + @Override + public void getAnnotationForTagValueResolver( + @MeterTag(key = "test", resolver = ValueResolver.class) String test) { } - // end::interface[] - static class MeterTagClass implements MeterTagClassInterface { + @Timed + @Override + public void getAnnotationForTagValueExpression( + @MeterTag(key = "test", expression = "'hello' + ' characters'") String test) { + } - @Timed - @Override - public void getAnnotationForTagValueResolver( - @MeterTag(key = "test", resolver = ValueResolver.class) String test) { - } + @Timed + @Override + public void getAnnotationForArgumentToString(@MeterTag("test") Long param) { + } - @Timed - @Override - public void getAnnotationForTagValueExpression( - @MeterTag(key = "test", expression = "'hello' + ' characters'") String test) { - } + @Timed + @Override + public void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", + expression = "'value2: ' + value2") DataHolder param) { + } - @Timed - @Override - public void getAnnotationForArgumentToString(@MeterTag("test") Long param) { - } + } + + static class MeterTagClassChild implements MeterTagClassInterface { + @Timed + @Override + public void getAnnotationForTagValueResolver(String test) { } - static class MeterTagClassChild implements MeterTagClassInterface { + @Timed + @Override + public void getAnnotationForTagValueExpression(String test) { + } - @Timed - @Override - public void getAnnotationForTagValueResolver(String test) { - } + @Timed + @Override + public void getAnnotationForArgumentToString(Long param) { + } - @Timed - @Override - public void getAnnotationForTagValueExpression(String test) { - } + @Timed + @Override + public void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value2", expression = "'value2: ' + value2") DataHolder param) { + } - @Timed - @Override - public void getAnnotationForArgumentToString(Long param) { - } + } + + static class DataHolder { + + private final String value1; + + private final String value2; + + private DataHolder(String value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + + public String getValue1() { + return value1; + } + public String getValue2() { + return value2; } } diff --git a/docs/src/test/java/io/micrometer/docs/observation/ObservationTestingTests.java b/docs/src/test/java/io/micrometer/docs/observation/ObservationTestingTests.java index 358f088c4e..543cf94f2e 100644 --- a/docs/src/test/java/io/micrometer/docs/observation/ObservationTestingTests.java +++ b/docs/src/test/java/io/micrometer/docs/observation/ObservationTestingTests.java @@ -44,6 +44,7 @@ void should_assert_your_observation() { .that() .hasHighCardinalityKeyValue("highTag", "highTagValue") .hasLowCardinalityKeyValue("lowTag", "lowTagValue") + .hasEvent("event1") .hasBeenStarted() .hasBeenStopped(); } @@ -87,10 +88,13 @@ static class Example { } void run() { - Observation.createNotStarted("foo", registry) + Observation observation = Observation.createNotStarted("foo", registry) .lowCardinalityKeyValue("lowTag", "lowTagValue") - .highCardinalityKeyValue("highTag", "highTagValue") - .observe(() -> System.out.println("Hello")); + .highCardinalityKeyValue("highTag", "highTagValue"); + observation.observe(() -> { + observation.event(Observation.Event.of("event1")); + System.out.println("Hello"); + }); } } diff --git a/gradle.properties b/gradle.properties index 1a6baba48a..e82d0cd5ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1g org.gradle.parallel=true org.gradle.vfs.watch=true -compatibleVersion=1.13.0 +compatibleVersion=1.14.0 kotlin.stdlib.default.dependency=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 72e1e4d8ec..0bfbc66aa7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,23 @@ [versions] -activemq-artemis = "2.37.0" +activemq-artemis = "2.38.0" application-insights = "2.6.4" archunit = "1.3.0" asmForPlugins = "7.3.1" # 1.9.20.1 is the last release that accepts jdk 11 for building aspectjweaver = "1.9.20.1" -assertj = "3.26.3" +assertj = "3.27.2" awaitility = "4.2.2" caffeine = "2.9.3" -cloudwatch2 = "2.28.19" +cloudwatch2 = "2.29.46" colt = "1.2.0" -dagger = "2.52" -dropwizard-metrics = "4.2.28" +dagger = "2.54" +dropwizard-metrics = "4.2.29" dropwizard-metrics5 = "5.0.0" dynatrace-utils = "2.2.1" ehcache2 = "2.10.9.2" ehcache3 = "3.10.8" gmetric4j = "1.0.10" -google-cloud-monitoring = "3.53.0" +google-cloud-monitoring = "3.56.0" grpc = "1.58.0" grpcKotlin = "1.4.1" guava = "32.1.2-jre" @@ -28,57 +28,57 @@ hazelcast3 = "3.12.13" hdrhistogram = "2.2.2" hibernate = "5.6.15.Final" # 2.6.0 requires JDK 11 -hsqldb = "2.7.3" +hsqldb = "2.7.4" httpcomponents-async = "4.1.5" httpcomponents-client = "4.5.14" -httpcomponents-client5 = "5.4" +httpcomponents-client5 = "5.4.1" # metrics are better with https://github.com/Netflix/Hystrix/pull/1568 introduced # in hystrix 1.5.12, but Netflix re-released 1.5.11 as 1.5.18 late in 2018. # <=1.5.11 or 1.5.18 doesn't break with Micrometer, but open metrics won't be correct necessarily. hystrix = "1.5.12" -jackson-databind = "2.18.0" +jackson-databind = "2.18.2" javax-cache = "1.1.1" javax-inject = "1" jaxb = "2.3.1" jetty9 = "9.4.56.v20240826" jetty11 = "11.0.16" jetty12 = "12.0.6" -jersey2 = "2.45" +jersey2 = "2.46" jersey3 = "3.1.9" jmh = "1.37" # 3.14.x is the newest version of OSS jOOQ that supports Java 8 jooqOld = "3.14.16" # latest version of jOOQ to run tests against -jooqNew = "3.19.13" +jooqNew = "3.19.16" jsr107 = "1.1.1" jsr305 = "3.0.2" -junit = "5.11.2" +junit = "5.11.4" kafka = "2.8.2" kafka-junit = "4.2.10" latency-utils = "2.0.3" logback12 = "1.2.13" -logback-latest = "1.5.7" -log4j = "2.24.1" +logback-latest = "1.5.12" +log4j = "2.24.3" maven-resolver = "1.9.22" mockito4 = "4.11.0" mockito5 = "5.11.0" -mongo = "4.11.4" -netty = "4.1.114.Final" +mongo = "4.11.5" +netty = "4.1.116.Final" newrelic-api = "5.14.0" # Kotlin 1.7 sample will fail from OkHttp 4.12.0 due to okio dependency being a Kotlin 1.9 module okhttp = "4.11.0" postgre = "42.7.4" -prometheus = "1.3.1" +prometheus = "1.3.5" prometheusSimpleClient = "0.16.0" reactor = "2022.0.22" rest-assured = "5.5.0" -signalfx = "1.0.46" +signalfx = "1.0.47" slf4j = "1.7.36" -spectator-atlas = "1.7.21" +spectator-atlas = "1.8.3" spring5 = "5.3.39" -spring6 = "6.1.13" +spring6 = "6.2.1" spring-javaformat = "0.0.43" -testcontainers = "1.20.2" +testcontainers = "1.20.4" tomcat = "8.5.100" wavefront = "3.4.3" wiremock = "2.35.2" @@ -98,7 +98,7 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c cloudwatch2 = { module = "software.amazon.awssdk:cloudwatch", version.ref = "cloudwatch2" } colt = { module = "colt:colt", version.ref = "colt" } commonsPool2 = "org.apache.commons:commons-pool2:2.12.0" -contextPropagation = { module = "io.micrometer:context-propagation", version = "1.1.1" } +contextPropagation = { module = "io.micrometer:context-propagation", version = "1.1.2" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } dropwizardMetricsCore = { module = "io.dropwizard.metrics:metrics-core", version.ref = "dropwizard-metrics" } @@ -111,9 +111,9 @@ ehcache3 = { module = "org.ehcache:ehcache", version.ref = "ehcache3" } felixFramework = "org.apache.felix:org.apache.felix.framework:7.0.5" felixScr = "org.apache.felix:org.apache.felix.scr:2.2.12" gmetric4j = { module = "info.ganglia.gmetric4j:gmetric4j", version.ref = "gmetric4j" } -googleCloudLibrariesBom = { module = "com.google.cloud:libraries-bom", version = "26.48.0" } +googleCloudLibrariesBom = { module = "com.google.cloud:libraries-bom", version = "26.52.0" } googleCloudMonitoring = { module = "com.google.cloud:google-cloud-monitoring", version.ref = "google-cloud-monitoring" } -googleOauth2Http = { module = "com.google.auth:google-auth-library-oauth2-http", version = "1.28.0"} +googleOauth2Http = { module = "com.google.auth:google-auth-library-oauth2-http", version = "1.30.1"} grpcApi = { module = "io.grpc:grpc-api", version.ref = "grpc" } grpcCore = { module = "io.grpc:grpc-core", version.ref = "grpc" } grpcInprocess = { module = "io.grpc:grpc-inprocess", version.ref = "grpc" } @@ -188,7 +188,7 @@ nettyBom = { module = "io.netty:netty-bom", version.ref = "netty" } newrelicApi = { module = "com.newrelic.agent.java:newrelic-api", version.ref = "newrelic-api" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } # some proto are marked alpha, hence the alpha version. metrics proto is what we use and it is marked stable -openTelemetry-proto = { module = "io.opentelemetry.proto:opentelemetry-proto", version = "1.3.2-alpha" } +openTelemetry-proto = { module = "io.opentelemetry.proto:opentelemetry-proto", version = "1.5.0-alpha" } osgiJunit5 = "org.osgi:org.osgi.test.junit5:1.3.0" postgre = { module = "org.postgresql:postgresql", version.ref = "postgre" } prometheusMetricsBom = { module = "io.prometheus:prometheus-metrics-bom", version.ref = "prometheus" } @@ -230,13 +230,14 @@ plugin-nebulaInfo = { module = "com.netflix.nebula:gradle-info-plugin", version plugin-noHttp = { module = "io.spring.nohttp:nohttp-gradle", version = "0.0.11" } plugin-nexusPublish = { module = "io.github.gradle-nexus:publish-plugin", version = "1.3.0" } plugin-javaformat = { module = "io.spring.javaformat:spring-javaformat-gradle-plugin", version.ref = "spring-javaformat" } -plugin-japicmp = { module = "me.champeau.gradle:japicmp-gradle-plugin", version = "0.4.4" } +plugin-japicmp = { module = "me.champeau.gradle:japicmp-gradle-plugin", version = "0.4.5" } plugin-downloadTask = { module = "de.undercouch:gradle-download-task", version = "5.6.0" } plugin-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.25.0" } plugin-bnd = "biz.aQute.bnd:biz.aQute.bnd.gradle:6.4.0" +plugin-bndForJava17 = "biz.aQute.bnd:biz.aQute.bnd.gradle:7.1.0" [plugins] kotlin19 = { id = "org.jetbrains.kotlin.jvm", version = "1.9.25" } kotlin17 = { id = "org.jetbrains.kotlin.jvm", version = "1.7.22" } jcstress = { id = "io.github.reyerizo.gradle.jcstress", version = "0.8.15" } -aspectj = { id = 'io.freefair.aspectj.post-compile-weaving', version = '8.10.2' } +aspectj = { id = 'io.freefair.aspectj.post-compile-weaving', version = '8.11' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72b8b..cea7a793a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d6b..f3b75f3b0d 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/implementations/micrometer-registry-azure-monitor/src/main/java/io/micrometer/azuremonitor/AzureMonitorMeterRegistry.java b/implementations/micrometer-registry-azure-monitor/src/main/java/io/micrometer/azuremonitor/AzureMonitorMeterRegistry.java index 9c04a552a2..6ec031187a 100644 --- a/implementations/micrometer-registry-azure-monitor/src/main/java/io/micrometer/azuremonitor/AzureMonitorMeterRegistry.java +++ b/implementations/micrometer-registry-azure-monitor/src/main/java/io/micrometer/azuremonitor/AzureMonitorMeterRegistry.java @@ -28,6 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Locale; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -114,7 +115,7 @@ protected void publish() { private Stream trackMeter(Meter meter) { return stream(meter.measure().spliterator(), false).map(ms -> { - MetricTelemetry mt = createMetricTelemetry(meter, ms.getStatistic().toString().toLowerCase()); + MetricTelemetry mt = createMetricTelemetry(meter, ms.getStatistic().toString().toLowerCase(Locale.ROOT)); mt.setValue(ms.getValue()); return mt; }); diff --git a/implementations/micrometer-registry-cloudwatch2/src/main/java/io/micrometer/cloudwatch2/CloudWatchMeterRegistry.java b/implementations/micrometer-registry-cloudwatch2/src/main/java/io/micrometer/cloudwatch2/CloudWatchMeterRegistry.java index 561d19b26c..b2c868629d 100644 --- a/implementations/micrometer-registry-cloudwatch2/src/main/java/io/micrometer/cloudwatch2/CloudWatchMeterRegistry.java +++ b/implementations/micrometer-registry-cloudwatch2/src/main/java/io/micrometer/cloudwatch2/CloudWatchMeterRegistry.java @@ -62,7 +62,7 @@ public class CloudWatchMeterRegistry extends StepMeterRegistry { Map standardUnitByLowercaseValue = new HashMap<>(); for (StandardUnit standardUnit : StandardUnit.values()) { if (standardUnit != StandardUnit.UNKNOWN_TO_SDK_VERSION) { - standardUnitByLowercaseValue.put(standardUnit.toString().toLowerCase(), standardUnit); + standardUnitByLowercaseValue.put(standardUnit.toString().toLowerCase(Locale.ROOT), standardUnit); } } STANDARD_UNIT_BY_LOWERCASE_VALUE = Collections.unmodifiableMap(standardUnitByLowercaseValue); @@ -308,7 +308,7 @@ StandardUnit toStandardUnit(@Nullable String unit) { if (unit == null) { return StandardUnit.NONE; } - StandardUnit standardUnit = STANDARD_UNIT_BY_LOWERCASE_VALUE.get(unit.toLowerCase()); + StandardUnit standardUnit = STANDARD_UNIT_BY_LOWERCASE_VALUE.get(unit.toLowerCase(Locale.ROOT)); return standardUnit != null ? standardUnit : StandardUnit.NONE; } diff --git a/implementations/micrometer-registry-datadog/src/test/java/io/micrometer/datadog/DatadogMeterRegistryTest.java b/implementations/micrometer-registry-datadog/src/test/java/io/micrometer/datadog/DatadogMeterRegistryTest.java index 6de8683aea..f2683e8388 100644 --- a/implementations/micrometer-registry-datadog/src/test/java/io/micrometer/datadog/DatadogMeterRegistryTest.java +++ b/implementations/micrometer-registry-datadog/src/test/java/io/micrometer/datadog/DatadogMeterRegistryTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import ru.lanwen.wiremock.ext.WiremockResolver; +import java.util.Locale; import java.util.concurrent.TimeUnit; import static com.github.tomakehurst.wiremock.client.WireMock.*; @@ -71,7 +72,7 @@ public boolean enabled() { server.stubFor(any(anyUrl())); Counter.builder("my.counter#abc") - .baseUnit(TimeUnit.MICROSECONDS.toString().toLowerCase()) + .baseUnit(TimeUnit.MICROSECONDS.toString().toLowerCase(Locale.ROOT)) .description("metric description") .register(registry) .increment(Math.PI); @@ -121,7 +122,7 @@ public boolean enabled() { server.stubFor(any(anyUrl())); Counter.builder("my.counter#abc") - .baseUnit(TimeUnit.MICROSECONDS.toString().toLowerCase()) + .baseUnit(TimeUnit.MICROSECONDS.toString().toLowerCase(Locale.ROOT)) .description("metric description") .register(registry) .increment(Math.PI); diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinition.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinition.java index 671f8653d8..018505db11 100644 --- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinition.java +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v1/DynatraceMetricDefinition.java @@ -20,6 +20,7 @@ import io.micrometer.core.instrument.util.StringEscapeUtils; import java.util.Collections; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -109,7 +110,7 @@ enum DynatraceUnit { private static Map UNITS_MAPPING = Collections .unmodifiableMap(Stream.of(DynatraceUnit.values()) - .collect(Collectors.toMap(k -> k.toString().toLowerCase() + "s", Function.identity()))); + .collect(Collectors.toMap(k -> k.toString().toLowerCase(Locale.ROOT) + "s", Function.identity()))); @Nullable static DynatraceUnit fromPlural(@Nullable String plural) { diff --git a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java index 6946f66dc0..1ba7bc8290 100644 --- a/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java +++ b/implementations/micrometer-registry-dynatrace/src/main/java/io/micrometer/dynatrace/v2/DynatraceExporterV2.java @@ -15,8 +15,11 @@ */ package io.micrometer.dynatrace.v2; -import com.dynatrace.metric.util.*; +import com.dynatrace.metric.util.DynatraceMetricApiConstants; +import com.dynatrace.metric.util.MetricException; +import com.dynatrace.metric.util.MetricLineBuilder; import com.dynatrace.metric.util.MetricLineBuilder.MetadataStep; +import com.dynatrace.metric.util.MetricLinePreConfiguration; import io.micrometer.common.lang.NonNull; import io.micrometer.common.util.StringUtils; import io.micrometer.common.util.internal.logging.InternalLogger; @@ -59,9 +62,11 @@ public final class DynatraceExporterV2 extends AbstractDynatraceExporter { private static final Pattern IS_NULL_ERROR_RESPONSE = Pattern.compile("\"error\":\\s?null"); - private static final Map staticDimensions = Collections.singletonMap("dt.metrics.source", + private static final Map STATIC_DIMENSIONS = Collections.singletonMap("dt.metrics.source", "micrometer"); + private static final Map UCUM_TIME_UNIT_MAP = ucumTimeUnitMap(); + // Loggers must be non-static for MockLoggerFactory.injectLogger() in tests. private final InternalLogger logger = InternalLoggerFactory.getInstance(DynatraceExporterV2.class); @@ -121,7 +126,7 @@ private boolean shouldIgnoreToken(DynatraceConfig config) { private Map enrichWithMetricsSourceDimensions(Map defaultDimensions) { LinkedHashMap orderedDimensions = new LinkedHashMap<>(defaultDimensions); - orderedDimensions.putAll(staticDimensions); + orderedDimensions.putAll(STATIC_DIMENSIONS); return orderedDimensions; } @@ -478,7 +483,8 @@ private boolean shouldExportMetadata(Meter.Id id) { } private MetricLineBuilder.MetadataStep enrichMetadata(MetricLineBuilder.MetadataStep metadataStep, Meter meter) { - return metadataStep.description(meter.getId().getDescription()).unit(meter.getId().getBaseUnit()); + return metadataStep.description(meter.getId().getDescription()) + .unit(mapUnitIfNeeded(meter.getId().getBaseUnit())); } /** @@ -546,4 +552,44 @@ private String extractMetricKey(String metadataLine) { return metricKey.toString(); } + /** + * Maps a unit string to a UCUM-compliant string, if the mapping is known, see: + * {@link #ucumTimeUnitMap()}. + * @param unit the unit that might be mapped + * @return The UCUM-compliant string if known, otherwise returns the original unit + */ + private static String mapUnitIfNeeded(String unit) { + return unit != null ? UCUM_TIME_UNIT_MAP.getOrDefault(unit.toLowerCase(Locale.ROOT), unit) : null; + } + + /** + * Mapping from OpenJDK's {@link TimeUnit#toString()} and other common time unit + * formats to UCUM-compliant format, see: ucum.org. + * @return Time unit mapping to UCUM-compliant format + */ + private static Map ucumTimeUnitMap() { + Map mapping = new HashMap<>(); + // There are redundant elements in case the toString method of TimeUnit changes + mapping.put(TimeUnit.NANOSECONDS.toString().toLowerCase(Locale.ROOT), "ns"); + mapping.put("nanoseconds", "ns"); + mapping.put("nanosecond", "ns"); + mapping.put(TimeUnit.MICROSECONDS.toString().toLowerCase(Locale.ROOT), "us"); + mapping.put("microseconds", "us"); + mapping.put("microsecond", "us"); + mapping.put(TimeUnit.MILLISECONDS.toString().toLowerCase(Locale.ROOT), "ms"); + mapping.put("milliseconds", "ms"); + mapping.put("millisecond", "ms"); + mapping.put(TimeUnit.SECONDS.toString().toLowerCase(Locale.ROOT), "s"); + mapping.put("seconds", "s"); + mapping.put("second", "s"); + mapping.put(TimeUnit.MINUTES.toString().toLowerCase(Locale.ROOT), "min"); + mapping.put("minutes", "min"); + mapping.put("minute", "min"); + mapping.put(TimeUnit.HOURS.toString().toLowerCase(Locale.ROOT), "h"); + mapping.put("hours", "h"); + mapping.put("hour", "h"); + + return Collections.unmodifiableMap(mapping); + } + } diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java index 3837885cf9..a6c8d09cb0 100644 --- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java +++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/DynatraceMeterRegistryTest.java @@ -90,7 +90,7 @@ void shouldSendProperRequest() throws Throwable { .containsExactly("my.counter,dt.metrics.source=micrometer count,delta=12 " + clock.wallTime(), "my.timer,dt.metrics.source=micrometer gauge,min=12,max=42,sum=108,count=4 " + clock.wallTime(), "my.gauge,dt.metrics.source=micrometer gauge," + formatDouble(gauge) + " " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); }))); } @@ -115,7 +115,7 @@ void shouldResetBetweenRequests() throws Throwable { assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=22,max=50,sum=72,count=2 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); // both are bigger than the previous min and smaller than the previous max. They // will only show up if the @@ -133,7 +133,7 @@ void shouldResetBetweenRequests() throws Throwable { assertThat(request2.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=33,max=44,sum=77,count=2 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); } @Test @@ -150,7 +150,7 @@ void shouldNotTrackPercentilesWithDynatraceSummary() throws Throwable { verify(httpClient).send(assertArg((request -> assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=22,max=55,sum=77,count=2 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds")))); + "#my.timer gauge dt.meta.unit=ms")))); } @Test @@ -204,13 +204,13 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw // Timer lines "my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds", + "#my.timer gauge dt.meta.unit=ms", // Timer percentile lines. Percentiles are 0 because the step // rolled over. "my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(), - "#my.timer.percentile gauge dt.meta.unit=milliseconds", + "#my.timer.percentile gauge dt.meta.unit=ms", // DistributionSummary lines "my.ds,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " @@ -224,7 +224,7 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw // LongTaskTimer lines "my.ltt,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), - "#my.ltt gauge dt.meta.unit=milliseconds", + "#my.ltt gauge dt.meta.unit=ms", // LongTaskTimer percentile lines // 0th percentile is missing because it doesn't clear the // "interpolatable line" threshold defined in @@ -232,7 +232,7 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed() throws Throw "my.ltt.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,100 " + clock.wallTime(), "my.ltt.percentile,dt.metrics.source=micrometer,phi=0.7 gauge,100 " + clock.wallTime(), "my.ltt.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,100 " + clock.wallTime(), - "#my.ltt.percentile gauge dt.meta.unit=milliseconds")))); + "#my.ltt.percentile gauge dt.meta.unit=ms")))); } @Test @@ -272,13 +272,13 @@ void shouldTrackPercentilesWhenDynatraceSummaryInstrumentsNotUsed_shouldExport0P // Timer lines "my.timer,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds", + "#my.timer gauge dt.meta.unit=ms", // Timer percentile lines. Percentiles are 0 because the step // rolled over. "my.timer.percentile,dt.metrics.source=micrometer,phi=0 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.5 gauge,0 " + clock.wallTime(), "my.timer.percentile,dt.metrics.source=micrometer,phi=0.99 gauge,0 " + clock.wallTime(), - "#my.timer.percentile gauge dt.meta.unit=milliseconds", + "#my.timer.percentile gauge dt.meta.unit=ms", // DistributionSummary lines "my.ds,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), @@ -303,7 +303,7 @@ void shouldNotExportLinesWithZeroCount() throws Throwable { verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=44,max=44,sum=44,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"))); + "#my.timer gauge dt.meta.unit=ms"))); // reset for next export interval reset(httpClient); @@ -328,7 +328,7 @@ void shouldNotExportLinesWithZeroCount() throws Throwable { verify(httpClient).send(assertArg(request -> assertThat(request.getEntity()).asString() .hasLineCount(2) .contains("my.timer,dt.metrics.source=micrometer gauge,min=33,max=33,sum=33,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"))); + "#my.timer gauge dt.meta.unit=ms"))); } private DynatraceConfig createDefaultDynatraceConfig() { diff --git a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java index 6b28ee4354..67d5997f29 100644 --- a/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java +++ b/implementations/micrometer-registry-dynatrace/src/test/java/io/micrometer/dynatrace/v2/DynatraceExporterV2Test.java @@ -18,6 +18,7 @@ import com.dynatrace.file.util.DynatraceFileBasedConfigurationProvider; import io.micrometer.common.lang.Nullable; import io.micrometer.core.Issue; +import io.micrometer.core.instrument.LongTaskTimer.Sample; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.*; import io.micrometer.core.ipc.http.HttpSender; @@ -43,8 +44,7 @@ import static io.micrometer.core.instrument.MockClock.clock; import static java.lang.Double.*; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.concurrent.TimeUnit.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.awaitility.Awaitility.await; @@ -75,8 +75,7 @@ class DynatraceExporterV2Test { void setUp() { this.config = createDefaultDynatraceConfig(); this.clock = new MockClock(); - // Set the clock to something recent so that the Dynatrace library will not - // complain. + // So that the Dynatrace library will not complain this.clock.add(System.currentTimeMillis(), MILLISECONDS); this.httpClient = mock(HttpSender.class); @@ -622,7 +621,7 @@ void shouldSendHeadersAndBody() throws Throwable { .containsSubsequence("my.counter,dt.metrics.source=micrometer count,delta=12 " + clock.wallTime(), "my.gauge,dt.metrics.source=micrometer gauge,42 " + clock.wallTime(), "my.timer,dt.metrics.source=micrometer gauge,min=22,max=22,sum=22,count=1 " + clock.wallTime(), - "#my.timer gauge dt.meta.unit=milliseconds"); + "#my.timer gauge dt.meta.unit=ms"); })); } @@ -767,6 +766,117 @@ void shouldAddMetadataOnlyWhenUnitOrDescriptionIsPresent() { "#gauge.du gauge dt.meta.description=temperature,dt.meta.unit=kelvin"))); } + @Test + void metersShouldUseUcumCompliantUnits() { + HttpSender.Request.Builder builder = spy(HttpSender.Request.build(config.uri(), httpClient)); + when(httpClient.post(anyString())).thenReturn(builder); + + meterRegistry.timer("test.timer").record(Duration.ofMillis(12)); + DistributionSummary.builder("test.summary").baseUnit("days").register(meterRegistry).record(42.0); + meterRegistry.more().timeGauge("test.timegauge", Tags.empty(), this, TimeUnit.MICROSECONDS, x -> 1_000); + FunctionTimer.builder("test.ft", this, x -> 1, x -> 100, MILLISECONDS).register(meterRegistry); + Counter.builder("test.seconds").baseUnit("seconds").register(meterRegistry).increment(10); + FunctionCounter.builder("test.fc", this, x -> 1_000_000).baseUnit("ns").register(meterRegistry); + + Sample sample = meterRegistry.more().longTaskTimer("test.ltt").start(); + clock.add(config.step().plus(Duration.ofSeconds(2))); + + exporter.export(meterRegistry.getMeters()); + sample.stop(); + + verify(builder).withPlainText(assertArg(body -> assertThat(body.split("\n")).containsExactlyInAnyOrder( + "test.timer,dt.metrics.source=micrometer gauge,min=12,max=12,sum=12,count=1 " + clock.wallTime(), + "#test.timer gauge dt.meta.unit=ms", + "test.summary,dt.metrics.source=micrometer gauge,min=42,max=42,sum=42,count=1 " + clock.wallTime(), + "#test.summary gauge dt.meta.unit=days", + "test.timegauge,dt.metrics.source=micrometer gauge,1 " + clock.wallTime(), + "#test.timegauge gauge dt.meta.unit=ms", + "test.ft,dt.metrics.source=micrometer gauge,min=100,max=100,sum=100,count=1 " + clock.wallTime(), + "#test.ft gauge dt.meta.unit=ms", + "test.seconds,dt.metrics.source=micrometer count,delta=10 " + clock.wallTime(), + "#test.seconds count dt.meta.unit=s", + "test.fc,dt.metrics.source=micrometer count,delta=1000000 " + clock.wallTime(), + "#test.fc count dt.meta.unit=ns", + "test.ltt,dt.metrics.source=micrometer gauge,min=62000,max=62000,sum=62000,count=1 " + clock.wallTime(), + "#test.ltt gauge dt.meta.unit=ms"))); + } + + @Test + void userDefinedUnitsShouldBeFormattedToUcumCompliantUnits() { + HttpSender.Request.Builder builder = spy(HttpSender.Request.build(config.uri(), httpClient)); + when(httpClient.post(anyString())).thenReturn(builder); + + Counter.builder("test.tu.nanos").baseUnit(NANOSECONDS.toString()).register(meterRegistry).increment(1); + Counter.builder("test.nanoseconds").baseUnit("nanoseconds").register(meterRegistry).increment(2); + Counter.builder("test.nanosecond").baseUnit("nanosecond").register(meterRegistry).increment(3); + + Counter.builder("test.tu.micros").baseUnit(MICROSECONDS.toString()).register(meterRegistry).increment(4); + Counter.builder("test.microseconds").baseUnit("microseconds").register(meterRegistry).increment(5); + Counter.builder("test.microsecond").baseUnit("microsecond").register(meterRegistry).increment(6); + + Counter.builder("test.tu.millis").baseUnit(MILLISECONDS.toString()).register(meterRegistry).increment(7); + Counter.builder("test.milliseconds").baseUnit("milliseconds").register(meterRegistry).increment(8); + Counter.builder("test.millisecond").baseUnit("millisecond").register(meterRegistry).increment(9); + + Counter.builder("test.tu.seconds").baseUnit(SECONDS.toString()).register(meterRegistry).increment(10); + Counter.builder("test.seconds").baseUnit("seconds").register(meterRegistry).increment(11); + Counter.builder("test.second").baseUnit("second").register(meterRegistry).increment(12); + + Counter.builder("test.tu.minutes").baseUnit(MINUTES.toString()).register(meterRegistry).increment(13); + Counter.builder("test.minutes").baseUnit("minutes").register(meterRegistry).increment(14); + Counter.builder("test.minute").baseUnit("minute").register(meterRegistry).increment(15); + + Counter.builder("test.tu.hours").baseUnit(HOURS.toString()).register(meterRegistry).increment(16); + Counter.builder("test.hours").baseUnit("hours").register(meterRegistry).increment(17); + Counter.builder("test.hour").baseUnit("hour").register(meterRegistry).increment(18); + + clock.add(config.step().plus(Duration.ofSeconds(2))); + exporter.export(meterRegistry.getMeters()); + + verify(builder).withPlainText(assertArg(body -> assertThat(body.split("\n")).containsExactlyInAnyOrder( + "test.tu.nanos,dt.metrics.source=micrometer count,delta=1 " + clock.wallTime(), + "#test.tu.nanos count dt.meta.unit=ns", + "test.nanoseconds,dt.metrics.source=micrometer count,delta=2 " + clock.wallTime(), + "#test.nanoseconds count dt.meta.unit=ns", + "test.nanosecond,dt.metrics.source=micrometer count,delta=3 " + clock.wallTime(), + "#test.nanosecond count dt.meta.unit=ns", + + "test.tu.micros,dt.metrics.source=micrometer count,delta=4 " + clock.wallTime(), + "#test.tu.micros count dt.meta.unit=us", + "test.microseconds,dt.metrics.source=micrometer count,delta=5 " + clock.wallTime(), + "#test.microseconds count dt.meta.unit=us", + "test.microsecond,dt.metrics.source=micrometer count,delta=6 " + clock.wallTime(), + "#test.microsecond count dt.meta.unit=us", + + "test.tu.millis,dt.metrics.source=micrometer count,delta=7 " + clock.wallTime(), + "#test.tu.millis count dt.meta.unit=ms", + "test.milliseconds,dt.metrics.source=micrometer count,delta=8 " + clock.wallTime(), + "#test.milliseconds count dt.meta.unit=ms", + "test.millisecond,dt.metrics.source=micrometer count,delta=9 " + clock.wallTime(), + "#test.millisecond count dt.meta.unit=ms", + + "test.tu.seconds,dt.metrics.source=micrometer count,delta=10 " + clock.wallTime(), + "#test.tu.seconds count dt.meta.unit=s", + "test.seconds,dt.metrics.source=micrometer count,delta=11 " + clock.wallTime(), + "#test.seconds count dt.meta.unit=s", + "test.second,dt.metrics.source=micrometer count,delta=12 " + clock.wallTime(), + "#test.second count dt.meta.unit=s", + + "test.tu.minutes,dt.metrics.source=micrometer count,delta=13 " + clock.wallTime(), + "#test.tu.minutes count dt.meta.unit=min", + "test.minutes,dt.metrics.source=micrometer count,delta=14 " + clock.wallTime(), + "#test.minutes count dt.meta.unit=min", + "test.minute,dt.metrics.source=micrometer count,delta=15 " + clock.wallTime(), + "#test.minute count dt.meta.unit=min", + + "test.tu.hours,dt.metrics.source=micrometer count,delta=16 " + clock.wallTime(), + "#test.tu.hours count dt.meta.unit=h", + "test.hours,dt.metrics.source=micrometer count,delta=17 " + clock.wallTime(), + "#test.hours count dt.meta.unit=h", + "test.hour,dt.metrics.source=micrometer count,delta=18 " + clock.wallTime(), + "#test.hour count dt.meta.unit=h"))); + } + @Test void sendsTwoRequestsWhenSizeLimitIsReachedWithMetadata() { HttpSender.Request.Builder firstReq = spy(HttpSender.Request.build(config.uri(), httpClient)); diff --git a/implementations/micrometer-registry-elastic/src/main/java/io/micrometer/elastic/ElasticMeterRegistry.java b/implementations/micrometer-registry-elastic/src/main/java/io/micrometer/elastic/ElasticMeterRegistry.java index 699a11a275..513d75e3b2 100644 --- a/implementations/micrometer-registry-elastic/src/main/java/io/micrometer/elastic/ElasticMeterRegistry.java +++ b/implementations/micrometer-registry-elastic/src/main/java/io/micrometer/elastic/ElasticMeterRegistry.java @@ -18,6 +18,7 @@ import io.micrometer.common.lang.NonNull; import io.micrometer.common.util.StringUtils; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.distribution.HistogramSnapshot; import io.micrometer.core.instrument.step.StepMeterRegistry; import io.micrometer.core.instrument.util.MeterPartition; @@ -31,10 +32,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -349,7 +347,7 @@ String writeDocument(Meter meter, Consumer consumer) { StringBuilder sb = new StringBuilder(actionLine); String timestamp = generateTimestamp(); String name = getConventionName(meter.getId()); - String type = meter.getId().getType().toString().toLowerCase(); + String type = meter.getId().getType().toString().toLowerCase(Locale.ROOT); sb.append("{\"") .append(config.timestampFieldName()) .append("\":\"") diff --git a/implementations/micrometer-registry-ganglia/src/main/java/io/micrometer/ganglia/GangliaMeterRegistry.java b/implementations/micrometer-registry-ganglia/src/main/java/io/micrometer/ganglia/GangliaMeterRegistry.java index cf29639964..cdb4e5275e 100644 --- a/implementations/micrometer-registry-ganglia/src/main/java/io/micrometer/ganglia/GangliaMeterRegistry.java +++ b/implementations/micrometer-registry-ganglia/src/main/java/io/micrometer/ganglia/GangliaMeterRegistry.java @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.Locale; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -134,7 +135,7 @@ protected void publish() { private void announceMeter(Meter meter) { for (Measurement measurement : meter.measure()) { - announce(meter, measurement.getValue(), measurement.getStatistic().toString().toLowerCase()); + announce(meter, measurement.getValue(), measurement.getStatistic().toString().toLowerCase(Locale.ROOT)); } } diff --git a/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/ServiceLevelObjective.java b/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/ServiceLevelObjective.java index 623db026e6..670260825c 100644 --- a/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/ServiceLevelObjective.java +++ b/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/ServiceLevelObjective.java @@ -161,7 +161,7 @@ public double getValue(MeterRegistry registry) { public String getValueAsString(MeterRegistry registry) { double value = getValue(registry); return Double.isNaN(value) ? "no value available" - : getBaseUnit() != null && getBaseUnit().toLowerCase().contains("percent") + : getBaseUnit() != null && getBaseUnit().toLowerCase(Locale.ROOT).contains("percent") ? WHOLE_OR_SHORT_DECIMAL.get().format(value * 100) + "%" : WHOLE_OR_SHORT_DECIMAL.get().format(value); } @@ -366,7 +366,7 @@ public abstract static class NumericQuery { abstract Double getValue(MeterRegistry registry); private String thresholdString(double threshold) { - return baseUnit != null && baseUnit.toLowerCase().contains("percent") + return baseUnit != null && baseUnit.toLowerCase(Locale.ROOT).contains("percent") ? WHOLE_OR_SHORT_DECIMAL.get().format(threshold * 100) + "%" : WHOLE_OR_SHORT_DECIMAL.get().format(threshold); } diff --git a/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxApiVersion.java b/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxApiVersion.java index b8a939cf29..d0021d2827 100644 --- a/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxApiVersion.java +++ b/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxApiVersion.java @@ -20,6 +20,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.Locale; /** * Enum for the version of the InfluxDB API. @@ -32,8 +33,8 @@ public enum InfluxApiVersion { V1 { @Override String writeEndpoint(final InfluxConfig config) { - String influxEndpoint = config.uri() + "/write?consistency=" + config.consistency().name().toLowerCase() - + "&precision=ms&db=" + config.db(); + String influxEndpoint = config.uri() + "/write?consistency=" + + config.consistency().name().toLowerCase(Locale.ROOT) + "&precision=ms&db=" + config.db(); if (StringUtils.isNotBlank(config.retentionPolicy())) { influxEndpoint += "&rp=" + config.retentionPolicy(); } diff --git a/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxMeterRegistry.java b/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxMeterRegistry.java index 9255ca42b2..490c0148bc 100644 --- a/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxMeterRegistry.java +++ b/implementations/micrometer-registry-influx/src/main/java/io/micrometer/influx/InfluxMeterRegistry.java @@ -30,6 +30,7 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -175,14 +176,14 @@ Stream writeMeter(Meter m) { String fieldKey = measurement.getStatistic() .getTagValueRepresentation() .replaceAll("(.)(\\p{Upper})", "$1_$2") - .toLowerCase(); + .toLowerCase(Locale.ROOT); fields.add(new Field(fieldKey, value)); } if (fields.isEmpty()) { return Stream.empty(); } Meter.Id id = m.getId(); - return Stream.of(influxLineProtocol(id, id.getType().name().toLowerCase(), fields.stream())); + return Stream.of(influxLineProtocol(id, id.getType().name().toLowerCase(Locale.ROOT), fields.stream())); } private Stream writeLongTaskTimer(LongTaskTimer timer) { diff --git a/implementations/micrometer-registry-influx/src/test/java/io/micrometer/influx/InfluxMeterRegistryVersionsTest.java b/implementations/micrometer-registry-influx/src/test/java/io/micrometer/influx/InfluxMeterRegistryVersionsTest.java index 4e18a31203..458148f309 100644 --- a/implementations/micrometer-registry-influx/src/test/java/io/micrometer/influx/InfluxMeterRegistryVersionsTest.java +++ b/implementations/micrometer-registry-influx/src/test/java/io/micrometer/influx/InfluxMeterRegistryVersionsTest.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -386,7 +387,7 @@ private void publishSimpleStat(InfluxConfig config) { InfluxMeterRegistry registry = new InfluxMeterRegistry(config, new MockClock()); Counter.builder("my.counter") - .baseUnit(TimeUnit.MICROSECONDS.name().toLowerCase()) + .baseUnit(TimeUnit.MICROSECONDS.name().toLowerCase(Locale.ROOT)) .description("metric description") .register(registry) .increment(Math.PI); diff --git a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsAgentClientProvider.java b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsAgentClientProvider.java index 350255bf0d..a15e522738 100644 --- a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsAgentClientProvider.java +++ b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsAgentClientProvider.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -93,7 +94,7 @@ public Map writeLongTaskTimer(LongTaskTimer timer) { Map attributes = new HashMap<>(); addAttribute(ACTIVE_TASKS, timer.activeTasks(), attributes); addAttribute(DURATION, timer.duration(timer.baseTimeUnit()), attributes); - addAttribute(TIME_UNIT, timer.baseTimeUnit().name().toLowerCase(), attributes); + addAttribute(TIME_UNIT, timer.baseTimeUnit().name().toLowerCase(Locale.ROOT), attributes); // process meter's name, type and tags addMeterAsAttributes(timer.getId(), attributes); return attributes; @@ -141,7 +142,7 @@ public Map writeTimeGauge(TimeGauge gauge) { } Map attributes = new HashMap<>(); addAttribute(VALUE, value, attributes); - addAttribute(TIME_UNIT, gauge.baseTimeUnit().name().toLowerCase(), attributes); + addAttribute(TIME_UNIT, gauge.baseTimeUnit().name().toLowerCase(Locale.ROOT), attributes); // process meter's name, type and tags addMeterAsAttributes(gauge.getId(), attributes); return attributes; @@ -167,7 +168,7 @@ public Map writeTimer(Timer timer) { addAttribute(AVG, timer.mean(timeUnit), attributes); addAttribute(TOTAL_TIME, timer.totalTime(timeUnit), attributes); addAttribute(MAX, timer.max(timeUnit), attributes); - addAttribute(TIME_UNIT, timeUnit.name().toLowerCase(), attributes); + addAttribute(TIME_UNIT, timeUnit.name().toLowerCase(Locale.ROOT), attributes); // process meter's name, type and tags addMeterAsAttributes(timer.getId(), attributes); return attributes; @@ -180,7 +181,7 @@ public Map writeFunctionTimer(FunctionTimer timer) { addAttribute(COUNT, timer.count(), attributes); addAttribute(AVG, timer.mean(timeUnit), attributes); addAttribute(TOTAL_TIME, timer.totalTime(timeUnit), attributes); - addAttribute(TIME_UNIT, timeUnit.name().toLowerCase(), attributes); + addAttribute(TIME_UNIT, timeUnit.name().toLowerCase(Locale.ROOT), attributes); // process meter's name, type and tags addMeterAsAttributes(timer.getId(), attributes); return attributes; diff --git a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsApiClientProvider.java b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsApiClientProvider.java index 82d1aa32ea..8bd14dd871 100644 --- a/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsApiClientProvider.java +++ b/implementations/micrometer-registry-new-relic/src/main/java/io/micrometer/newrelic/NewRelicInsightsApiClientProvider.java @@ -16,6 +16,7 @@ package io.micrometer.newrelic; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.config.NamingConvention; import io.micrometer.core.instrument.util.DoubleFormat; import io.micrometer.core.instrument.util.MeterPartition; @@ -26,10 +27,7 @@ import java.net.InetSocketAddress; import java.net.Proxy; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -125,7 +123,7 @@ public Stream writeLongTaskTimer(LongTaskTimer timer) { TimeUnit timeUnit = timer.baseTimeUnit(); return Stream.of(event(timer.getId(), new Attribute(ACTIVE_TASKS, timer.activeTasks()), new Attribute(DURATION, timer.duration(timeUnit)), - new Attribute(TIME_UNIT, timeUnit.name().toLowerCase()))); + new Attribute(TIME_UNIT, timeUnit.name().toLowerCase(Locale.ROOT)))); } @Override @@ -156,7 +154,7 @@ public Stream writeTimeGauge(TimeGauge gauge) { double value = gauge.value(); if (Double.isFinite(value)) { return Stream.of(event(gauge.getId(), new Attribute(VALUE, value), - new Attribute(TIME_UNIT, gauge.baseTimeUnit().name().toLowerCase()))); + new Attribute(TIME_UNIT, gauge.baseTimeUnit().name().toLowerCase(Locale.ROOT)))); } return Stream.empty(); } @@ -171,9 +169,10 @@ public Stream writeSummary(DistributionSummary summary) { @Override public Stream writeTimer(Timer timer) { TimeUnit timeUnit = timer.baseTimeUnit(); - return Stream.of(event(timer.getId(), new Attribute(COUNT, timer.count()), - new Attribute(AVG, timer.mean(timeUnit)), new Attribute(TOTAL_TIME, timer.totalTime(timeUnit)), - new Attribute(MAX, timer.max(timeUnit)), new Attribute(TIME_UNIT, timeUnit.name().toLowerCase()))); + return Stream + .of(event(timer.getId(), new Attribute(COUNT, timer.count()), new Attribute(AVG, timer.mean(timeUnit)), + new Attribute(TOTAL_TIME, timer.totalTime(timeUnit)), new Attribute(MAX, timer.max(timeUnit)), + new Attribute(TIME_UNIT, timeUnit.name().toLowerCase(Locale.ROOT)))); } @Override @@ -181,7 +180,7 @@ public Stream writeFunctionTimer(FunctionTimer timer) { TimeUnit timeUnit = timer.baseTimeUnit(); return Stream.of(event(timer.getId(), new Attribute(COUNT, timer.count()), new Attribute(AVG, timer.mean(timeUnit)), new Attribute(TOTAL_TIME, timer.totalTime(timeUnit)), - new Attribute(TIME_UNIT, timeUnit.name().toLowerCase()))); + new Attribute(TIME_UNIT, timeUnit.name().toLowerCase(Locale.ROOT)))); } @Override diff --git a/implementations/micrometer-registry-otlp/build.gradle b/implementations/micrometer-registry-otlp/build.gradle index 4d7bc0e06a..42ec3791bc 100644 --- a/implementations/micrometer-registry-otlp/build.gradle +++ b/implementations/micrometer-registry-otlp/build.gradle @@ -10,6 +10,7 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.awaitility:awaitility' + testImplementation libs.mockitoCore5 } dockerTest { diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/AggregationTemporality.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/AggregationTemporality.java index ed597f96b9..4d832b8d18 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/AggregationTemporality.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/AggregationTemporality.java @@ -36,7 +36,7 @@ public enum AggregationTemporality { */ CUMULATIVE; - public static io.opentelemetry.proto.metrics.v1.AggregationTemporality toOtlpAggregationTemporality( + static io.opentelemetry.proto.metrics.v1.AggregationTemporality toOtlpAggregationTemporality( AggregationTemporality aggregationTemporality) { switch (aggregationTemporality) { case DELTA: diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java index 753473ebd6..8b28489dc4 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java @@ -22,6 +22,7 @@ import java.time.Duration; import java.net.URLDecoder; import java.util.Arrays; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -151,7 +152,7 @@ default AggregationTemporality aggregationTemporality() { return getEnum(this, AggregationTemporality.class, "aggregationTemporality").orElseGet(() -> { String preference = System.getenv().get("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"); if (preference != null) { - return AggregationTemporality.valueOf(preference.toUpperCase()); + return AggregationTemporality.valueOf(preference.toUpperCase(Locale.ROOT)); } return AggregationTemporality.CUMULATIVE; }); @@ -184,12 +185,12 @@ default Map headers() { String metricsHeaders = env.getOrDefault("OTEL_EXPORTER_OTLP_METRICS_HEADERS", "").trim(); headersString = Objects.equals(headersString, "") ? metricsHeaders : headersString + "," + metricsHeaders; try { - // headers are encoded as URL - see + // headers are URL-encoded - see // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables headersString = URLDecoder.decode(headersString, "UTF-8"); } catch (Exception e) { - throw new InvalidConfigurationException("Cannot URL decode header value: " + headersString, e); + throw new InvalidConfigurationException("Cannot URL decode headers value: " + headersString, e); } } diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java index 3427ff2b31..75e4e4769a 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java @@ -91,6 +91,8 @@ public class OtlpMeterRegistry extends PushMeterRegistry { private final TimeUnit baseTimeUnit; + private final String userAgentHeader; + // Time when the last scheduled rollOver has started. Applicable only for delta // flavour. private volatile long lastMeterRolloverStartTime = -1; @@ -117,15 +119,17 @@ public OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFac this(config, clock, threadFactory, new HttpUrlConnectionSender()); } + // VisibleForTesting // not public until we decide what we want to expose in public API // HttpSender may not be a good idea if we will support a non-HTTP transport - private OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpSender) { + OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpSender) { super(config, clock); this.config = config; this.baseTimeUnit = config.baseTimeUnit(); this.httpSender = httpSender; this.resource = Resource.newBuilder().addAllAttributes(getResourceAttributes()).build(); this.aggregationTemporality = config.aggregationTemporality(); + this.userAgentHeader = getUserAgentHeader(); config().namingConvention(NamingConvention.dot); start(threadFactory); } @@ -175,6 +179,7 @@ protected void publish() { .build()) .build(); HttpSender.Request.Builder httpRequest = this.httpSender.post(this.config.url()) + .withHeader("User-Agent", this.userAgentHeader) .withContent("application/x-protobuf", request.toByteArray()); this.config.headers().forEach(httpRequest::withHeader); HttpSender.Response response = httpRequest.send(); @@ -487,4 +492,13 @@ static double[] getSloWithPositiveInf(DistributionStatisticConfig distributionSt return sloWithPositiveInf; } + private String getUserAgentHeader() { + String userAgent = "Micrometer-OTLP-Exporter-Java"; + String version = getClass().getPackage().getImplementationVersion(); + if (version != null) { + userAgent += "/" + version; + } + return userAgent; + } + } diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMetricConverter.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMetricConverter.java index 93e2313a70..2c79d334fa 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMetricConverter.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMetricConverter.java @@ -57,6 +57,7 @@ class OtlpMetricConverter { private final long deltaTimeUnixNano; + @SuppressWarnings("deprecation") OtlpMetricConverter(Clock clock, Duration step, TimeUnit baseTimeUnit, AggregationTemporality aggregationTemporality, NamingConvention namingConvention) { this.clock = clock; diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/Base2ExponentialHistogram.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/Base2ExponentialHistogram.java index 8f58b3c794..ef2e54115b 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/Base2ExponentialHistogram.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/Base2ExponentialHistogram.java @@ -167,15 +167,16 @@ public void recordDouble(double value) { zeroCount.increment(); return; } + recordToHistogram(value); + } + private synchronized void recordToHistogram(final double value) { int index = base2IndexProvider.getIndexForValue(value); if (!circularCountHolder.increment(index, 1)) { - synchronized (this) { - int downScaleFactor = getDownScaleFactor(index); - downScale(downScaleFactor); - index = base2IndexProvider.getIndexForValue(value); - circularCountHolder.increment(index, 1); - } + int downScaleFactor = getDownScaleFactor(index); + downScale(downScaleFactor); + index = base2IndexProvider.getIndexForValue(value); + circularCountHolder.increment(index, 1); } } diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/CircularCountHolder.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/CircularCountHolder.java index 0911a98c9a..d97d4f4543 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/CircularCountHolder.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/internal/CircularCountHolder.java @@ -15,7 +15,7 @@ */ package io.micrometer.registry.otlp.internal; -import java.util.concurrent.atomic.AtomicLongArray; +import java.util.Arrays; /** * The CircularCountHolder is inspired from null; withEnvironmentVariable("OTEL_EXPORTER_OTLP_HEADERS", "header2=%-1").execute(() -> { assertThatThrownBy(config::headers).isInstanceOf(InvalidConfigurationException.class) - .hasMessage("Cannot URL decode header value: header2=%-1,"); + .hasMessage("Cannot URL decode headers value: header2=%-1,"); }); } diff --git a/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java b/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java index e86e67d6c2..9ddd41918e 100644 --- a/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java +++ b/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java @@ -15,14 +15,17 @@ */ package io.micrometer.registry.otlp; -import io.micrometer.core.instrument.*; +import io.micrometer.core.Issue; import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.*; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; -import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.util.NamedThreadFactory; +import io.micrometer.core.ipc.http.HttpSender; import io.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint; import io.opentelemetry.proto.metrics.v1.HistogramDataPoint; import io.opentelemetry.proto.metrics.v1.Metric; import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -31,6 +34,8 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.Mockito.*; import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; /** @@ -47,16 +52,27 @@ abstract class OtlpMeterRegistryTest { protected static final Tag meterTag = Tag.of("key", "value"); - protected MockClock clock = new MockClock(); + protected MockClock clock; - OtlpMeterRegistry registry = new OtlpMeterRegistry(otlpConfig(), clock); + private HttpSender mockHttpSender; - OtlpMeterRegistry registryWithExponentialHistogram = new OtlpMeterRegistry(exponentialHistogramOtlpConfig(), clock); + OtlpMeterRegistry registry; + + OtlpMeterRegistry registryWithExponentialHistogram; abstract OtlpConfig otlpConfig(); abstract OtlpConfig exponentialHistogramOtlpConfig(); + @BeforeEach + void setUp() { + this.clock = new MockClock(); + this.mockHttpSender = mock(HttpSender.class); + this.registry = new OtlpMeterRegistry(otlpConfig(), this.clock, + new NamedThreadFactory("otlp-metrics-publisher"), this.mockHttpSender); + this.registryWithExponentialHistogram = new OtlpMeterRegistry(exponentialHistogramOtlpConfig(), clock); + } + // If the service.name was not specified, SDKs MUST fallback to 'unknown_service' @Test void unknownServiceByDefault() { @@ -129,6 +145,23 @@ void timeGauge() { + " time_unix_nano: 1000000\n" + " as_double: 0.024\n" + " }\n" + "}\n"); } + @Issue("#5577") + @Test + void httpHeaders() throws Throwable { + HttpSender.Request.Builder builder = HttpSender.Request.build(otlpConfig().url(), this.mockHttpSender); + when(mockHttpSender.post(otlpConfig().url())).thenReturn(builder); + + when(mockHttpSender.send(isA(HttpSender.Request.class))).thenReturn(new HttpSender.Response(200, "")); + + writeToMetric(TimeGauge.builder("gauge.time", this, TimeUnit.MICROSECONDS, o -> 24).register(registry)); + registry.publish(); + + verify(this.mockHttpSender).send(assertArg(request -> { + assertThat(request.getRequestHeaders().get("User-Agent")).startsWith("Micrometer-OTLP-Exporter-Java"); + assertThat(request.getRequestHeaders()).containsEntry("Content-Type", "application/x-protobuf"); + })); + } + @Test void distributionWithPercentileShouldWriteSummary() { Timer.Builder timer = Timer.builder("timer") @@ -173,6 +206,7 @@ void distributionWithPercentileHistogramShouldWriteHistogramOrExponentialHistogr .isEqualTo(Metric.DataCase.EXPONENTIAL_HISTOGRAM.getNumber()); } + @SuppressWarnings("deprecation") @Test void multipleMetricsWithSameMetaDataShouldBeSingleMetric() { Tags firstTag = Tags.of("key", "first"); @@ -394,6 +428,7 @@ protected void stepOverNStep(int numStepsToSkip) { clock.addSeconds(otlpConfig().step().getSeconds() * numStepsToSkip); } + @SuppressWarnings("deprecation") protected void assertHistogram(Metric metric, long startTime, long endTime, String unit, long count, double sum, double max) { assertThat(metric.getHistogram().getAggregationTemporality()) @@ -419,6 +454,7 @@ protected void assertHistogram(Metric metric, long startTime, long endTime, Stri } } + @SuppressWarnings("deprecation") protected void assertSum(Metric metric, long startTime, long endTime, double expectedValue) { NumberDataPoint sumDataPoint = metric.getSum().getDataPoints(0); assertMetricMetadata(metric, Optional.empty()); diff --git a/implementations/micrometer-registry-prometheus/build.gradle b/implementations/micrometer-registry-prometheus/build.gradle index cad117908b..c91ceb20a9 100644 --- a/implementations/micrometer-registry-prometheus/build.gradle +++ b/implementations/micrometer-registry-prometheus/build.gradle @@ -18,3 +18,7 @@ dependencies { testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.awaitility:awaitility' } + +dockerTest { + systemProperty 'prometheus.version', 'v2.55.1' +} diff --git a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java index 1a1f833519..b7e187cea5 100644 --- a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java +++ b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java @@ -44,7 +44,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -563,7 +562,7 @@ private void onMeterRemoved(Meter meter) { } private MetricMetadata getMetadata(String name, @Nullable String description) { - String help = prometheusConfig.descriptions() ? Optional.ofNullable(description).orElse(" ") : " "; + String help = prometheusConfig.descriptions() && description != null ? description : " "; // Unit is intentionally not set, see: // https://github.com/OpenObservability/OpenMetrics/blob/1386544931307dff279688f332890c31b6c5de36/specification/OpenMetrics.md#unit return new MetricMetadata(name, help, null); diff --git a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryIntegrationTest.java b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryIntegrationTest.java index 6614b12578..fb60845f4e 100644 --- a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryIntegrationTest.java +++ b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryIntegrationTest.java @@ -57,7 +57,8 @@ class PrometheusMeterRegistryIntegrationTest { @Container - static GenericContainer prometheus = new GenericContainer<>(DockerImageName.parse("prom/prometheus:latest")) + static GenericContainer prometheus = new GenericContainer<>( + DockerImageName.parse("prom/prometheus:" + getPrometheusImageVersion())) .withCommand("--config.file=/etc/prometheus/prometheus.yml") .withClasspathResourceMapping("prometheus.yml", "/etc/prometheus/prometheus.yml", READ_ONLY) .waitingFor(Wait.forLogMessage(".*Server is ready to receive web requests.*", 1)) @@ -72,6 +73,15 @@ class PrometheusMeterRegistryIntegrationTest { @Nullable private HttpServer prometheusTextServer; + private static String getPrometheusImageVersion() { + String version = System.getProperty("prometheus.version"); + if (version == null) { + throw new IllegalStateException( + "System property 'prometheus.version' is not set. This should be set in the build configuration for running from the command line. If you are running PrometheusMeterRegistryIntegrationTest from an IDE, set the system property to the desired prom/prometheus image version."); + } + return version; + } + @BeforeEach void setUp() { org.testcontainers.Testcontainers.exposeHostPorts(12345, 12346); @@ -193,7 +203,7 @@ private void verifyPrometheusTextScrapeResult() { .then() .statusCode(200) .header("Content-Type", "text/plain; version=0.0.4; charset=utf-8") - .body(not((contains("# EOF")))); + .body(not(contains("# EOF"))); // @formatter:on } diff --git a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java index ebb6bd26cf..860712e4f3 100644 --- a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java +++ b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java @@ -1016,6 +1016,12 @@ void doesNotCallConventionOnScrape() { assertThat(convention.tagKeyCount.get()).isEqualTo(expectedTagKeyCount); } + @Test + void scrapeWhenMeterNameContainsSingleCharacter() { + registry.counter("c").increment(); + assertThatNoException().isThrownBy(() -> registry.scrape()); + } + private static class CountingPrometheusNamingConvention extends PrometheusNamingConvention { AtomicInteger nameCount = new AtomicInteger(); diff --git a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusNamingConventionTest.java b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusNamingConventionTest.java index cf8e209a42..e02b2fda9f 100644 --- a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusNamingConventionTest.java +++ b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusNamingConventionTest.java @@ -30,7 +30,7 @@ class PrometheusNamingConventionTest { @Test void formatName() { - assertThat(convention.name("123abc/{:id}æ°´", Meter.Type.GAUGE)).startsWith("_23abc__:id__"); + assertThat(convention.name("123abc/{:id}æ°´", Meter.Type.GAUGE)).startsWith("_23abc___id__"); } @Test diff --git a/implementations/micrometer-registry-signalfx/build.gradle b/implementations/micrometer-registry-signalfx/build.gradle index f653ce2ee0..83e0b942c8 100644 --- a/implementations/micrometer-registry-signalfx/build.gradle +++ b/implementations/micrometer-registry-signalfx/build.gradle @@ -1,3 +1,5 @@ +description = 'MeterRegistry implementation for sending metrics to SignalFX. This module is deprecated in favor of the micrometer-registry-otlp module.' + dependencies { api project(':micrometer-core') diff --git a/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxConfig.java b/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxConfig.java index cab1e903db..6228201573 100644 --- a/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxConfig.java +++ b/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxConfig.java @@ -30,7 +30,9 @@ * Configuration for {@link SignalFxMeterRegistry}. * * @author Jon Schneider + * @deprecated this whole module is deprecated in favor of micrometer-registry-otlp */ +@Deprecated public interface SignalFxConfig extends StepRegistryConfig { @Override diff --git a/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxMeterRegistry.java b/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxMeterRegistry.java index d2287f0a83..d254f08136 100644 --- a/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxMeterRegistry.java +++ b/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxMeterRegistry.java @@ -52,7 +52,9 @@ * @author Jon Schneider * @author Johnny Lim * @since 1.0.0 + * @deprecated this whole module is deprecated in favor of micrometer-registry-otlp */ +@Deprecated public class SignalFxMeterRegistry extends StepMeterRegistry { private static final ThreadFactory DEFAULT_THREAD_FACTORY = new NamedThreadFactory("signalfx-metrics-publisher"); diff --git a/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxNamingConvention.java b/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxNamingConvention.java index 786111067f..5191105b4d 100644 --- a/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxNamingConvention.java +++ b/implementations/micrometer-registry-signalfx/src/main/java/io/micrometer/signalfx/SignalFxNamingConvention.java @@ -32,7 +32,9 @@ * conventions for metrics and dimensions * @author Jon Schneider * @author Johnny Lim + * @deprecated this whole module is deprecated in favor of micrometer-registry-otlp */ +@Deprecated public class SignalFxNamingConvention implements NamingConvention { private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(SignalFxNamingConvention.class); @@ -74,7 +76,7 @@ public String tagKey(String key) { String conventionKey = delegate.tagKey(key); conventionKey = PATTERN_TAG_KEY_DENYLISTED_CHARS.matcher(conventionKey).replaceAll("_"); - if (conventionKey.length() < 1) { + if (conventionKey.isEmpty()) { return conventionKey; } @@ -93,7 +95,7 @@ public String tagKey(String key) { if (i > 0) { conventionKey = conventionKey.substring(i); - if (conventionKey.length() < 1) { + if (conventionKey.isEmpty()) { return conventionKey; } } diff --git a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxConfigTest.java b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxConfigTest.java index 7fef3a68ce..985165130f 100644 --- a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxConfigTest.java +++ b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxConfigTest.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@SuppressWarnings("deprecation") class SignalFxConfigTest { private final Map props = new HashMap<>(); diff --git a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryCompatibilityTest.java b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryCompatibilityTest.java index 7ff7c850f9..bf51eb3fef 100644 --- a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryCompatibilityTest.java +++ b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryCompatibilityTest.java @@ -22,6 +22,7 @@ import java.time.Duration; +@SuppressWarnings("deprecation") class SignalFxMeterRegistryCompatibilityTest extends MeterRegistryCompatibilityKit { private final SignalFxConfig config = new SignalFxConfig() { diff --git a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryTest.java b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryTest.java index 5d1f6f5f56..91a4b1459f 100644 --- a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryTest.java +++ b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxMeterRegistryTest.java @@ -52,6 +52,7 @@ * * @author Johnny Lim */ +@SuppressWarnings("deprecation") class SignalFxMeterRegistryTest { private final SignalFxConfig config = new SignalFxConfig() { diff --git a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxNamingConventionTest.java b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxNamingConventionTest.java index ebfa28531b..c8ccad99a0 100644 --- a/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxNamingConventionTest.java +++ b/implementations/micrometer-registry-signalfx/src/test/java/io/micrometer/signalfx/SignalFxNamingConventionTest.java @@ -27,6 +27,7 @@ * @author Jon Schneider * @author Johnny Lim */ +@SuppressWarnings("deprecation") class SignalFxNamingConventionTest { private final SignalFxNamingConvention convention = new SignalFxNamingConvention(); diff --git a/implementations/micrometer-registry-statsd/build.gradle b/implementations/micrometer-registry-statsd/build.gradle index 5fb721f8d9..f03e936a58 100644 --- a/implementations/micrometer-registry-statsd/build.gradle +++ b/implementations/micrometer-registry-statsd/build.gradle @@ -67,5 +67,3 @@ publishing { } } } - -tasks.japicmp.enabled = false diff --git a/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryPublishTest.java b/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryPublishTest.java index 2414fec1b9..ae1d89fb6e 100644 --- a/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryPublishTest.java +++ b/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryPublishTest.java @@ -80,7 +80,7 @@ void cleanUp() { } @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource void receiveAllBufferedMetricsAfterCloseSuccessfully(StatsdProtocol protocol) throws InterruptedException { skipUdsTestOnWindows(protocol); serverLatch = new CountDownLatch(3); @@ -89,7 +89,6 @@ void receiveAllBufferedMetricsAfterCloseSuccessfully(StatsdProtocol protocol) th final int port = getPort(protocol); meterRegistry = new StatsdMeterRegistry(getBufferedConfig(protocol, port), Clock.SYSTEM); startRegistryAndWaitForClient(); - Thread.sleep(1000); Counter counter = Counter.builder("my.counter").register(meterRegistry); counter.increment(); counter.increment(); @@ -99,7 +98,7 @@ void receiveAllBufferedMetricsAfterCloseSuccessfully(StatsdProtocol protocol) th } @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource void receiveMetricsSuccessfully(StatsdProtocol protocol) throws InterruptedException { skipUdsTestOnWindows(protocol); serverLatch = new CountDownLatch(3); @@ -117,7 +116,7 @@ void receiveMetricsSuccessfully(StatsdProtocol protocol) throws InterruptedExcep } @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource void resumeSendingMetrics_whenServerIntermittentlyFails(StatsdProtocol protocol) throws InterruptedException { skipUdsTestOnWindows(protocol); serverLatch = new CountDownLatch(1); @@ -164,7 +163,7 @@ void resumeSendingMetrics_whenServerIntermittentlyFails(StatsdProtocol protocol) } @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource @Issue("#1676") void stopAndStartMeterRegistrySendsMetrics(StatsdProtocol protocol) throws InterruptedException { skipUdsTestOnWindows(protocol); @@ -207,7 +206,7 @@ void stopAndStartMeterRegistryWithLineSink() throws InterruptedException { } @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource void whenBackendInitiallyDown_metricsSentAfterBackendStarts(StatsdProtocol protocol) throws InterruptedException { skipUdsTestOnWindows(protocol); AtomicInteger writeCount = new AtomicInteger(); @@ -246,7 +245,7 @@ void whenBackendInitiallyDown_metricsSentAfterBackendStarts(StatsdProtocol proto } @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource void whenRegistryStopped_doNotConnectToBackend(StatsdProtocol protocol) throws InterruptedException { skipUdsTestOnWindows(protocol); serverLatch = new CountDownLatch(3); @@ -265,7 +264,7 @@ void whenRegistryStopped_doNotConnectToBackend(StatsdProtocol protocol) throws I } @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource @Issue("#2177") void whenSendError_reconnectsAndWritesNewMetrics(StatsdProtocol protocol) throws InterruptedException { skipUdsTestOnWindows(protocol); @@ -297,7 +296,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) @Issue("#2880") @ParameterizedTest - @EnumSource(StatsdProtocol.class) + @EnumSource void receiveParallelMetricsSuccessfully(StatsdProtocol protocol) throws InterruptedException { final int n = 10; diff --git a/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java b/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java index c3fc93166a..56c72fb54e 100644 --- a/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java +++ b/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java @@ -35,6 +35,7 @@ import java.time.Duration; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -79,7 +80,7 @@ public StatsdFlavor flavor() { } @ParameterizedTest - @EnumSource(StatsdFlavor.class) + @EnumSource void counterLineProtocol(StatsdFlavor flavor) { String line = null; switch (flavor) { @@ -112,7 +113,7 @@ void counterLineProtocol(StatsdFlavor flavor) { } @ParameterizedTest - @EnumSource(StatsdFlavor.class) + @EnumSource void gaugeLineProtocol(StatsdFlavor flavor) { final AtomicInteger n = new AtomicInteger(2); final StatsdConfig config = configWithFlavor(flavor); @@ -145,7 +146,7 @@ void gaugeLineProtocol(StatsdFlavor flavor) { } @ParameterizedTest - @EnumSource(StatsdFlavor.class) + @EnumSource void timerLineProtocol(StatsdFlavor flavor) { String line = null; switch (flavor) { @@ -178,7 +179,7 @@ void timerLineProtocol(StatsdFlavor flavor) { } @ParameterizedTest - @EnumSource(StatsdFlavor.class) + @EnumSource void summaryLineProtocol(StatsdFlavor flavor) { String line = null; switch (flavor) { @@ -211,7 +212,7 @@ void summaryLineProtocol(StatsdFlavor flavor) { } @ParameterizedTest - @EnumSource(StatsdFlavor.class) + @EnumSource void longTaskTimerLineProtocol(StatsdFlavor flavor) { final StatsdConfig config = configWithFlavor(flavor); long stepMillis = config.step().toMillis(); @@ -260,7 +261,7 @@ void longTaskTimerLineProtocol(StatsdFlavor flavor) { void customNamingConvention() { final Processor lines = lineProcessor(); registry = StatsdMeterRegistry.builder(configWithFlavor(StatsdFlavor.ETSY)) - .nameMapper((id, convention) -> id.getName().toUpperCase()) + .nameMapper((id, convention) -> id.getName().toUpperCase(Locale.ROOT)) .clock(clock) .lineSink(toLineSink(lines)) .build(); @@ -286,7 +287,7 @@ void counterIncrementDoesNotCauseStackOverflow() { } @ParameterizedTest - @EnumSource(StatsdFlavor.class) + @EnumSource @Issue("#370") void serviceLevelObjectivesOnlyNoPercentileHistogram(StatsdFlavor flavor) { StatsdConfig config = configWithFlavor(flavor); @@ -390,7 +391,7 @@ void interactWithStoppedRegistry() { } @ParameterizedTest - @EnumSource(StatsdFlavor.class) + @EnumSource @Issue("#600") void memoryPerformanceOfNamingConventionInHotLoops(StatsdFlavor flavor) { AtomicInteger namingConventionUses = new AtomicInteger(); diff --git a/micrometer-commons/src/main/java/io/micrometer/common/KeyValues.java b/micrometer-commons/src/main/java/io/micrometer/common/KeyValues.java index 8a2730e729..6c32e8f878 100644 --- a/micrometer-commons/src/main/java/io/micrometer/common/KeyValues.java +++ b/micrometer-commons/src/main/java/io/micrometer/common/KeyValues.java @@ -36,24 +36,26 @@ */ public final class KeyValues implements Iterable { - private static final KeyValues EMPTY = new KeyValues(new KeyValue[] {}, 0); + private static final KeyValue[] EMPTY_KEY_VALUE_ARRAY = new KeyValue[0]; + + private static final KeyValues EMPTY = new KeyValues(EMPTY_KEY_VALUE_ARRAY, 0); /** - * A private array of {@code KeyValue} objects containing the sorted and deduplicated - * tags. + * An array of {@code KeyValue} objects containing the sorted and deduplicated + * key-values. */ private final KeyValue[] sortedSet; /** - * The number of valid tags present in the {@link #sortedSet} array. + * The number of valid key-values present in the {@link #sortedSet} array. */ private final int length; /** - * A private constructor that initializes a {@code KeyValues} object with a sorted set - * of keyvalues and its length. - * @param sortedSet an ordered set of unique keyvalues by key - * @param length the number of valid tags in the {@code sortedSet} + * A constructor that initializes a {@code KeyValues} object with a sorted set of + * key-values and its length. + * @param sortedSet an ordered set of unique key-values by key + * @param length the number of valid key-values in the {@code sortedSet} */ private KeyValues(KeyValue[] sortedSet, int length) { this.sortedSet = sortedSet; @@ -61,12 +63,12 @@ private KeyValues(KeyValue[] sortedSet, int length) { } /** - * Checks if the first {@code length} elements of the {@code keyvalues} array form an - * ordered set of keyvalues. - * @param keyValues an array of keyvalues. - * @param length the number of items to check. - * @return {@code true} if the first {@code length} items of {@code keyvalues} form an - * ordered set; otherwise {@code false}. + * Checks if the first {@code length} elements of the {@code keyValues} array form an + * ordered set of key-values. + * @param keyValues an array of key-values. + * @param length the number of elements to check. + * @return {@code true} if the first {@code length} elements of {@code keyValues} form + * an ordered set; otherwise {@code false}. */ private static boolean isSortedSet(KeyValue[] keyValues, int length) { if (length > keyValues.length) { @@ -82,12 +84,13 @@ private static boolean isSortedSet(KeyValue[] keyValues, int length) { } /** - * Constructs a {@code Tags} collection from the provided array of tags. - * @param keyValues an array of {@code Tag} objects, possibly unordered and/or + * Constructs a {@code KeyValues} collection from the provided array of key-values. + * @param keyValues an array of {@code KeyValue} objects, possibly unordered and/or * containing duplicates. - * @return a {@code Tags} instance with a deduplicated and ordered set of tags. + * @return a {@code KeyValues} instance with a deduplicated and ordered set of + * key-values. */ - private static KeyValues make(KeyValue[] keyValues) { + private static KeyValues toKeyValues(KeyValue[] keyValues) { int len = keyValues.length; if (!isSortedSet(keyValues, len)) { Arrays.sort(keyValues); @@ -97,10 +100,10 @@ private static KeyValues make(KeyValue[] keyValues) { } /** - * Removes duplicate tags from an ordered array of tags. - * @param keyValues an ordered array of {@code Tag} objects. - * @return the number of unique tags in the {@code tags} array after removing - * duplicates. + * Removes duplicate key-values from an ordered array of key-values. + * @param keyValues an ordered array of {@code KeyValue} objects. + * @return the number of unique key-values in the {@code keyValues} array after + * removing duplicates. */ private static int dedup(KeyValue[] keyValues) { int n = keyValues.length; @@ -121,12 +124,12 @@ private static int dedup(KeyValue[] keyValues) { } /** - * Constructs a {@code Tags} instance by merging two sets of tags in time proportional - * to the sum of their sizes. - * @param other the set of tags to merge with this one. - * @return a {@code Tags} instance with the merged sets of tags. + * Constructs a {@code KeyValues} instance by merging two sets of key-values in time + * proportional to the sum of their sizes. + * @param other the set of key-values to merge with this one. + * @return a {@code KeyValues} instance with the merged sets of key-values. */ - private KeyValues merged(KeyValues other) { + private KeyValues merge(KeyValues other) { if (other.length == 0) { return this; } @@ -134,36 +137,42 @@ private KeyValues merged(KeyValues other) { return this; } KeyValue[] sortedSet = new KeyValue[this.length + other.length]; - int sortedIdx = 0, thisIdx = 0, otherIdx = 0; - while (thisIdx < this.length && otherIdx < other.length) { - int cmp = this.sortedSet[thisIdx].compareTo(other.sortedSet[otherIdx]); + int sortedIndex = 0; + int thisIndex = 0; + int otherIndex = 0; + while (thisIndex < this.length && otherIndex < other.length) { + KeyValue thisKeyValue = this.sortedSet[thisIndex]; + KeyValue otherKeyValue = other.sortedSet[otherIndex]; + int cmp = thisKeyValue.compareTo(otherKeyValue); if (cmp > 0) { - sortedSet[sortedIdx] = other.sortedSet[otherIdx]; - otherIdx++; + sortedSet[sortedIndex] = otherKeyValue; + otherIndex++; } else if (cmp < 0) { - sortedSet[sortedIdx] = this.sortedSet[thisIdx]; - thisIdx++; + sortedSet[sortedIndex] = thisKeyValue; + thisIndex++; } else { - // In case of key conflict prefer tag from other set - sortedSet[sortedIdx] = other.sortedSet[otherIdx]; - thisIdx++; - otherIdx++; + // In case of key conflict prefer key-value from other set + sortedSet[sortedIndex] = otherKeyValue; + thisIndex++; + otherIndex++; } - sortedIdx++; + sortedIndex++; } - int thisRemaining = this.length - thisIdx; + int thisRemaining = this.length - thisIndex; if (thisRemaining > 0) { - System.arraycopy(this.sortedSet, thisIdx, sortedSet, sortedIdx, thisRemaining); - sortedIdx += thisRemaining; + System.arraycopy(this.sortedSet, thisIndex, sortedSet, sortedIndex, thisRemaining); + sortedIndex += thisRemaining; } - int otherRemaining = other.length - otherIdx; - if (otherIdx < other.sortedSet.length) { - System.arraycopy(other.sortedSet, otherIdx, sortedSet, sortedIdx, otherRemaining); - sortedIdx += otherRemaining; + else { + int otherRemaining = other.length - otherIndex; + if (otherRemaining > 0) { + System.arraycopy(other.sortedSet, otherIndex, sortedSet, sortedIndex, otherRemaining); + sortedIndex += otherRemaining; + } } - return new KeyValues(sortedSet, sortedIdx); + return new KeyValues(sortedSet, sortedIndex); } /** @@ -200,7 +209,7 @@ public KeyValues and(@Nullable KeyValue... keyValues) { if (blankVarargs(keyValues)) { return this; } - return and(make(keyValues)); + return and(toKeyValues(keyValues)); } /** @@ -237,7 +246,7 @@ public KeyValues and(@Nullable Iterable keyValues) { return KeyValues.of(keyValues); } - return merged(KeyValues.of(keyValues)); + return merge(KeyValues.of(keyValues)); } @Override @@ -362,10 +371,10 @@ else if (keyValues instanceof KeyValues) { } else if (keyValues instanceof Collection) { Collection keyValuesCollection = (Collection) keyValues; - return make(keyValuesCollection.toArray(new KeyValue[0])); + return toKeyValues(keyValuesCollection.toArray(EMPTY_KEY_VALUE_ARRAY)); } else { - return make(StreamSupport.stream(keyValues.spliterator(), false).toArray(KeyValue[]::new)); + return toKeyValues(StreamSupport.stream(keyValues.spliterator(), false).toArray(KeyValue[]::new)); } } @@ -397,7 +406,7 @@ public static KeyValues of(@Nullable String... keyValues) { for (int i = 0; i < keyValues.length; i += 2) { keyValueArray[i / 2] = KeyValue.of(keyValues[i], keyValues[i + 1]); } - return make(keyValueArray); + return toKeyValues(keyValueArray); } private static boolean blankVarargs(@Nullable Object[] args) { diff --git a/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotatedParameter.java b/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotatedObject.java similarity index 86% rename from micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotatedParameter.java rename to micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotatedObject.java index fa21bfd53a..2e1d202ecd 100644 --- a/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotatedParameter.java +++ b/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotatedObject.java @@ -25,15 +25,15 @@ * * @author Christian Schwerdtfeger */ -class AnnotatedParameter { +class AnnotatedObject { final Annotation annotation; - final Object argument; + final Object object; - AnnotatedParameter(Annotation annotation, Object argument) { + AnnotatedObject(Annotation annotation, Object object) { this.annotation = annotation; - this.argument = argument; + this.object = object; } } diff --git a/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationHandler.java b/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationHandler.java index 2a49b4c172..18b507ff08 100644 --- a/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationHandler.java +++ b/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationHandler.java @@ -26,6 +26,7 @@ import java.util.*; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -89,9 +90,9 @@ public void addAnnotatedParameters(T objectToModify, ProceedingJoinPoint pjp) { try { Method method = ((MethodSignature) pjp.getSignature()).getMethod(); method = tryToTakeMethodFromTargetClass(pjp, method); - List annotatedParameters = AnnotationUtils.findAnnotatedParameters(annotationClass, - method, pjp.getArgs()); - getAnnotationsFromInterfaces(pjp, method, annotatedParameters); + List annotatedParameters = AnnotationUtils.findAnnotatedParameters(annotationClass, method, + pjp.getArgs()); + getParametersAnnotationsFromInterfaces(pjp, method, annotatedParameters); addAnnotatedArguments(objectToModify, annotatedParameters); } catch (Exception ex) { @@ -99,6 +100,26 @@ public void addAnnotatedParameters(T objectToModify, ProceedingJoinPoint pjp) { } } + public void addAnnotatedMethodResult(T objectToModify, ProceedingJoinPoint pjp, Object result) { + try { + Method method = ((MethodSignature) pjp.getSignature()).getMethod(); + method = tryToTakeMethodFromTargetClass(pjp, method); + + List annotatedResult = new ArrayList<>(); + Arrays.stream(method.getAnnotationsByType(annotationClass)) + .map(annotation -> new AnnotatedObject(annotation, result)) + .forEach(annotatedResult::add); + getMethodAnnotationsFromInterfaces(pjp, method).stream() + .map(annotation -> new AnnotatedObject(annotation, result)) + .forEach(annotatedResult::add); + + addAnnotatedArguments(objectToModify, annotatedResult); + } + catch (Exception ex) { + log.error("Exception occurred while trying to add annotated method result", ex); + } + } + private static Method tryToTakeMethodFromTargetClass(ProceedingJoinPoint pjp, Method method) { try { return pjp.getTarget().getClass().getDeclaredMethod(method.getName(), method.getParameterTypes()); @@ -109,34 +130,48 @@ private static Method tryToTakeMethodFromTargetClass(ProceedingJoinPoint pjp, Me return method; } - private void getAnnotationsFromInterfaces(ProceedingJoinPoint pjp, Method mostSpecificMethod, - List annotatedParameters) { + private void getParametersAnnotationsFromInterfaces(ProceedingJoinPoint pjp, Method mostSpecificMethod, + List annotatedParameters) { + traverseInterfacesHierarchy(pjp, mostSpecificMethod, method -> { + List annotatedParametersForActualMethod = AnnotationUtils + .findAnnotatedParameters(annotationClass, method, pjp.getArgs()); + // annotations for a single parameter can be `duplicated` by the ones + // from parent interface, + // however later on during key-based deduplication the ones from + // specific method(target class) + // will take precedence + annotatedParameters.addAll(annotatedParametersForActualMethod); + }); + } + + private void traverseInterfacesHierarchy(ProceedingJoinPoint pjp, Method mostSpecificMethod, + Consumer consumer) { Class[] implementedInterfaces = pjp.getThis().getClass().getInterfaces(); for (Class implementedInterface : implementedInterfaces) { for (Method methodFromInterface : implementedInterface.getMethods()) { if (methodsAreTheSame(mostSpecificMethod, methodFromInterface)) { - List annotatedParametersForActualMethod = AnnotationUtils - .findAnnotatedParameters(annotationClass, methodFromInterface, pjp.getArgs()); - // annotations for a single parameter can be `duplicated` by the ones - // from parent interface, - // however later on during key-based deduplication the ones from - // specific method(target class) - // will take precedence - annotatedParameters.addAll(annotatedParametersForActualMethod); + consumer.accept(methodFromInterface); } } } } + private List getMethodAnnotationsFromInterfaces(ProceedingJoinPoint pjp, Method mostSpecificMethod) { + List allAnnotations = new ArrayList<>(); + traverseInterfacesHierarchy(pjp, mostSpecificMethod, + method -> allAnnotations.addAll(Arrays.asList(method.getAnnotationsByType(annotationClass)))); + return allAnnotations; + } + private boolean methodsAreTheSame(Method mostSpecificMethod, Method method) { return method.getName().equals(mostSpecificMethod.getName()) && Arrays.equals(method.getParameterTypes(), mostSpecificMethod.getParameterTypes()); } - private void addAnnotatedArguments(T objectToModify, List toBeAdded) { + private void addAnnotatedArguments(T objectToModify, List toBeAdded) { Set seen = new HashSet<>(); - for (AnnotatedParameter container : toBeAdded) { - KeyValue keyValue = toKeyValue.apply(container.annotation, container.argument); + for (AnnotatedObject container : toBeAdded) { + KeyValue keyValue = toKeyValue.apply(container.annotation, container.object); if (seen.add(keyValue.getKey())) { keyValueConsumer.accept(keyValue, objectToModify); } diff --git a/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationUtils.java b/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationUtils.java index f2a056241b..4969365dc2 100644 --- a/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationUtils.java +++ b/micrometer-commons/src/main/java/io/micrometer/common/annotation/AnnotationUtils.java @@ -35,14 +35,14 @@ private AnnotationUtils() { } - static List findAnnotatedParameters(Class annotationClazz, Method method, + static List findAnnotatedParameters(Class annotationClazz, Method method, Object[] args) { Parameter[] parameters = method.getParameters(); - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (int i = 0; i < parameters.length; i++) { Parameter parameter = parameters[i]; for (Annotation annotation : parameter.getAnnotationsByType(annotationClazz)) { - result.add(new AnnotatedParameter(annotation, args[i])); + result.add(new AnnotatedObject(annotation, args[i])); } } return result; diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java b/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java index 94166eecf6..91952fe525 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java @@ -17,6 +17,7 @@ import io.micrometer.common.lang.NonNullApi; import io.micrometer.common.lang.Nullable; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.core.annotation.Counted; import io.micrometer.core.instrument.*; import org.aspectj.lang.ProceedingJoinPoint; @@ -70,6 +71,7 @@ * @author Jonatan Ivanov * @author Johnny Lim * @author Yanming Zhou + * @author Jeonggi Kim * @since 1.2.0 * @see Counted */ @@ -77,6 +79,8 @@ @NonNullApi public class CountedAspect { + private static final WarnThenDebugLogger joinPointTagsFunctionLogger = new WarnThenDebugLogger(CountedAspect.class); + private static final Predicate DONT_SKIP_ANYTHING = pjp -> false; public final String DEFAULT_EXCEPTION_TAG_VALUE = "none"; @@ -165,10 +169,24 @@ public CountedAspect(MeterRegistry registry, Predicate shou public CountedAspect(MeterRegistry registry, Function> tagsBasedOnJoinPoint, Predicate shouldSkip) { this.registry = registry; - this.tagsBasedOnJoinPoint = tagsBasedOnJoinPoint; + this.tagsBasedOnJoinPoint = makeSafe(tagsBasedOnJoinPoint); this.shouldSkip = shouldSkip; } + private Function> makeSafe( + Function> function) { + return pjp -> { + try { + return function.apply(pjp); + } + catch (Throwable t) { + joinPointTagsFunctionLogger + .log("Exception thrown from the tagsBasedOnJoinPoint function configured on CountedAspect.", t); + return Tags.empty(); + } + }; + } + @Around("@within(io.micrometer.core.annotation.Counted) && !@annotation(io.micrometer.core.annotation.Counted) && execution(* *(..))") @Nullable public Object countedClass(ProceedingJoinPoint pjp) throws Throwable { @@ -220,11 +238,21 @@ private Object perform(ProceedingJoinPoint pjp, Counted counted) throws Throwabl if (stopWhenCompleted) { try { - return ((CompletionStage) pjp.proceed()) - .whenComplete((result, throwable) -> recordCompletionResult(pjp, counted, throwable)); + Object result = pjp.proceed(); + if (result == null) { + if (!counted.recordFailuresOnly()) { + record(pjp, result, counted, DEFAULT_EXCEPTION_TAG_VALUE, RESULT_TAG_SUCCESS_VALUE); + } + return result; + } + else { + CompletionStage stage = ((CompletionStage) result); + return stage + .whenComplete((res, throwable) -> recordCompletionResult(pjp, result, counted, throwable)); + } } catch (Throwable e) { - record(pjp, counted, e.getClass().getSimpleName(), RESULT_TAG_FAILURE_VALUE); + record(pjp, null, counted, e.getClass().getSimpleName(), RESULT_TAG_FAILURE_VALUE); throw e; } } @@ -232,47 +260,43 @@ private Object perform(ProceedingJoinPoint pjp, Counted counted) throws Throwabl try { Object result = pjp.proceed(); if (!counted.recordFailuresOnly()) { - record(pjp, counted, DEFAULT_EXCEPTION_TAG_VALUE, RESULT_TAG_SUCCESS_VALUE); + record(pjp, result, counted, DEFAULT_EXCEPTION_TAG_VALUE, RESULT_TAG_SUCCESS_VALUE); } return result; } catch (Throwable e) { - record(pjp, counted, e.getClass().getSimpleName(), RESULT_TAG_FAILURE_VALUE); + record(pjp, null, counted, e.getClass().getSimpleName(), RESULT_TAG_FAILURE_VALUE); throw e; } } - private void recordCompletionResult(ProceedingJoinPoint pjp, Counted counted, Throwable throwable) { + private void recordCompletionResult(ProceedingJoinPoint pjp, Object methodResult, Counted counted, + Throwable throwable) { if (throwable != null) { String exceptionTagValue = throwable.getCause() == null ? throwable.getClass().getSimpleName() : throwable.getCause().getClass().getSimpleName(); - record(pjp, counted, exceptionTagValue, RESULT_TAG_FAILURE_VALUE); + record(pjp, methodResult, counted, exceptionTagValue, RESULT_TAG_FAILURE_VALUE); } else if (!counted.recordFailuresOnly()) { - record(pjp, counted, DEFAULT_EXCEPTION_TAG_VALUE, RESULT_TAG_SUCCESS_VALUE); + record(pjp, methodResult, counted, DEFAULT_EXCEPTION_TAG_VALUE, RESULT_TAG_SUCCESS_VALUE); } } - private void record(ProceedingJoinPoint pjp, Counted counted, String exception, String result) { - counter(pjp, counted).tag(EXCEPTION_TAG, exception) - .tag(RESULT_TAG, result) + private void record(ProceedingJoinPoint pjp, Object methodResult, Counted counted, String exception, + String result) { + Counter.Builder builder = Counter.builder(counted.value()) + .description(counted.description().isEmpty() ? null : counted.description()) .tags(counted.extraTags()) - .register(registry) - .increment(); - } - - private Counter.Builder counter(ProceedingJoinPoint pjp, Counted counted) { - Counter.Builder builder = Counter.builder(counted.value()).tags(tagsBasedOnJoinPoint.apply(pjp)); - String description = counted.description(); - if (!description.isEmpty()) { - builder.description(description); - } + .tag(EXCEPTION_TAG, exception) + .tag(RESULT_TAG, result) + .tags(tagsBasedOnJoinPoint.apply(pjp)); if (meterTagAnnotationHandler != null) { meterTagAnnotationHandler.addAnnotatedParameters(builder, pjp); + meterTagAnnotationHandler.addAnnotatedMethodResult(builder, pjp, methodResult); } - return builder; + builder.register(registry).increment(); } /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java index 07415baf65..f6243224a7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTag.java @@ -37,7 +37,7 @@ */ @Retention(RetentionPolicy.RUNTIME) @Inherited -@Target(ElementType.PARAMETER) +@Target({ ElementType.PARAMETER, ElementType.METHOD }) @Repeatable(MeterTags.class) public @interface MeterTag { diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTags.java b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTags.java index 28a954333d..64f8708961 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/MeterTags.java @@ -37,7 +37,7 @@ */ @Retention(RetentionPolicy.RUNTIME) @Inherited -@Target(ElementType.PARAMETER) +@Target({ ElementType.METHOD, ElementType.PARAMETER }) @Documented public @interface MeterTags { diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java b/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java index cb6712b983..ffff8bf5d7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java @@ -17,6 +17,7 @@ import io.micrometer.common.lang.NonNullApi; import io.micrometer.common.lang.Nullable; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.core.annotation.Incubating; import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.*; @@ -81,6 +82,7 @@ * @author Nejc Korasa * @author Jonatan Ivanov * @author Yanming Zhou + * @author Jeonggi Kim * @since 1.0.0 */ @Aspect @@ -88,6 +90,8 @@ @Incubating(since = "1.0.0") public class TimedAspect { + private static final WarnThenDebugLogger joinPointTagsFunctionLogger = new WarnThenDebugLogger(TimedAspect.class); + private static final Predicate DONT_SKIP_ANYTHING = pjp -> false; public static final String DEFAULT_METRIC_NAME = "method.timed"; @@ -161,10 +165,24 @@ public TimedAspect(MeterRegistry registry, Predicate should public TimedAspect(MeterRegistry registry, Function> tagsBasedOnJoinPoint, Predicate shouldSkip) { this.registry = registry; - this.tagsBasedOnJoinPoint = tagsBasedOnJoinPoint; + this.tagsBasedOnJoinPoint = makeSafe(tagsBasedOnJoinPoint); this.shouldSkip = shouldSkip; } + private Function> makeSafe( + Function> function) { + return pjp -> { + try { + return function.apply(pjp); + } + catch (Throwable t) { + joinPointTagsFunctionLogger + .log("Exception thrown from the tagsBasedOnJoinPoint function configured on TimedAspect.", t); + return Tags.empty(); + } + }; + } + @Around("@within(io.micrometer.core.annotation.Timed) && !@annotation(io.micrometer.core.annotation.Timed) && execution(* *(..))") @Nullable public Object timedClass(ProceedingJoinPoint pjp) throws Throwable { @@ -218,39 +236,49 @@ private Object processWithTimer(ProceedingJoinPoint pjp, Timed timed, String met if (stopWhenCompleted) { try { - return ((CompletionStage) pjp.proceed()).whenComplete( - (result, throwable) -> record(pjp, timed, metricName, sample, getExceptionTag(throwable))); + Object result = pjp.proceed(); + if (result == null) { + record(pjp, result, timed, metricName, sample, DEFAULT_EXCEPTION_TAG_VALUE); + return result; + } + else { + CompletionStage stage = ((CompletionStage) result); + return stage.whenComplete((res, throwable) -> record(pjp, result, timed, metricName, sample, + getExceptionTag(throwable))); + } } catch (Throwable e) { - record(pjp, timed, metricName, sample, e.getClass().getSimpleName()); + record(pjp, null, timed, metricName, sample, e.getClass().getSimpleName()); throw e; } } String exceptionClass = DEFAULT_EXCEPTION_TAG_VALUE; + Object result = null; try { - return pjp.proceed(); + result = pjp.proceed(); + return result; } catch (Throwable e) { exceptionClass = e.getClass().getSimpleName(); throw e; } finally { - record(pjp, timed, metricName, sample, exceptionClass); + record(pjp, result, timed, metricName, sample, exceptionClass); } } - private void record(ProceedingJoinPoint pjp, Timed timed, String metricName, Timer.Sample sample, - String exceptionClass) { + private void record(ProceedingJoinPoint pjp, Object methodResult, Timed timed, String metricName, + Timer.Sample sample, String exceptionClass) { try { - sample.stop(recordBuilder(pjp, timed, metricName, exceptionClass).register(registry)); + sample.stop(recordBuilder(pjp, methodResult, timed, metricName, exceptionClass).register(registry)); } catch (Exception e) { // ignoring on purpose } } - private Timer.Builder recordBuilder(ProceedingJoinPoint pjp, Timed timed, String metricName, + private Timer.Builder recordBuilder(ProceedingJoinPoint pjp, Object methodResult, Timed timed, String metricName, String exceptionClass) { Timer.Builder builder = Timer.builder(metricName) .description(timed.description().isEmpty() ? null : timed.description()) @@ -266,6 +294,7 @@ private Timer.Builder recordBuilder(ProceedingJoinPoint pjp, Timed timed, String if (meterTagAnnotationHandler != null) { meterTagAnnotationHandler.addAnnotatedParameters(builder, pjp); + meterTagAnnotationHandler.addAnnotatedMethodResult(builder, pjp, methodResult); } return builder; } @@ -290,8 +319,15 @@ private Object processWithLongTaskTimer(ProceedingJoinPoint pjp, Timed timed, St if (stopWhenCompleted) { try { - return ((CompletionStage) pjp.proceed()) - .whenComplete((result, throwable) -> sample.ifPresent(this::stopTimer)); + Object result = pjp.proceed(); + if (result == null) { + sample.ifPresent(this::stopTimer); + return result; + } + else { + CompletionStage stage = ((CompletionStage) result); + return stage.whenComplete((res, throwable) -> sample.ifPresent(this::stopTimer)); + } } catch (Throwable e) { sample.ifPresent(this::stopTimer); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/Gauge.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Gauge.java index 54d2b88af8..dec1042359 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Gauge.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Gauge.java @@ -53,7 +53,7 @@ static Builder builder(String name, @Nullable T obj, ToDoubleFunction * @since 1.1.0 */ @Incubating(since = "1.1.0") - static Builder> builder(String name, Supplier f) { + static Builder> builder(String name, Supplier f) { return new Builder<>(name, f, f2 -> { Number val = f2.get(); return val == null ? Double.NaN : val.doubleValue(); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java index c4a6db3ef6..4e3a453cec 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/MeterRegistry.java @@ -18,6 +18,7 @@ import io.micrometer.common.lang.Nullable; import io.micrometer.common.util.internal.logging.InternalLogger; import io.micrometer.common.util.internal.logging.InternalLoggerFactory; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.core.annotation.Incubating; import io.micrometer.core.instrument.Meter.Id; import io.micrometer.core.instrument.config.MeterFilter; @@ -66,12 +67,15 @@ */ public abstract class MeterRegistry { + private static final WarnThenDebugLogger gaugeDoubleRegistrationLogger = new WarnThenDebugLogger( + MeterRegistry.class); + // @formatter:off private static final EnumMap BASE_TIME_UNIT_STRING_CACHE = Arrays.stream(TimeUnit.values()) .collect( Collectors.toMap( Function.identity(), - (timeUnit) -> timeUnit.toString().toLowerCase(), + (timeUnit) -> timeUnit.toString().toLowerCase(Locale.ROOT), (k, v) -> { throw new IllegalStateException("Duplicate keys should not exist."); }, () -> new EnumMap<>(TimeUnit.class) ) @@ -108,6 +112,12 @@ public abstract class MeterRegistry { */ private final Map preFilterIdToMeterMap = new HashMap<>(); + /** + * For reverse looking up pre-filter ID in {@link #preFilterIdToMeterMap} from the + * Meter being removed in {@link #remove(Id)}. Guarded by the {@link #meterMapLock}. + */ + private final Map meterToPreFilterIdMap = new HashMap<>(); + /** * Only needed when MeterFilter configured after Meters registered. Write/remove * guarded by meterMapLock, remove in {@link #unmarkStaleId(Id)} and other operations @@ -630,6 +640,7 @@ private Meter getOrCreateMeter(@Nullable DistributionStatisticConfig config, Meter m = preFilterIdToMeterMap.get(originalId); if (m != null && !isStaleId(originalId)) { + checkAndWarnAboutGaugeDoubleRegistration(m); return m; } @@ -642,6 +653,7 @@ private Meter getOrCreateMeter(@Nullable DistributionStatisticConfig config, if (isStaleId(originalId)) { unmarkStaleId(originalId); } + checkAndWarnAboutGaugeDoubleRegistration(m); } else { if (isClosed()) { @@ -678,6 +690,7 @@ private Meter getOrCreateMeter(@Nullable DistributionStatisticConfig config, } meterMap.put(mappedId, m); preFilterIdToMeterMap.put(originalId, m); + meterToPreFilterIdMap.put(m, originalId); unmarkStaleId(originalId); } } @@ -699,6 +712,14 @@ private boolean unmarkStaleId(Id originalId) { return !stalePreFilterIds.isEmpty() && stalePreFilterIds.remove(originalId); } + private void checkAndWarnAboutGaugeDoubleRegistration(Meter meter) { + if (meter instanceof Gauge) { + gaugeDoubleRegistrationLogger.log(() -> String.format( + "This Gauge has been already registered (%s), the Gauge registration will be ignored.", + meter.getId())); + } + } + private boolean accept(Meter.Id id) { for (MeterFilter filter : filters) { MeterFilterReply reply = filter.accept(id); @@ -760,14 +781,9 @@ public Meter remove(Meter.Id mappedId) { synchronized (meterMapLock) { final Meter removedMeter = meterMap.remove(mappedId); if (removedMeter != null) { - Iterator> iterator = preFilterIdToMeterMap.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry nextEntry = iterator.next(); - if (nextEntry.getValue().equals(removedMeter)) { - stalePreFilterIds.remove(nextEntry.getKey()); - iterator.remove(); - } - } + Id preFilterIdToRemove = meterToPreFilterIdMap.remove(removedMeter); + preFilterIdToMeterMap.remove(preFilterIdToRemove); + stalePreFilterIds.remove(preFilterIdToRemove); Set synthetics = syntheticAssociations.remove(mappedId); if (synthetics != null) { @@ -1215,6 +1231,11 @@ Map _getPreFilterIdToMeterMap() { return Collections.unmodifiableMap(preFilterIdToMeterMap); } + // VisibleForTesting + Map _getMeterToPreFilterIdMap() { + return Collections.unmodifiableMap(meterToPreFilterIdMap); + } + // VisibleForTesting Set _getStalePreFilterIds() { return Collections.unmodifiableSet(stalePreFilterIds); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/MultiGauge.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/MultiGauge.java index dd7a854505..b1e4b16757 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/MultiGauge.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/MultiGauge.java @@ -112,7 +112,7 @@ public static Row of(Tags uniqueTags, Number number) { return new Row<>(uniqueTags, number, Number::doubleValue); } - public static Row> of(Tags uniqueTags, Supplier valueFunction) { + public static Row> of(Tags uniqueTags, Supplier valueFunction) { return new Row<>(uniqueTags, valueFunction, f -> { Number value = valueFunction.get(); return value == null ? Double.NaN : value.doubleValue(); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java index 523f274c1d..1c696066e8 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java @@ -34,10 +34,12 @@ */ public final class Tags implements Iterable { - private static final Tags EMPTY = new Tags(new Tag[] {}, 0); + private static final Tag[] EMPTY_TAG_ARRAY = new Tag[0]; + + private static final Tags EMPTY = new Tags(EMPTY_TAG_ARRAY, 0); /** - * A private array of {@code Tag} objects containing the sorted and deduplicated tags. + * An array of {@code Tag} objects containing the sorted and deduplicated tags. */ private final Tag[] sortedSet; @@ -47,8 +49,8 @@ public final class Tags implements Iterable { private final int length; /** - * A private constructor that initializes a {@code Tags} object with a sorted set of - * tags and its length. + * A constructor that initializes a {@code Tags} object with a sorted set of tags and + * its length. * @param sortedSet an ordered set of unique tags by key * @param length the number of valid tags in the {@code sortedSet} */ @@ -61,8 +63,8 @@ private Tags(Tag[] sortedSet, int length) { * Checks if the first {@code length} elements of the {@code tags} array form an * ordered set of tags. * @param tags an array of tags. - * @param length the number of items to check. - * @return {@code true} if the first {@code length} items of {@code tags} form an + * @param length the number of elements to check. + * @return {@code true} if the first {@code length} elements of {@code tags} form an * ordered set; otherwise {@code false}. */ private static boolean isSortedSet(Tag[] tags, int length) { @@ -84,7 +86,7 @@ private static boolean isSortedSet(Tag[] tags, int length) { * duplicates. * @return a {@code Tags} instance with a deduplicated and ordered set of tags. */ - private static Tags make(Tag[] tags) { + private static Tags toTags(Tag[] tags) { int len = tags.length; if (!isSortedSet(tags, len)) { Arrays.sort(tags); @@ -123,7 +125,7 @@ private static int dedup(Tag[] tags) { * @param other the set of tags to merge with this one. * @return a {@code Tags} instance with the merged sets of tags. */ - private Tags merged(Tags other) { + private Tags merge(Tags other) { if (other.length == 0) { return this; } @@ -131,36 +133,42 @@ private Tags merged(Tags other) { return this; } Tag[] sortedSet = new Tag[this.length + other.length]; - int sortedIdx = 0, thisIdx = 0, otherIdx = 0; - while (thisIdx < this.length && otherIdx < other.length) { - int cmp = this.sortedSet[thisIdx].compareTo(other.sortedSet[otherIdx]); + int sortedIndex = 0; + int thisIndex = 0; + int otherIndex = 0; + while (thisIndex < this.length && otherIndex < other.length) { + Tag thisTag = this.sortedSet[thisIndex]; + Tag otherTag = other.sortedSet[otherIndex]; + int cmp = thisTag.compareTo(otherTag); if (cmp > 0) { - sortedSet[sortedIdx] = other.sortedSet[otherIdx]; - otherIdx++; + sortedSet[sortedIndex] = otherTag; + otherIndex++; } else if (cmp < 0) { - sortedSet[sortedIdx] = this.sortedSet[thisIdx]; - thisIdx++; + sortedSet[sortedIndex] = thisTag; + thisIndex++; } else { // In case of key conflict prefer tag from other set - sortedSet[sortedIdx] = other.sortedSet[otherIdx]; - thisIdx++; - otherIdx++; + sortedSet[sortedIndex] = otherTag; + thisIndex++; + otherIndex++; } - sortedIdx++; + sortedIndex++; } - int thisRemaining = this.length - thisIdx; + int thisRemaining = this.length - thisIndex; if (thisRemaining > 0) { - System.arraycopy(this.sortedSet, thisIdx, sortedSet, sortedIdx, thisRemaining); - sortedIdx += thisRemaining; + System.arraycopy(this.sortedSet, thisIndex, sortedSet, sortedIndex, thisRemaining); + sortedIndex += thisRemaining; } - int otherRemaining = other.length - otherIdx; - if (otherIdx < other.sortedSet.length) { - System.arraycopy(other.sortedSet, otherIdx, sortedSet, sortedIdx, otherRemaining); - sortedIdx += otherRemaining; + else { + int otherRemaining = other.length - otherIndex; + if (otherRemaining > 0) { + System.arraycopy(other.sortedSet, otherIndex, sortedSet, sortedIndex, otherRemaining); + sortedIndex += otherRemaining; + } } - return new Tags(sortedSet, sortedIdx); + return new Tags(sortedSet, sortedIndex); } /** @@ -197,7 +205,7 @@ public Tags and(@Nullable Tag... tags) { if (blankVarargs(tags)) { return this; } - return and(make(tags)); + return and(toTags(tags)); } /** @@ -214,7 +222,7 @@ public Tags and(@Nullable Iterable tags) { if (this.length == 0) { return Tags.of(tags); } - return merged(Tags.of(tags)); + return merge(Tags.of(tags)); } @Override @@ -323,10 +331,10 @@ else if (tags instanceof Tags) { } else if (tags instanceof Collection) { Collection tagsCollection = (Collection) tags; - return make(tagsCollection.toArray(new Tag[0])); + return toTags(tagsCollection.toArray(EMPTY_TAG_ARRAY)); } else { - return make(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new)); + return toTags(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new)); } } @@ -358,7 +366,7 @@ public static Tags of(@Nullable String... keyValues) { for (int i = 0; i < keyValues.length; i += 2) { tags[i / 2] = Tag.of(keyValues[i], keyValues[i + 1]); } - return make(tags); + return toTags(tags); } private static boolean blankVarargs(@Nullable Object[] args) { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/TimeGauge.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/TimeGauge.java index 0537c4ddd4..21ff8e3078 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/TimeGauge.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/TimeGauge.java @@ -46,7 +46,7 @@ static Builder builder(String name, @Nullable T obj, TimeUnit fUnits, ToD * @since 1.7.0 */ @Incubating(since = "1.7.0") - static Builder> builder(String name, Supplier f, TimeUnit fUnits) { + static Builder> builder(String name, Supplier f, TimeUnit fUnits) { return new Builder<>(name, f, fUnits, f2 -> { Number val = f2.get(); return val == null ? Double.NaN : val.doubleValue(); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java index 90aada7c50..9d2b31c320 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java @@ -37,6 +37,8 @@ class HazelcastIMapAdapter { private static final InternalLogger log = InternalLoggerFactory.getInstance(HazelcastIMapAdapter.class); + private static final Class CLASS_DISTRIBUTED_OBJECT = resolveClass("com.hazelcast.core.DistributedObject"); + private static final Class CLASS_I_MAP = resolveOneOf("com.hazelcast.map.IMap", "com.hazelcast.core.IMap"); private static final Class CLASS_LOCAL_MAP = resolveOneOf("com.hazelcast.map.LocalMapStats", @@ -50,8 +52,8 @@ class HazelcastIMapAdapter { private static final MethodHandle GET_LOCAL_MAP_STATS; static { - GET_NAME = resolveIMapMethod("getName", methodType(String.class)); - GET_LOCAL_MAP_STATS = resolveIMapMethod("getLocalMapStats", methodType(CLASS_LOCAL_MAP)); + GET_NAME = resolveMethod(CLASS_DISTRIBUTED_OBJECT, "getName", methodType(String.class)); + GET_LOCAL_MAP_STATS = resolveMethod(CLASS_I_MAP, "getLocalMapStats", methodType(CLASS_LOCAL_MAP)); } private final WeakReference cache; @@ -252,9 +254,9 @@ private static MethodHandle resolveMethod(String name, MethodType mt) { } - private static MethodHandle resolveIMapMethod(String name, MethodType mt) { + private static MethodHandle resolveMethod(Class clazz, String name, MethodType mt) { try { - return MethodHandles.publicLookup().findVirtual(CLASS_I_MAP, name, mt); + return MethodHandles.publicLookup().findVirtual(clazz, name, mt); } catch (NoSuchMethodException | IllegalAccessException e) { throw new IllegalStateException(e); @@ -266,12 +268,16 @@ private static Class resolveOneOf(String class1, String class2) { return Class.forName(class1); } catch (ClassNotFoundException e) { - try { - return Class.forName(class2); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException(ex); - } + return resolveClass(class2); + } + } + + private static Class resolveClass(String clazz) { + try { + return Class.forName(clazz); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException(e); } } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java index b5f464f215..712ecac813 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java @@ -24,6 +24,7 @@ import org.jooq.impl.DefaultExecuteListener; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.function.Supplier; @@ -91,8 +92,8 @@ private void stopTimerIfStillRunning(ExecuteContext ctx) { if (exception != null) { if (exception instanceof DataAccessException) { DataAccessException dae = (DataAccessException) exception; - exceptionName = dae.sqlStateClass().name().toLowerCase().replace('_', ' '); - exceptionSubclass = dae.sqlStateSubclass().name().toLowerCase().replace('_', ' '); + exceptionName = dae.sqlStateClass().name().toLowerCase(Locale.ROOT).replace('_', ' '); + exceptionSubclass = dae.sqlStateSubclass().name().toLowerCase(Locale.ROOT).replace('_', ' '); if (exceptionSubclass.contains("no subclass")) { exceptionSubclass = "none"; } @@ -107,7 +108,7 @@ private void stopTimerIfStillRunning(ExecuteContext ctx) { sample.stop(Timer.builder("jooq.query") .description("Execution time of a SQL query performed with JOOQ") .tags(queryTags) - .tag("type", ctx.type().name().toLowerCase()) + .tag("type", ctx.type().name().toLowerCase(Locale.ROOT)) .tag("exception", exceptionName) .tag("exception.subclass", exceptionSubclass) .tags(tags) diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcClientObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcClientObservationConvention.java index 99e15375b8..3e21bdeec7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcClientObservationConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcClientObservationConvention.java @@ -19,9 +19,6 @@ import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.binder.grpc.GrpcObservationDocumentation.LowCardinalityKeyNames; -import java.util.ArrayList; -import java.util.List; - /** * Default convention for gRPC client. This class defines how to extract values from * {@link GrpcClientObservationContext}. @@ -31,6 +28,10 @@ */ public class DefaultGrpcClientObservationConvention implements GrpcClientObservationConvention { + private static final KeyValue STATUS_CODE_UNKNOWN = LowCardinalityKeyNames.STATUS_CODE.withValue(UNKNOWN); + + private static final KeyValue PEER_PORT_UNKNOWN = LowCardinalityKeyNames.PEER_PORT.withValue(UNKNOWN); + @Override public String getName() { return "grpc.client"; @@ -43,14 +44,14 @@ public String getContextualName(GrpcClientObservationContext context) { @Override public KeyValues getLowCardinalityKeyValues(GrpcClientObservationContext context) { - List keyValues = new ArrayList<>(); - keyValues.add(LowCardinalityKeyNames.METHOD.withValue(context.getMethodName())); - keyValues.add(LowCardinalityKeyNames.SERVICE.withValue(context.getServiceName())); - keyValues.add(LowCardinalityKeyNames.METHOD_TYPE.withValue(context.getMethodType().name())); - if (context.getStatusCode() != null) { - keyValues.add(LowCardinalityKeyNames.STATUS_CODE.withValue(context.getStatusCode().name())); - } - return KeyValues.of(keyValues); + KeyValue statusCodeKeyValue = context.getStatusCode() != null + ? LowCardinalityKeyNames.STATUS_CODE.withValue(context.getStatusCode().name()) : STATUS_CODE_UNKNOWN; + KeyValue peerPortKeyValue = context.getPeerPort() != null + ? LowCardinalityKeyNames.PEER_PORT.withValue(context.getPeerPort().toString()) : PEER_PORT_UNKNOWN; + return KeyValues.of(statusCodeKeyValue, LowCardinalityKeyNames.PEER_NAME.withValue(context.getPeerName()), + peerPortKeyValue, LowCardinalityKeyNames.METHOD.withValue(context.getMethodName()), + LowCardinalityKeyNames.SERVICE.withValue(context.getServiceName()), + LowCardinalityKeyNames.METHOD_TYPE.withValue(context.getMethodType().name())); } } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcServerObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcServerObservationConvention.java index ba8d9bee3a..b236b23ae2 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcServerObservationConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/DefaultGrpcServerObservationConvention.java @@ -19,9 +19,6 @@ import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.binder.grpc.GrpcObservationDocumentation.LowCardinalityKeyNames; -import java.util.ArrayList; -import java.util.List; - /** * Default convention for gRPC server. This class defines how to extract values from * {@link GrpcServerObservationContext}. @@ -31,6 +28,12 @@ */ public class DefaultGrpcServerObservationConvention implements GrpcServerObservationConvention { + private static final KeyValue STATUS_CODE_UNKNOWN = LowCardinalityKeyNames.STATUS_CODE.withValue(UNKNOWN); + + private static final KeyValue PEER_NAME_UNKNOWN = LowCardinalityKeyNames.PEER_NAME.withValue(UNKNOWN); + + private static final KeyValue PEER_PORT_UNKNOWN = LowCardinalityKeyNames.PEER_PORT.withValue(UNKNOWN); + @Override public String getName() { return "grpc.server"; @@ -43,14 +46,16 @@ public String getContextualName(GrpcServerObservationContext context) { @Override public KeyValues getLowCardinalityKeyValues(GrpcServerObservationContext context) { - List keyValues = new ArrayList<>(); - keyValues.add(LowCardinalityKeyNames.METHOD.withValue(context.getMethodName())); - keyValues.add(LowCardinalityKeyNames.SERVICE.withValue(context.getServiceName())); - keyValues.add(LowCardinalityKeyNames.METHOD_TYPE.withValue(context.getMethodType().name())); - if (context.getStatusCode() != null) { - keyValues.add(LowCardinalityKeyNames.STATUS_CODE.withValue(context.getStatusCode().name())); - } - return KeyValues.of(keyValues); + KeyValue statusCodeKeyValue = context.getStatusCode() != null + ? LowCardinalityKeyNames.STATUS_CODE.withValue(context.getStatusCode().name()) : STATUS_CODE_UNKNOWN; + KeyValue peerNameKeyValue = context.getPeerName() != null + ? LowCardinalityKeyNames.PEER_NAME.withValue(context.getPeerName()) : PEER_NAME_UNKNOWN; + KeyValue peerPortKeyValue = context.getPeerPort() != null + ? LowCardinalityKeyNames.PEER_PORT.withValue(context.getPeerPort().toString()) : PEER_PORT_UNKNOWN; + return KeyValues.of(statusCodeKeyValue, peerNameKeyValue, peerPortKeyValue, + LowCardinalityKeyNames.METHOD.withValue(context.getMethodName()), + LowCardinalityKeyNames.SERVICE.withValue(context.getServiceName()), + LowCardinalityKeyNames.METHOD_TYPE.withValue(context.getMethodType().name())); } } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationContext.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationContext.java index 00f23dd399..d2f7fc8a6b 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationContext.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationContext.java @@ -49,6 +49,11 @@ public class GrpcClientObservationContext extends RequestReplySenderContext setter) { super(setter); } @@ -138,4 +143,21 @@ public void setTrailers(Metadata trailers) { this.trailers = trailers; } + public String getPeerName() { + return this.peerName; + } + + public void setPeerName(String peerName) { + this.peerName = peerName; + } + + @Nullable + public Integer getPeerPort() { + return this.peerPort; + } + + public void setPeerPort(@Nullable Integer peerPort) { + this.peerPort = peerPort; + } + } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationConvention.java index 350b0ec12c..0c429f3674 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcClientObservationConvention.java @@ -26,6 +26,8 @@ */ public interface GrpcClientObservationConvention extends ObservationConvention { + String UNKNOWN = "UNKNOWN"; + @Override default boolean supportsContext(Context context) { return context instanceof GrpcClientObservationContext; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationDocumentation.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationDocumentation.java index 1b7a7d8665..ec5ee8620f 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationDocumentation.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationDocumentation.java @@ -83,6 +83,19 @@ public String asString() { public String asString() { return "grpc.status_code"; } + }, + PEER_NAME { + @Override + public String asString() { + return "net.peer.name"; + } + }, + PEER_PORT { + @Override + public String asString() { + return "net.peer.port"; + } + } } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationContext.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationContext.java index 1643ff0bd5..b086d9814b 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationContext.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationContext.java @@ -52,6 +52,12 @@ public class GrpcServerObservationContext extends RequestReplyReceiverContext getter) { super(getter); } @@ -159,4 +165,22 @@ public void setCancelled(boolean cancelled) { this.cancelled = cancelled; } + @Nullable + public String getPeerName() { + return this.peerName; + } + + public void setPeerName(@Nullable String peerName) { + this.peerName = peerName; + } + + @Nullable + public Integer getPeerPort() { + return this.peerPort; + } + + public void setPeerPort(@Nullable Integer peerPort) { + this.peerPort = peerPort; + } + } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationConvention.java index 87c3e70b86..139a464716 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/GrpcServerObservationConvention.java @@ -26,6 +26,8 @@ */ public interface GrpcServerObservationConvention extends ObservationConvention { + String UNKNOWN = "UNKNOWN"; + @Override default boolean supportsContext(Context context) { return context instanceof GrpcServerObservationContext; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/ObservationGrpcClientInterceptor.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/ObservationGrpcClientInterceptor.java index 75c15e29f9..efd387f700 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/ObservationGrpcClientInterceptor.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/ObservationGrpcClientInterceptor.java @@ -23,6 +23,7 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import java.net.URI; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -80,7 +81,15 @@ public ClientCall interceptCall(MethodDescriptor Listener interceptCall(ServerCall call, } context.setFullMethodName(fullMethodName); context.setMethodType(methodType); - context.setAuthority(call.getAuthority()); - + String authority = call.getAuthority(); + if (authority != null) { + context.setAuthority(authority); + try { + URI uri = new URI(null, authority, null, null, null); + context.setPeerName(uri.getHost()); + context.setPeerPort(uri.getPort()); + } + catch (Exception ex) { + } + } return context; }; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConvention.java index 3415ed297f..dda3ca0431 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConvention.java @@ -27,6 +27,8 @@ import org.apache.hc.core5.http.HttpResponse; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; /** * Default implementation of {@link ApacheHttpClientObservationConvention}. @@ -157,9 +159,26 @@ protected KeyValue targetHost(ApacheHttpClientContext context) { if (httpRoute != null) { return ApacheHttpClientKeyNames.TARGET_HOST.withValue(httpRoute.getTargetHost().getHostName()); } + URI uri = getUri(context); + if (uri != null && uri.getHost() != null) { + return ApacheHttpClientKeyNames.TARGET_HOST.withValue(uri.getHost()); + } return TARGET_HOST_UNKNOWN; } + @Nullable + private static URI getUri(ApacheHttpClientContext context) { + HttpRequest request = context.getCarrier(); + if (request != null) { + try { + return request.getUri(); + } + catch (URISyntaxException ignored) { + } + } + return null; + } + /** * Extract {@code target.port} key value from context. * @param context HTTP client context @@ -172,6 +191,10 @@ protected KeyValue targetPort(ApacheHttpClientContext context) { int port = httpRoute.getTargetHost().getPort(); return ApacheHttpClientKeyNames.TARGET_PORT.withValue(String.valueOf(port)); } + URI uri = getUri(context); + if (uri != null && uri.getPort() != -1) { + return ApacheHttpClientKeyNames.TARGET_PORT.withValue(String.valueOf(uri.getPort())); + } return TARGET_PORT_UNKNOWN; } @@ -186,6 +209,10 @@ protected KeyValue targetScheme(ApacheHttpClientContext context) { if (httpRoute != null) { return ApacheHttpClientKeyNames.TARGET_SCHEME.withValue(httpRoute.getTargetHost().getSchemeName()); } + URI uri = getUri(context); + if (uri != null && uri.getScheme() != null) { + return ApacheHttpClientKeyNames.TARGET_SCHEME.withValue(String.valueOf(uri.getScheme())); + } return TARGET_SCHEME_UNKNOWN; } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java index ba378c850f..643e0da748 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -162,7 +163,7 @@ else if (executionLatency < -1) { private Counter getCounter(HystrixEventType hystrixEventType) { return Counter.builder(NAME_HYSTRIX_EXECUTION) .description(DESCRIPTION_HYSTRIX_EXECUTION) - .tags(Tags.concat(tags, "event", hystrixEventType.name().toLowerCase(), "terminal", + .tags(Tags.concat(tags, "event", hystrixEventType.name().toLowerCase(Locale.ROOT), "terminal", Boolean.toString(hystrixEventType.isTerminal()))) .register(meterRegistry); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java index 2b29b4dc7a..8871c878dd 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java @@ -103,7 +103,7 @@ public static Tag uri(RequestEvent event) { if (matchingPattern == null) { return URI_UNKNOWN; } - else if (matchingPattern.equals("/")) { + if (matchingPattern.equals("/")) { return URI_ROOT; } return Tag.of("uri", matchingPattern); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java index 7548de1e3f..1b9397e624 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java @@ -202,7 +202,7 @@ public void outgoing(Socket socket, ByteBuffer bytes) { } /** - * Configures metrics instrumentation on all the {@link Server}'s {@link Connector}. + * Configures metrics instrumentation on all the {@link Server}'s {@link Connector}s. * @param server apply to this server's connectors * @param registry register metrics to this registry * @param tags add these tags as additional tags on metrics registered via this @@ -230,7 +230,7 @@ public static void addToAllConnectors(Server server, MeterRegistry registry, Ite } /** - * Configures metrics instrumentation on all the {@link Server}'s {@link Connector}. + * Configures metrics instrumentation on all the {@link Server}'s {@link Connector}s. * @param server apply to this server's connectors * @param registry register metrics to this registry */ diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java index 402c193d6a..37af74807c 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java @@ -55,7 +55,7 @@ public void bindTo(MeterRegistry registry) { FunctionCounter.builder("jvm.classes.unloaded", classLoadingBean, ClassLoadingMXBean::getUnloadedClassCount) .tags(tags) - .description("The total number of classes unloaded since the Java virtual machine has started execution") + .description("The number of classes unloaded in the Java virtual machine") .baseUnit(BaseUnits.CLASSES) .register(registry); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java index e810544bd0..b45952b7d0 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java @@ -310,7 +310,8 @@ else if (allowIllegalReflectiveAccess) { if (className.equals("java.util.concurrent.Executors$DelegatedScheduledExecutorService")) { monitor(registry, unwrapThreadPoolExecutor(executorService, executorService.getClass())); } - else if (className.equals("java.util.concurrent.Executors$FinalizableDelegatedExecutorService")) { + else if (className.equals("java.util.concurrent.Executors$FinalizableDelegatedExecutorService") + || className.equals("java.util.concurrent.Executors$AutoShutdownDelegatedExecutorService")) { monitor(registry, unwrapThreadPoolExecutor(executorService, executorService.getClass().getSuperclass())); } @@ -406,9 +407,11 @@ private void monitor(MeterRegistry registry, ForkJoinPool fj) { + "underestimates the actual total number of steals when the pool " + "is not quiescent") .register(registry), - Gauge.builder(metricPrefix + "executor.queued", fj, ForkJoinPool::getQueuedTaskCount) + Gauge + .builder(metricPrefix + "executor.queued", fj, + pool -> pool.getQueuedTaskCount() + pool.getQueuedSubmissionCount()) .tags(tags) - .description("An estimate of the total number of tasks currently held in queues by worker threads") + .description("The approximate number of tasks that are queued for execution") .register(registry), Gauge.builder(metricPrefix + "executor.active", fj, ForkJoinPool::getActiveThreadCount) diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java index acf928fde9..e5bc9c3f6e 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java @@ -28,6 +28,7 @@ import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import java.util.Arrays; +import java.util.Locale; import static java.util.Collections.emptyList; @@ -103,7 +104,7 @@ static long getThreadStateCount(ThreadMXBean threadBean, Thread.State state) { } private static String getStateTagValue(Thread.State state) { - return state.name().toLowerCase().replace("_", "-"); + return state.name().toLowerCase(Locale.ROOT).replace("_", "-"); } } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java index 27c6024966..a56b98c53d 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java @@ -24,6 +24,8 @@ import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.common.Metric; +import java.util.concurrent.ScheduledExecutorService; + /** * Kafka Client metrics binder. This should be closed on application shutdown to clean up * resources. @@ -43,6 +45,21 @@ @NonNullFields public class KafkaClientMetrics extends KafkaMetrics { + /** + * Kafka {@link Producer} metrics binder. The lifecycle of the custom scheduler passed + * is the responsibility of the caller. It will not be shut down when this instance is + * {@link #close() closed}. A scheduler can be shared among multiple instances of + * {@link KafkaClientMetrics} to reduce resource usage by reducing the number of + * threads if there will be many instances. + * @param kafkaProducer producer instance to be instrumented + * @param tags additional tags + * @param scheduler custom scheduler to check and bind metrics + * @since 1.14.0 + */ + public KafkaClientMetrics(Producer kafkaProducer, Iterable tags, ScheduledExecutorService scheduler) { + super(kafkaProducer::metrics, tags, scheduler); + } + /** * Kafka {@link Producer} metrics binder * @param kafkaProducer producer instance to be instrumented @@ -60,6 +77,21 @@ public KafkaClientMetrics(Producer kafkaProducer) { super(kafkaProducer::metrics); } + /** + * Kafka {@link Consumer} metrics binder. The lifecycle of the custom scheduler passed + * is the responsibility of the caller. It will not be shut down when this instance is + * {@link #close() closed}. A scheduler can be shared among multiple instances of + * {@link KafkaClientMetrics} to reduce resource usage by reducing the number of + * threads if there will be many instances. + * @param kafkaConsumer consumer instance to be instrumented + * @param tags additional tags + * @param scheduler custom scheduler to check and bind metrics + * @since 1.14.0 + */ + public KafkaClientMetrics(Consumer kafkaConsumer, Iterable tags, ScheduledExecutorService scheduler) { + super(kafkaConsumer::metrics, tags, scheduler); + } + /** * Kafka {@link Consumer} metrics binder * @param kafkaConsumer consumer instance to be instrumented @@ -77,6 +109,21 @@ public KafkaClientMetrics(Consumer kafkaConsumer) { super(kafkaConsumer::metrics); } + /** + * Kafka {@link AdminClient} metrics binder. The lifecycle of the custom scheduler + * passed is the responsibility of the caller. It will not be shut down when this + * instance is {@link #close() closed}. A scheduler can be shared among multiple + * instances of {@link KafkaClientMetrics} to reduce resource usage by reducing the + * number of threads if there will be many instances. + * @param adminClient instance to be instrumented + * @param tags additional tags + * @param scheduler custom scheduler to check and bind metrics + * @since 1.14.0 + */ + public KafkaClientMetrics(AdminClient adminClient, Iterable tags, ScheduledExecutorService scheduler) { + super(adminClient::metrics, tags, scheduler); + } + /** * Kafka {@link AdminClient} metrics binder * @param adminClient instance to be instrumented diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java index 5e90c7561b..2e998671ec 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java @@ -67,10 +67,11 @@ class KafkaMetrics implements MeterBinder, AutoCloseable { static final String METRIC_GROUP_METRICS_COUNT = "kafka-metrics-count"; static final String VERSION_METRIC_NAME = "version"; static final String START_TIME_METRIC_NAME = "start-time-ms"; - static final Duration DEFAULT_REFRESH_INTERVAL = Duration.ofSeconds(60); static final String KAFKA_VERSION_TAG_NAME = "kafka.version"; static final String DEFAULT_VALUE = "unknown"; + private static final long REFRESH_INTERVAL_MILLIS = Duration.ofSeconds(60).toMillis(); + private static final Set> counterMeasurableClasses = new HashSet<>(); static { @@ -94,10 +95,9 @@ class KafkaMetrics implements MeterBinder, AutoCloseable { private final Iterable extraTags; - private final Duration refreshInterval; + private final ScheduledExecutorService scheduler; - private final ScheduledExecutorService scheduler = Executors - .newSingleThreadScheduledExecutor(new NamedThreadFactory("micrometer-kafka-metrics")); + private final boolean schedulerExternallyManaged; @Nullable private Iterable commonTags; @@ -119,14 +119,20 @@ class KafkaMetrics implements MeterBinder, AutoCloseable { } KafkaMetrics(Supplier> metricsSupplier, Iterable extraTags) { - this(metricsSupplier, extraTags, DEFAULT_REFRESH_INTERVAL); + this(metricsSupplier, extraTags, createDefaultScheduler(), false); } KafkaMetrics(Supplier> metricsSupplier, Iterable extraTags, - Duration refreshInterval) { + ScheduledExecutorService scheduler) { + this(metricsSupplier, extraTags, scheduler, true); + } + + KafkaMetrics(Supplier> metricsSupplier, Iterable extraTags, + ScheduledExecutorService scheduler, boolean schedulerExternallyManaged) { this.metricsSupplier = metricsSupplier; this.extraTags = extraTags; - this.refreshInterval = refreshInterval; + this.scheduler = scheduler; + this.schedulerExternallyManaged = schedulerExternallyManaged; } @Override @@ -136,8 +142,8 @@ public void bindTo(MeterRegistry registry) { commonTags = getCommonTags(registry); prepareToBindMetrics(registry); checkAndBindMetrics(registry); - scheduler.scheduleAtFixedRate(() -> checkAndBindMetrics(registry), getRefreshIntervalInMillis(), - getRefreshIntervalInMillis(), TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(() -> checkAndBindMetrics(registry), REFRESH_INTERVAL_MILLIS, + REFRESH_INTERVAL_MILLIS, TimeUnit.MILLISECONDS); } private Iterable getCommonTags(MeterRegistry registry) { @@ -173,10 +179,6 @@ else if (START_TIME_METRIC_NAME.equals(name.name())) { } } - private long getRefreshIntervalInMillis() { - return refreshInterval.toMillis(); - } - /** * Gather metrics from Kafka metrics API and register Meters. *

@@ -295,6 +297,10 @@ private static Class getMeasurableClass(Metric metric) { } } + private static ScheduledExecutorService createDefaultScheduler() { + return Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("micrometer-kafka-metrics")); + } + private Gauge registerGauge(MeterRegistry registry, MetricName metricName, String meterName, Iterable tags) { return Gauge.builder(meterName, this.metrics, toMetricValue(metricName)) .tags(tags) @@ -344,7 +350,9 @@ private Meter.Id meterIdForComparison(MetricName metricName) { @Override public void close() { - this.scheduler.shutdownNow(); + if (!schedulerExternallyManaged) { + this.scheduler.shutdownNow(); + } for (Meter.Id id : registeredMeterIds) { registry.remove(id); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java index 3f0f7d569a..07ff3ccb69 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java @@ -22,6 +22,8 @@ import org.apache.kafka.common.Metric; import org.apache.kafka.streams.KafkaStreams; +import java.util.concurrent.ScheduledExecutorService; + /** * Kafka Streams metrics binder. This should be closed on application shutdown to clean up * resources. @@ -58,4 +60,19 @@ public KafkaStreamsMetrics(KafkaStreams kafkaStreams) { super(kafkaStreams::metrics); } + /** + * {@link KafkaStreams} metrics binder. The lifecycle of the custom scheduler passed + * is the responsibility of the caller. It will not be shut down when this instance is + * {@link #close() closed}. A scheduler can be shared among multiple instances of + * {@link KafkaStreamsMetrics} to reduce resource usage by reducing the number of + * threads if there will be many instances. + * @param kafkaStreams instance to be instrumented + * @param tags additional tags + * @param scheduler customer scheduler to run the task that checks and binds metrics + * @since 1.14.0 + */ + public KafkaStreamsMetrics(KafkaStreams kafkaStreams, Iterable tags, ScheduledExecutorService scheduler) { + super(kafkaStreams::metrics, tags, scheduler); + } + } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java index 166b424db9..98e9b772f5 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java @@ -31,9 +31,9 @@ import org.apache.logging.log4j.core.filter.AbstractFilter; import org.apache.logging.log4j.core.filter.CompositeFilter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import static java.util.Collections.emptyList; @@ -61,7 +61,7 @@ public class Log4j2Metrics implements MeterBinder, AutoCloseable { private final LoggerContext loggerContext; - private final List metricsFilters = new ArrayList<>(); + private final ConcurrentMap metricsFilters = new ConcurrentHashMap<>(); public Log4j2Metrics() { this(emptyList()); @@ -80,7 +80,7 @@ public Log4j2Metrics(Iterable tags, LoggerContext loggerContext) { public void bindTo(MeterRegistry registry) { Configuration configuration = loggerContext.getConfiguration(); LoggerConfig rootLoggerConfig = configuration.getRootLogger(); - rootLoggerConfig.addFilter(createMetricsFilterAndStart(registry)); + rootLoggerConfig.addFilter(getOrCreateMetricsFilterAndStart(registry)); loggerContext.getConfiguration() .getLoggers() @@ -102,17 +102,18 @@ public void bindTo(MeterRegistry registry) { if (logFilter instanceof MetricsFilter) { return; } - loggerConfig.addFilter(createMetricsFilterAndStart(registry)); + loggerConfig.addFilter(getOrCreateMetricsFilterAndStart(registry)); }); loggerContext.updateLoggers(configuration); } - private MetricsFilter createMetricsFilterAndStart(MeterRegistry registry) { - MetricsFilter metricsFilter = new MetricsFilter(registry, tags); - metricsFilter.start(); - metricsFilters.add(metricsFilter); - return metricsFilter; + private MetricsFilter getOrCreateMetricsFilterAndStart(MeterRegistry registry) { + return metricsFilters.computeIfAbsent(registry, r -> { + MetricsFilter metricsFilter = new MetricsFilter(r, tags); + metricsFilter.start(); + return metricsFilter; + }); } @Override @@ -120,7 +121,7 @@ public void close() { if (!metricsFilters.isEmpty()) { Configuration configuration = loggerContext.getConfiguration(); LoggerConfig rootLoggerConfig = configuration.getRootLogger(); - metricsFilters.forEach(rootLoggerConfig::removeFilter); + metricsFilters.values().forEach(rootLoggerConfig::removeFilter); loggerContext.getConfiguration() .getLoggers() @@ -129,12 +130,13 @@ public void close() { .filter(loggerConfig -> !loggerConfig.isAdditive()) .forEach(loggerConfig -> { if (loggerConfig != rootLoggerConfig) { - metricsFilters.forEach(loggerConfig::removeFilter); + metricsFilters.values().forEach(loggerConfig::removeFilter); } }); loggerContext.updateLoggers(configuration); - metricsFilters.forEach(MetricsFilter::stop); + metricsFilters.values().forEach(MetricsFilter::stop); + metricsFilters.clear(); } } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/DefaultOkHttpObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/DefaultOkHttpObservationConvention.java index a4756b4235..0ef6d36bfd 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/DefaultOkHttpObservationConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/DefaultOkHttpObservationConvention.java @@ -88,7 +88,7 @@ public KeyValues getLowCardinalityKeyValues(OkHttpContext context) { // TODO: Tags to key values and back - maybe we can improve this? KeyValues keyValues = KeyValues .of(METHOD.withValue(requestAvailable ? request.method() : TAG_VALUE_UNKNOWN), - URI.withValue(getUriTag(urlMapper, state, request)), + URI.withValue(getUriTag(urlMapper, request)), STATUS.withValue(getStatusMessage(state.response, state.exception)), OUTCOME.withValue(getStatusOutcome(state.response).name())) .and(extraTags) @@ -105,13 +105,11 @@ public KeyValues getLowCardinalityKeyValues(OkHttpContext context) { return keyValues; } - private String getUriTag(Function urlMapper, OkHttpObservationInterceptor.CallState state, - @Nullable Request request) { + private String getUriTag(Function urlMapper, @Nullable Request request) { if (request == null) { return TAG_VALUE_UNKNOWN; } - return state.response != null && (state.response.code() == 404 || state.response.code() == 301) ? "NOT_FOUND" - : urlMapper.apply(request); + return urlMapper.apply(request); } private Outcome getStatusOutcome(@Nullable Response response) { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java index 8b1809303d..c19ff9d6df 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java @@ -167,8 +167,8 @@ void time(CallState state) { boolean requestAvailable = request != null; Iterable tags = Tags - .of("method", requestAvailable ? request.method() : TAG_VALUE_UNKNOWN, "uri", getUriTag(state, request), - "status", getStatusMessage(state.response, state.exception)) + .of("method", requestAvailable ? request.method() : TAG_VALUE_UNKNOWN, "uri", getUriTag(request), "status", + getStatusMessage(state.response, state.exception)) .and(getStatusOutcome(state.response).asTag()) .and(extraTags) .and(stream(contextSpecificTags.spliterator(), false) @@ -196,12 +196,11 @@ private Tags generateTagsForRoute(@Nullable Request request) { TAG_TARGET_PORT, Integer.toString(request.url().port())); } - private String getUriTag(CallState state, @Nullable Request request) { + private String getUriTag(@Nullable Request request) { if (request == null) { return TAG_VALUE_UNKNOWN; } - return state.response != null && (state.response.code() == 404 || state.response.code() == 301) ? "NOT_FOUND" - : urlMapper.apply(request); + return urlMapper.apply(request); } private Iterable getRequestTags(@Nullable Request request) { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/InvalidConfigurationException.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/InvalidConfigurationException.java index 69e4d7700b..dddd699596 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/InvalidConfigurationException.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/InvalidConfigurationException.java @@ -15,6 +15,8 @@ */ package io.micrometer.core.instrument.config; +import io.micrometer.common.lang.Nullable; + /** * Signals that a piece of provided configuration is not acceptable for some reason. For * example negative SLO boundaries. @@ -31,12 +33,12 @@ public class InvalidConfigurationException extends IllegalStateException { * indicates that the cause is nonexistent or unknown.) * @since 1.11.9 */ - public InvalidConfigurationException(String message, Throwable cause) { + public InvalidConfigurationException(String message, @Nullable Throwable cause) { super(message, cause); } - public InvalidConfigurationException(String s) { - super(s); + public InvalidConfigurationException(String message) { + super(message); } } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java index 209e0accb8..ab4b7be0d4 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/NamingConvention.java @@ -109,7 +109,7 @@ public String tagKey(String key) { } private String capitalize(String name) { - if (name.length() == 0 || Character.isUpperCase(name.charAt(0))) { + if (name.isEmpty() || Character.isUpperCase(name.charAt(0))) { return name; } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/validate/DurationValidator.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/validate/DurationValidator.java index 1608e54391..b0172f63b0 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/validate/DurationValidator.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/validate/DurationValidator.java @@ -23,6 +23,7 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,7 +45,7 @@ public enum DurationValidator { "^\\s*([\\+]?\\d{0,3}([_,]?\\d{3})*(\\.\\d*)?)\\s*([a-zA-Z]{0,2})\\s*") { @Override protected Validated doParse(String property, String value) { - Matcher matcher = patterns.get(0).matcher(value.toLowerCase().replaceAll("[,_\\s]", "")); + Matcher matcher = patterns.get(0).matcher(value.toLowerCase(Locale.ROOT).replaceAll("[,_\\s]", "")); if (!matcher.matches()) { return Validated.invalid(property, value, "must be a valid duration", InvalidReason.MALFORMED); } @@ -143,7 +144,7 @@ public static Validated validateChronoUnit(String property, @Nullabl return Validated.valid(property, null); } - switch (unit.toLowerCase()) { + switch (unit.toLowerCase(Locale.ROOT)) { case "ns": case "nanoseconds": case "nanosecond": diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogram.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogram.java index d740c3a21f..bbbc5a06a9 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogram.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogram.java @@ -29,8 +29,8 @@ class FixedBoundaryHistogram { /** * Creates a FixedBoundaryHistogram which tracks the count of values for each bucket - * bound). - * @param buckets - sorted bucket boundaries + * bound. + * @param buckets sorted bucket boundaries * @param isCumulativeBucketCounts - whether the count values should be cumulative * count of lower buckets and current bucket. */ @@ -46,10 +46,10 @@ class FixedBoundaryHistogram { /** * Returns the number of values that was recorded between previous bucket and the - * queried bucket (upper bound inclusive) + * queried bucket (upper bound inclusive). * @param bucket - the bucket to find values for * @return 0 if bucket is not a valid bucket otherwise number of values recorded - * between (index(bucket) - 1, bucket] + * between (previous bucket, bucket] */ private long countAtBucket(double bucket) { int index = Arrays.binarySearch(buckets, bucket); @@ -75,7 +75,7 @@ void record(long value) { * valueToRecord is greater than the highest bucket. */ // VisibleForTesting - int leastLessThanOrEqualTo(long valueToRecord) { + int leastLessThanOrEqualTo(double valueToRecord) { int low = 0; int high = buckets.length - 1; @@ -121,7 +121,7 @@ public CountAtBucket next() { * Returns the list of {@link CountAtBucket} for each of the buckets tracked by this * histogram. */ - CountAtBucket[] getCountsAtBucket() { + CountAtBucket[] getCountAtBuckets() { CountAtBucket[] countAtBuckets = new CountAtBucket[this.buckets.length]; long cumulativeCount = 0; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/StepBucketHistogram.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/StepBucketHistogram.java index 5ce563def1..e81725339e 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/StepBucketHistogram.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/distribution/StepBucketHistogram.java @@ -64,7 +64,7 @@ protected Supplier valueSupplier() { return () -> { CountAtBucket[] countAtBuckets; synchronized (fixedBoundaryHistogram) { - countAtBuckets = fixedBoundaryHistogram.getCountsAtBucket(); + countAtBuckets = fixedBoundaryHistogram.getCountAtBuckets(); fixedBoundaryHistogram.reset(); } return countAtBuckets; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/logging/LoggingMeterRegistry.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/logging/LoggingMeterRegistry.java index 6cbea5b75b..3387642151 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/logging/LoggingMeterRegistry.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/logging/LoggingMeterRegistry.java @@ -42,6 +42,7 @@ import java.util.stream.StreamSupport; import static io.micrometer.core.instrument.util.DoubleFormat.decimalOrNan; +import static io.micrometer.core.instrument.util.DoubleFormat.wholeOrDecimal; import static java.util.stream.Collectors.joining; /** @@ -49,6 +50,7 @@ * * @author Jon Schneider * @author Matthieu Borgraeve + * @author Francois Staudt * @since 1.1.0 */ @Incubating(since = "1.1.0") @@ -126,22 +128,24 @@ protected void publish() { double count = counter.count(); if (!config.logInactive() && count == 0) return; - loggingSink.accept(print.id() + " throughput=" + print.rate(count)); + loggingSink.accept(print.id() + " delta_count=" + print.humanReadableBaseUnit(count) + + " throughput=" + print.rate(count)); }, timer -> { HistogramSnapshot snapshot = timer.takeSnapshot(); long count = snapshot.count(); if (!config.logInactive() && count == 0) return; - loggingSink.accept(print.id() + " throughput=" + print.unitlessRate(count) + " mean=" - + print.time(snapshot.mean(getBaseTimeUnit())) + " max=" - + print.time(snapshot.max(getBaseTimeUnit()))); + loggingSink.accept(print.id() + " delta_count=" + wholeOrDecimal(count) + " throughput=" + + print.unitlessRate(count) + " mean=" + print.time(snapshot.mean(getBaseTimeUnit())) + + " max=" + print.time(snapshot.max(getBaseTimeUnit()))); }, summary -> { HistogramSnapshot snapshot = summary.takeSnapshot(); long count = snapshot.count(); if (!config.logInactive() && count == 0) return; - loggingSink.accept(print.id() + " throughput=" + print.unitlessRate(count) + " mean=" - + print.value(snapshot.mean()) + " max=" + print.value(snapshot.max())); + loggingSink.accept(print.id() + " delta_count=" + wholeOrDecimal(count) + " throughput=" + + print.unitlessRate(count) + " mean=" + print.value(snapshot.mean()) + " max=" + + print.value(snapshot.max())); }, longTaskTimer -> { int activeTasks = longTaskTimer.activeTasks(); if (!config.logInactive() && activeTasks == 0) @@ -157,13 +161,14 @@ protected void publish() { double count = counter.count(); if (!config.logInactive() && count == 0) return; - loggingSink.accept(print.id() + " throughput=" + print.rate(count)); + loggingSink.accept(print.id() + " delta_count=" + print.humanReadableBaseUnit(count) + + " throughput=" + print.rate(count)); }, timer -> { double count = timer.count(); if (!config.logInactive() && count == 0) return; - loggingSink.accept(print.id() + " throughput=" + print.rate(count) + " mean=" - + print.time(timer.mean(getBaseTimeUnit()))); + loggingSink.accept(print.id() + " delta_count=" + wholeOrDecimal(count) + " throughput=" + + print.unitlessRate(count) + " mean=" + print.time(timer.mean(getBaseTimeUnit()))); }, meter -> loggingSink.accept(writeMeter(meter, print))); }); } @@ -181,7 +186,8 @@ String writeMeter(Meter meter, Printer print) { case DURATION: return msLine + print.time(ms.getValue()); case COUNT: - return "throughput=" + print.rate(ms.getValue()); + return "delta_count=" + print.humanReadableBaseUnit(ms.getValue()) + ", throughput=" + + print.rate(ms.getValue()); default: return msLine + decimalOrNan(ms.getValue()); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java index 213d915b82..98d85fe790 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/simple/SimpleMeterRegistry.java @@ -28,6 +28,7 @@ import io.micrometer.core.instrument.step.*; import java.util.Comparator; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.function.ToDoubleFunction; import java.util.function.ToLongFunction; @@ -203,7 +204,7 @@ private String toString(Tag tag) { private String toString(Measurement measurement, String meterUnitSuffix) { Statistic statistic = measurement.getStatistic(); - return String.format("%s=%s%s", statistic.toString().toLowerCase(), measurement.getValue(), + return String.format("%s=%s%s", statistic.toString().toLowerCase(Locale.ROOT), measurement.getValue(), getUnitSuffix(statistic, meterUnitSuffix)); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/util/TimeUtils.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/util/TimeUtils.java index 7ec63516f7..779bfa8d7c 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/util/TimeUtils.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/util/TimeUtils.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -218,7 +219,7 @@ public static double daysToUnit(double days, TimeUnit destinationUnit) { */ @Deprecated public static Duration simpleParse(String time) { - String timeLower = PARSE_PATTERN.matcher(time.toLowerCase()).replaceAll(""); + String timeLower = PARSE_PATTERN.matcher(time.toLowerCase(Locale.ROOT)).replaceAll(""); if (timeLower.endsWith("ns")) { return Duration.ofNanos(Long.parseLong(timeLower.substring(0, timeLower.length() - 2))); } diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java index f0b4901c7a..a0de050c31 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/DefaultHttpClientObservationConvention.java @@ -45,29 +45,36 @@ public KeyValues getLowCardinalityKeyValues(HttpClientContext context) { return KeyValues.empty(); } HttpRequest httpRequest = context.getCarrier().build(); - KeyValues keyValues = KeyValues.of( + return KeyValues.of( HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.withValue(httpRequest.method()), HttpClientObservationDocumentation.LowCardinalityKeys.URI - .withValue(getUriTag(httpRequest, context.getResponse(), context.getUriMapper()))); - if (context.getResponse() != null) { - keyValues = keyValues - .and(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS - .withValue(String.valueOf(context.getResponse().statusCode()))) - .and(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME - .withValue(Outcome.forStatus(context.getResponse().statusCode()).name())); - } - return keyValues; + .withValue(getUriTag(httpRequest, context.getResponse(), context.getUriMapper())), + HttpClientObservationDocumentation.LowCardinalityKeys.STATUS + .withValue(getStatus(context.getResponse())), + HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME + .withValue(getOutcome(context.getResponse()))); } - String getUriTag(@Nullable HttpRequest request, @Nullable HttpResponse httpResponse, + String getUriTag(HttpRequest request, @Nullable HttpResponse httpResponse, Function uriMapper) { - if (request == null) { - return null; - } return httpResponse != null && (httpResponse.statusCode() == 404 || httpResponse.statusCode() == 301) ? "NOT_FOUND" : uriMapper.apply(request); } + String getStatus(@Nullable HttpResponse response) { + if (response == null) { + return "UNKNOWN"; + } + return String.valueOf(response.statusCode()); + } + + String getOutcome(@Nullable HttpResponse response) { + if (response == null) { + return Outcome.UNKNOWN.name(); + } + return Outcome.forStatus(response.statusCode()).name(); + } + @Override @NonNull public String getName() { diff --git a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java index 178ea68cb7..f28c1afe9e 100644 --- a/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java +++ b/micrometer-core/src/main/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClient.java @@ -17,9 +17,7 @@ import io.micrometer.common.lang.Nullable; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.binder.http.Outcome; import io.micrometer.core.instrument.observation.ObservationOrTimerCompatibleInstrumentation; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; @@ -36,6 +34,7 @@ import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; import java.util.function.Function; @@ -236,19 +235,13 @@ private void stopObservationOrTimer( ObservationOrTimerCompatibleInstrumentation instrumentation, HttpRequest request, @Nullable HttpResponse res) { instrumentation.stop(DefaultHttpClientObservationConvention.INSTANCE.getName(), "Timer for JDK's HttpClient", - () -> { - Tags tags = Tags.of(HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.asString(), - request.method(), HttpClientObservationDocumentation.LowCardinalityKeys.URI.asString(), - DefaultHttpClientObservationConvention.INSTANCE.getUriTag(request, res, uriMapper)); - if (res != null) { - tags = tags - .and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(), - String.valueOf(res.statusCode()))) - .and(Tag.of(HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME.asString(), - Outcome.forStatus(res.statusCode()).name())); - } - return tags; - }); + () -> Tags.of(HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.asString(), request.method(), + HttpClientObservationDocumentation.LowCardinalityKeys.URI.asString(), + DefaultHttpClientObservationConvention.INSTANCE.getUriTag(request, res, uriMapper), + HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(), + DefaultHttpClientObservationConvention.INSTANCE.getStatus(res), + HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME.asString(), + DefaultHttpClientObservationConvention.INSTANCE.getOutcome(res))); } private ObservationOrTimerCompatibleInstrumentation observationOrTimer( @@ -274,12 +267,16 @@ public CompletableFuture> sendAsync(HttpRequest httpRequest, httpRequestBuilder); HttpRequest request = httpRequestBuilder.build(); return client.sendAsync(request, bodyHandler, pushPromiseHandler).handle((response, throwable) -> { + instrumentation.setResponse(response); if (throwable != null) { instrumentation.setThrowable(throwable); + stopObservationOrTimer(instrumentation, request, response); + throw new CompletionException(throwable); + } + else { + stopObservationOrTimer(instrumentation, request, response); + return response; } - instrumentation.setResponse(response); - stopObservationOrTimer(instrumentation, request, response); - return response; }); } diff --git a/micrometer-core/src/main/resources/META-INF/native-image/io.micrometer/micrometer-core/reflect-config.json b/micrometer-core/src/main/resources/META-INF/native-image/io.micrometer/micrometer-core/reflect-config.json index 01c1060f64..9b3a95b4ac 100644 --- a/micrometer-core/src/main/resources/META-INF/native-image/io.micrometer/micrometer-core/reflect-config.json +++ b/micrometer-core/src/main/resources/META-INF/native-image/io.micrometer/micrometer-core/reflect-config.json @@ -27,5 +27,71 @@ { "name":"org.HdrHistogram.Histogram", "methods":[{"name":"","parameterTypes":["long","long","int"] }] + }, + { + "name":"com.hazelcast.core.DistributedObject", + "methods":[{"name":"getName","parameterTypes":[] }] + }, + { + "name":"com.hazelcast.map.IMap", + "methods":[{"name":"getLocalMapStats","parameterTypes":[] }] + }, + { + "name":"com.hazelcast.core.IMap", + "methods":[{"name":"getLocalMapStats","parameterTypes":[] }] + }, + { + "name":"com.hazelcast.map.LocalMapStats", + "methods":[ + {"name":"getNearCacheStats","parameterTypes":[] }, + {"name":"getOwnedEntryCount","parameterTypes":[] }, + {"name":"getHits","parameterTypes":[] }, + {"name":"getPutOperationCount","parameterTypes":[] }, + {"name":"getSetOperationCount","parameterTypes":[] }, + {"name":"getBackupEntryCount","parameterTypes":[] }, + {"name":"getBackupEntryMemoryCost","parameterTypes":[] }, + {"name":"getOwnedEntryMemoryCost","parameterTypes":[] }, + {"name":"getGetOperationCount","parameterTypes":[] }, + {"name":"getTotalGetLatency","parameterTypes":[] }, + {"name":"getTotalPutLatency","parameterTypes":[] }, + {"name":"getRemoveOperationCount","parameterTypes":[] }, + {"name":"getTotalRemoveLatency","parameterTypes":[] } + ] + }, + { + "name":"com.hazelcast.monitor.LocalMapStats", + "methods":[ + {"name":"getNearCacheStats","parameterTypes":[] }, + {"name":"getOwnedEntryCount","parameterTypes":[] }, + {"name":"getHits","parameterTypes":[] }, + {"name":"getPutOperationCount","parameterTypes":[] }, + {"name":"getSetOperationCount","parameterTypes":[] }, + {"name":"getBackupEntryCount","parameterTypes":[] }, + {"name":"getBackupEntryMemoryCost","parameterTypes":[] }, + {"name":"getOwnedEntryMemoryCost","parameterTypes":[] }, + {"name":"getGetOperationCount","parameterTypes":[] }, + {"name":"getTotalGetLatency","parameterTypes":[] }, + {"name":"getTotalPutLatency","parameterTypes":[] }, + {"name":"getRemoveOperationCount","parameterTypes":[] }, + {"name":"getTotalRemoveLatency","parameterTypes":[] } + ] + }, + { + "name":"com.hazelcast.nearcache.NearCacheStats", + "methods":[ + {"name":"getHits","parameterTypes":[] }, + {"name":"getMisses","parameterTypes":[] }, + {"name":"getEvictions","parameterTypes":[] }, + {"name":"getPersistenceCount","parameterTypes":[] } + ] + }, + { + "name":"com.hazelcast.monitor.NearCacheStats", + "methods":[ + {"name":"getHits","parameterTypes":[] }, + {"name":"getMisses","parameterTypes":[] }, + {"name":"getEvictions","parameterTypes":[] }, + {"name":"getPersistenceCount","parameterTypes":[] } + ] } ] diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/MeterRegistryTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/MeterRegistryTest.java index 25246d662b..678583b234 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/MeterRegistryTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/MeterRegistryTest.java @@ -287,10 +287,12 @@ void differentPreFilterIdsMapToSameId_thenCacheIsBounded() { // to the map because it would result in a memory leak with a high cardinality tag // that's otherwise limited in cardinality by a MeterFilter assertThat(registry._getPreFilterIdToMeterMap()).hasSize(1); + assertThat(registry._getMeterToPreFilterIdMap()).hasSize(1); assertThat(registry.remove(c1)).isSameAs(c2); assertThat(registry.getMeters()).isEmpty(); assertThat(registry._getPreFilterIdToMeterMap()).isEmpty(); + assertThat(registry._getMeterToPreFilterIdMap()).isEmpty(); } @Test @@ -318,6 +320,7 @@ void removingStaleMeterRemovesItFromAllInternalState() { registry.remove(c1.getId()); assertThat(registry.getMeters()).isEmpty(); assertThat(registry._getPreFilterIdToMeterMap()).isEmpty(); + assertThat(registry._getMeterToPreFilterIdMap()).isEmpty(); assertThat(registry._getStalePreFilterIds()).isEmpty(); } @@ -331,6 +334,8 @@ void multiplePreFilterIdsMapToSameId_removeByPreFilterId() { Meter.Id preFilterId = new Meter.Id("counter", Tags.of("secret", "value2"), null, null, Meter.Type.COUNTER); assertThat(registry.removeByPreFilterId(preFilterId)).isSameAs(c1).isSameAs(c2); assertThat(registry.getMeters()).isEmpty(); + assertThat(registry._getPreFilterIdToMeterMap()).isEmpty(); + assertThat(registry._getMeterToPreFilterIdMap()).isEmpty(); } @Test diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/MultiGaugeTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/MultiGaugeTest.java index d080c217d2..5ea202712a 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/MultiGaugeTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/MultiGaugeTest.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -93,6 +94,14 @@ void rowGaugesHoldStrongReferences() { assertThat(registry.get("colors").tag("color", "red").gauge().value()).isEqualTo(1); } + @Test + void rowGaugesCanTakeSubClassOfNumberSuppliers() { + final Supplier supplier = () -> 1L; + colorGauges.register(Collections.singletonList(Row.of(Tags.of("color", "red"), supplier))); + + assertThat(registry.get("colors").tag("color", "red").gauge().value()).isEqualTo(1); + } + @Test void overwrite() { testOverwrite(); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounterTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounterTest.java index 358969bd7c..b10fb495a6 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounterTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounterTest.java @@ -94,7 +94,7 @@ void loadFailure() { } @ParameterizedTest - @EnumSource(RemovalCause.class) + @EnumSource void evictionWithCause(RemovalCause cause) { stats.recordEviction(3, cause); DistributionSummary summary = fetch("cache.evictions", "cause", cause.name()).summary(); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationTest.java index d15752a19e..84fe509575 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/grpc/GrpcObservationTest.java @@ -65,6 +65,7 @@ import io.micrometer.observation.ObservationHandler; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationTextPublisher; +import io.micrometer.observation.tck.ObservationContextAssert; import io.micrometer.observation.tck.TestObservationRegistry; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -169,9 +170,13 @@ void unaryRpc() { assertThat(clientHandler.getEvents()).containsExactly(GrpcClientEvents.MESSAGE_SENT, GrpcClientEvents.MESSAGE_RECEIVED); // tag::assertion[] - assertThat(observationRegistry) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.client")) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server")); + assertThat(observationRegistry).hasAnObservation(observationContextAssert -> { + observationContextAssert.hasNameEqualTo("grpc.client"); + assertCommonKeyValueNames(observationContextAssert); + }).hasAnObservation(observationContextAssert -> { + observationContextAssert.hasNameEqualTo("grpc.server"); + assertCommonKeyValueNames(observationContextAssert); + }); // end::assertion[] verifyHeaders(); } @@ -204,9 +209,7 @@ public void onFailure(Throwable t) { await().until(() -> futures.stream().allMatch(Future::isDone)); assertThat(responses).hasSize(count).containsExactlyInAnyOrderElementsOf(messages); - assertThat(observationRegistry) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.client")) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server")); + assertClientAndServerObservations(); verifyHeaders(); } @@ -247,9 +250,7 @@ void clientStreamingRpc() { verifyServerContext("grpc.testing.SimpleService", "ClientStreamingRpc", "grpc.testing.SimpleService/ClientStreamingRpc", MethodType.CLIENT_STREAMING); assertThat(serverHandler.getContext().getStatusCode()).isEqualTo(Code.OK); - assertThat(observationRegistry) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.client")) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server")); + assertClientAndServerObservations(); verifyHeaders(); } @@ -282,9 +283,7 @@ void serverStreamingRpc() { assertThat(clientHandler.getContext().getStatusCode()).isEqualTo(Code.OK); assertThat(clientHandler.getEvents()).containsExactly(GrpcClientEvents.MESSAGE_SENT, GrpcClientEvents.MESSAGE_RECEIVED, GrpcClientEvents.MESSAGE_RECEIVED); - assertThat(observationRegistry) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.client")) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server")); + assertClientAndServerObservations(); verifyHeaders(); } @@ -335,9 +334,7 @@ void bidiStreamingRpc() { assertThat(serverHandler.getContext().getStatusCode()).isEqualTo(Code.OK); assertThat(clientHandler.getContext().getStatusCode()).isEqualTo(Code.OK); - assertThat(observationRegistry) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.client")) - .hasAnObservation(observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server")); + assertClientAndServerObservations(); verifyHeaders(); } @@ -374,6 +371,16 @@ private void verifyHeaders() { } + private void assertClientAndServerObservations() { + assertThat(observationRegistry).hasAnObservation(observationContextAssert -> { + observationContextAssert.hasNameEqualTo("grpc.client"); + assertCommonKeyValueNames(observationContextAssert); + }).hasAnObservation(observationContextAssert -> { + observationContextAssert.hasNameEqualTo("grpc.server"); + assertCommonKeyValueNames(observationContextAssert); + }); + } + @Nested class WithExceptionService { @@ -407,8 +414,7 @@ void unaryRpcFailure() { assertThat(clientHandler.getContext().getStatusCode()).isEqualTo(Code.UNKNOWN); assertThat(serverHandler.getEvents()).containsExactly(GrpcServerEvents.MESSAGE_RECEIVED); assertThat(clientHandler.getEvents()).containsExactly(GrpcClientEvents.MESSAGE_SENT); - assertThat(observationRegistry).hasAnObservation( - observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server").hasError()); + assertServerErrorObservation(); } @Test @@ -430,8 +436,7 @@ void clientStreamingRpcFailure() { assertThat(serverHandler.getContext().getStatusCode()).isNull(); assertThat(clientHandler.getEvents()).isEmpty(); assertThat(serverHandler.getEvents()).isEmpty(); - assertThat(observationRegistry).hasAnObservation( - observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server").hasError()); + assertServerErrorObservation(); } @Test @@ -455,8 +460,7 @@ void serverStreamingRpcFailure() { assertThat(serverHandler.getContext().getStatusCode()).isNull(); assertThat(clientHandler.getEvents()).containsExactly(GrpcClientEvents.MESSAGE_SENT); assertThat(serverHandler.getEvents()).containsExactly(GrpcServerEvents.MESSAGE_RECEIVED); - assertThat(observationRegistry).hasAnObservation( - observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server").hasError()); + assertServerErrorObservation(); } @Test @@ -479,8 +483,14 @@ void bidiStreamingRpcFailure() { assertThat(serverHandler.getContext().getStatusCode()).isNull(); assertThat(clientHandler.getEvents()).isEmpty(); assertThat(serverHandler.getEvents()).isEmpty(); - assertThat(observationRegistry).hasAnObservation( - observationContextAssert -> observationContextAssert.hasNameEqualTo("grpc.server").hasError()); + assertServerErrorObservation(); + } + + private void assertServerErrorObservation() { + assertThat(observationRegistry).hasAnObservation(observationContextAssert -> { + observationContextAssert.hasNameEqualTo("grpc.server").hasError(); + assertCommonKeyValueNames(observationContextAssert); + }); } private StreamObserver createResponseObserver(AtomicBoolean errored) { @@ -605,6 +615,8 @@ void verifyServerContext(String serviceName, String methodName, String contextua assertThat(serverContext.getFullMethodName()).isEqualTo(contextualName); assertThat(serverContext.getMethodType()).isEqualTo(methodType); assertThat(serverContext.getAuthority()).isEqualTo("localhost"); + assertThat(serverContext.getPeerName()).isEqualTo("localhost"); + assertThat(serverContext.getPeerPort()).isEqualTo(-1); }); } @@ -617,9 +629,21 @@ void verifyClientContext(String serviceName, String methodName, String contextua assertThat(clientContext.getFullMethodName()).isEqualTo(contextualName); assertThat(clientContext.getMethodType()).isEqualTo(methodType); assertThat(clientContext.getAuthority()).isEqualTo("localhost"); + assertThat(clientContext.getPeerName()).isEqualTo("localhost"); + assertThat(clientContext.getPeerPort()).isEqualTo(-1); }); } + void assertCommonKeyValueNames(ObservationContextAssert observationContextAssert) { + observationContextAssert + .hasLowCardinalityKeyValueWithKey(GrpcObservationDocumentation.LowCardinalityKeyNames.METHOD.asString()) + .hasLowCardinalityKeyValueWithKey( + GrpcObservationDocumentation.LowCardinalityKeyNames.METHOD_TYPE.asString()) + .hasLowCardinalityKeyValueWithKey(GrpcObservationDocumentation.LowCardinalityKeyNames.SERVICE.asString()) + .hasLowCardinalityKeyValueWithKey( + GrpcObservationDocumentation.LowCardinalityKeyNames.STATUS_CODE.asString()); + } + // GRPC service extending SimpleService and provides echo implementation. static class EchoService extends SimpleServiceImplBase { diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConventionTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConventionTest.java index 465f1510d2..e465f778b0 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConventionTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/DefaultApacheHttpClientObservationConventionTest.java @@ -150,8 +150,9 @@ void shouldContributeTargetWhenUnknown() { SimpleHttpRequest request = SimpleRequestBuilder.get("https://example.org/resource").build(); HttpClientContext clientContext = HttpClientContext.create(); ApacheHttpClientContext context = new ApacheHttpClientContext(request, clientContext); - assertThat(observationConvention.getLowCardinalityKeyValues(context)).contains(TARGET_HOST.withValue("UNKNOWN"), - TARGET_PORT.withValue("UNKNOWN"), TARGET_SCHEME.withValue("UNKNOWN")); + assertThat(observationConvention.getLowCardinalityKeyValues(context)).contains( + TARGET_HOST.withValue("example.org"), TARGET_PORT.withValue("UNKNOWN"), + TARGET_SCHEME.withValue("https")); } @Test diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java index 3a1683def6..eb8151bd8c 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/MicrometerHttpRequestExecutorTest.java @@ -37,7 +37,6 @@ import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.util.Timeout; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -182,12 +181,12 @@ void routeTaggedIfEnabled(boolean configureObservationRegistry, @WiremockResolve } @Test - @Disabled("brittle test using reflection to check internals of third-party code") void waitForContinueGetsPassedToSuper() { MicrometerHttpRequestExecutor requestExecutor = MicrometerHttpRequestExecutor.builder(registry) .waitForContinue(Timeout.ofMilliseconds(1000)) .build(); - assertThat(requestExecutor).hasFieldOrPropertyWithValue("waitForContinue", Timeout.ofMilliseconds(1000)); + assertThat(requestExecutor).extracting("http1Config.waitForContinueTimeout") + .isEqualTo(Timeout.ofMilliseconds(1000)); } @ParameterizedTest diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/ObservationExecChainHandlerIntegrationTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/ObservationExecChainHandlerIntegrationTest.java index 9bad5e85ac..ad78a173bc 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/ObservationExecChainHandlerIntegrationTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/hc5/ObservationExecChainHandlerIntegrationTest.java @@ -17,6 +17,7 @@ import com.github.tomakehurst.wiremock.WireMockServer; import io.micrometer.observation.tck.TestObservationRegistry; +import org.apache.hc.client5.http.HttpHostConnectException; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; import org.apache.hc.client5.http.classic.methods.HttpGet; @@ -55,8 +56,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static io.micrometer.core.instrument.binder.httpcomponents.hc5.ApacheHttpClientObservationDocumentation.ApacheHttpClientKeyNames.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.*; /** * Wiremock-based integration tests for {@link ObservationExecChainHandler}. @@ -237,6 +237,18 @@ void recordAggregateRetriesWithSuccess(@WiremockResolver.Wiremock WireMockServer .doesNotHaveAnyRemainingCurrentObservation(); } + @Test + void targetHostPortAndSchemeShouldBeProvidedEvenWhenHttpHostConnectExceptionIsThrown() throws IOException { + try (CloseableHttpClient client = classicClient()) { + assertThatExceptionOfType(HttpHostConnectException.class) + .isThrownBy(() -> executeClassic(client, new HttpGet("http://localhost:777/123"))); + } + assertThat(observationRegistry).hasAnObservationWithAKeyValue(TARGET_HOST.withValue("localhost")) + .hasAnObservationWithAKeyValue(TARGET_PORT.withValue("777")) + .hasAnObservationWithAKeyValue(TARGET_SCHEME.withValue("http")) + .hasNumberOfObservationsWithNameEqualTo(DEFAULT_METER_NAME, 1); + } + } @Nested @@ -443,14 +455,12 @@ private CloseableHttpClient classicClient_aggregateRetries() { .setConnectTimeout(2000L, TimeUnit.MILLISECONDS) .build(); - // tag::setup_classic_aggregate_retries[] HttpClientBuilder clientBuilder = HttpClients.custom() .setRetryStrategy(retryStrategy) .addExecInterceptorFirst("micrometer", new ObservationExecChainHandler(observationRegistry)) .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() .setDefaultConnectionConfig(connectionConfig) .build()); - // end::setup_classic_aggregate_retries[] return clientBuilder.build(); } diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommandTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommandTest.java index 5d7d579bb8..b6ce283813 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommandTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommandTest.java @@ -26,6 +26,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Locale; + import static org.assertj.core.api.Assertions.assertThat; class MicrometerMetricsPublisherCommandTest { @@ -86,7 +88,7 @@ void cumulativeCounters() throws Exception { } private void assertExecutionMetric(Iterable tags, HystrixEventType eventType, double count) { - Iterable myTags = Tags.concat(tags, "event", eventType.name().toLowerCase(), "terminal", + Iterable myTags = Tags.concat(tags, "event", eventType.name().toLowerCase(Locale.ROOT), "terminal", Boolean.toString(eventType.isTerminal())); assertThat(registry.get("hystrix.execution").tags(myTags).counter().count()).isEqualTo(count); } diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsTest.java index c8af1a4d8b..03ecb3b831 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.params.provider.CsvSource; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.awaitility.Awaitility.await; @@ -302,6 +303,32 @@ void monitorScheduledExecutorServiceWithRepetitiveTasks(String metricPrefix, Str assertThat(registry.get(expectedMetricPrefix + "executor.idle").tags(userTags).timer().count()).isEqualTo(0L); } + @Test + @Issue("#5650") + void queuedSubmissionsAreIncludedInExecutorQueuedMetric() { + ForkJoinPool pool = new ForkJoinPool(1, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, false, 1, 1, 1, + a -> true, 555, TimeUnit.MILLISECONDS); + ExecutorServiceMetrics.monitor(registry, pool, "myForkJoinPool"); + AtomicBoolean busy = new AtomicBoolean(true); + + // will be an active task + pool.execute(() -> { + while (busy.get()) { + } + }); + + // will be queued for submission + pool.execute(() -> { + }); + pool.execute(() -> { + }); + + double queued = registry.get("executor.queued").tag("name", "myForkJoinPool").gauge().value(); + busy.set(false); + + assertThat(queued).isEqualTo(2.0); + } + @SuppressWarnings("unchecked") private T monitorExecutorService(String executorName, String metricPrefix, T exec) { if (metricPrefix == null) { diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetricsTest.java index 26547c4622..f3b3f06bf2 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetricsTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetricsTest.java @@ -107,17 +107,17 @@ void gcTimingIsCorrectForPauseCycleCollectors() { // get initial GC timing metrics from JMX, if any // GC could have happened before this test due to testing infrastructure // If it did, it will not be captured in the metrics - long initialPausePhaseCount = 0; + long initialPauseCount = 0; long initialPauseTimeMs = 0; - long initialConcurrentPhaseCount = 0; + long initialConcurrentCount = 0; long initialConcurrentTimeMs = 0; for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { if (mbean.getName().contains("Pauses")) { - initialPausePhaseCount += mbean.getCollectionCount(); + initialPauseCount += mbean.getCollectionCount(); initialPauseTimeMs += mbean.getCollectionTime(); } else if (mbean.getName().contains("Cycles")) { - initialConcurrentPhaseCount += mbean.getCollectionCount(); + initialConcurrentCount += mbean.getCollectionCount(); initialConcurrentTimeMs += mbean.getCollectionTime(); } } @@ -127,33 +127,11 @@ else if (mbean.getName().contains("Cycles")) { // cause GC to record new metrics System.gc(); - // get metrics from JMX again to obtain difference - long pausePhaseCount = 0; - long pauseTimeMs = 0; - long concurrentPhaseCount = 0; - long concurrentTimeMs = 0; - for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { - if (mbean.getName().contains("Pauses")) { - pausePhaseCount += mbean.getCollectionCount(); - pauseTimeMs += mbean.getCollectionTime(); - } - else if (mbean.getName().contains("Cycles")) { - concurrentPhaseCount += mbean.getCollectionCount(); - concurrentTimeMs += mbean.getCollectionTime(); - } - } - - // subtract any difference - pausePhaseCount -= initialPausePhaseCount; - pauseTimeMs -= initialPauseTimeMs; - concurrentPhaseCount -= initialConcurrentPhaseCount; - concurrentTimeMs -= initialConcurrentTimeMs; - - checkPhaseCount(pausePhaseCount, concurrentPhaseCount); - checkCollectionTime(pauseTimeMs, concurrentTimeMs); + checkPhaseCountAndCollectionTime(initialPauseCount, initialConcurrentCount, initialPauseTimeMs, + initialConcurrentTimeMs); } - boolean isPauseCyclesGc() { + static boolean isPauseCyclesGc() { return ManagementFactory.getGarbageCollectorMXBeans() .stream() .map(MemoryManagerMXBean::getName) @@ -225,8 +203,31 @@ public void handleNotification(Notification notification, Object handback) { } - private void checkPhaseCount(long expectedPauseCount, long expectedConcurrentCount) { + private void checkPhaseCountAndCollectionTime(long initialPauseCount, long initialConcurrentCount, + long initialPauseTimeMs, long initialConcurrentTimeMs) { await().atMost(200, TimeUnit.MILLISECONDS).untilAsserted(() -> { + long pauseCount = 0; + long concurrentCount = 0; + long pauseTimeMs = 0; + long concurrentTimeMs = 0; + + // get metrics from JMX again to obtain the difference + for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (mbean.getName().contains("Pauses")) { + pauseCount += mbean.getCollectionCount(); + pauseTimeMs += mbean.getCollectionTime(); + } + else if (mbean.getName().contains("Cycles")) { + concurrentCount += mbean.getCollectionCount(); + concurrentTimeMs += mbean.getCollectionTime(); + } + } + + long expectedPauseCount = pauseCount - initialPauseCount; + long expectedConcurrentCount = concurrentCount - initialConcurrentCount; + long expectedPauseTimeMs = pauseTimeMs - initialPauseTimeMs; + long expectedConcurrentTimeMs = concurrentTimeMs - initialConcurrentTimeMs; + long observedPauseCount = registry.find("jvm.gc.pause").timers().stream().mapToLong(Timer::count).sum(); long observedConcurrentCount = registry.find("jvm.gc.concurrent.phase.time") .timers() @@ -235,11 +236,7 @@ private void checkPhaseCount(long expectedPauseCount, long expectedConcurrentCou .sum(); assertThat(observedPauseCount).isEqualTo(expectedPauseCount); assertThat(observedConcurrentCount).isEqualTo(expectedConcurrentCount); - }); - } - private void checkCollectionTime(long expectedPauseTimeMs, long expectedConcurrentTimeMs) { - await().atMost(200, TimeUnit.MILLISECONDS).untilAsserted(() -> { double observedPauseTimeMs = registry.find("jvm.gc.pause") .timers() .stream() diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsAdminTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsAdminTest.java index e060af2ffe..4dae4edf95 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsAdminTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsAdminTest.java @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Test; import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import static io.micrometer.core.instrument.binder.kafka.KafkaClientMetrics.METRIC_NAME_PREFIX; import static org.apache.kafka.clients.admin.AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG; @@ -32,7 +34,7 @@ class KafkaClientMetricsAdminTest { private static final String BOOTSTRAP_SERVERS = "localhost:9092"; - private Tags tags = Tags.of("app", "myapp", "version", "1"); + private final Tags tags = Tags.of("app", "myapp", "version", "1"); KafkaClientMetrics metrics; @@ -69,6 +71,27 @@ void shouldCreateMetersWithTags() { } } + @Test + void shouldCreateMetersWithTagsAndCustomScheduler() { + try (AdminClient adminClient = createAdmin()) { + ScheduledExecutorService customScheduler = Executors.newScheduledThreadPool(1); + metrics = new KafkaClientMetrics(adminClient, tags, customScheduler); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()).hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + + metrics.close(); + assertThat(customScheduler.isShutdown()).isFalse(); + + customScheduler.shutdownNow(); + assertThat(customScheduler.isShutdown()).isTrue(); + } + } + private AdminClient createAdmin() { Properties adminConfig = new Properties(); adminConfig.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsConsumerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsConsumerTest.java index 7908f318d2..eb783f143c 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsConsumerTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsConsumerTest.java @@ -25,6 +25,8 @@ import org.junit.jupiter.api.Test; import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import static io.micrometer.core.instrument.binder.kafka.KafkaClientMetrics.METRIC_NAME_PREFIX; import static org.apache.kafka.clients.consumer.ConsumerConfig.*; @@ -34,7 +36,7 @@ class KafkaClientMetricsConsumerTest { private static final String BOOTSTRAP_SERVERS = "localhost:9092"; - private Tags tags = Tags.of("app", "myapp", "version", "1"); + private final Tags tags = Tags.of("app", "myapp", "version", "1"); KafkaClientMetrics metrics; @@ -71,6 +73,27 @@ void shouldCreateMetersWithTags() { } } + @Test + void shouldCreateMetersWithTagsAndCustomScheduler() { + try (Consumer consumer = createConsumer()) { + ScheduledExecutorService customScheduler = Executors.newScheduledThreadPool(1); + metrics = new KafkaClientMetrics(consumer, tags, customScheduler); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()).hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + + metrics.close(); + assertThat(customScheduler.isShutdown()).isFalse(); + + customScheduler.shutdownNow(); + assertThat(customScheduler.isShutdown()).isTrue(); + } + } + private Consumer createConsumer() { Properties consumerConfig = new Properties(); consumerConfig.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsProducerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsProducerTest.java index 7d8131ff52..3d0d94ec02 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsProducerTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetricsProducerTest.java @@ -25,6 +25,8 @@ import org.junit.jupiter.api.Test; import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import static io.micrometer.core.instrument.binder.kafka.KafkaClientMetrics.METRIC_NAME_PREFIX; import static org.apache.kafka.clients.producer.ProducerConfig.*; @@ -34,7 +36,7 @@ class KafkaClientMetricsProducerTest { private static final String BOOTSTRAP_SERVERS = "localhost:9092"; - private Tags tags = Tags.of("app", "myapp", "version", "1"); + private final Tags tags = Tags.of("app", "myapp", "version", "1"); KafkaClientMetrics metrics; @@ -71,6 +73,27 @@ void shouldCreateMetersWithTags() { } } + @Test + void shouldCreateMetersWithTagsAndCustomScheduler() { + try (Producer producer = createProducer()) { + ScheduledExecutorService customScheduler = Executors.newScheduledThreadPool(1); + metrics = new KafkaClientMetrics(producer, tags, customScheduler); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()).hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + + metrics.close(); + assertThat(customScheduler.isShutdown()).isFalse(); + + customScheduler.shutdownNow(); + assertThat(customScheduler.isShutdown()).isTrue(); + } + } + private Producer createProducer() { Properties producerConfig = new Properties(); producerConfig.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaMetricsTest.java index 452c5254f8..ff44ffd1a8 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaMetricsTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaMetricsTest.java @@ -34,10 +34,13 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; class KafkaMetricsTest { @@ -68,7 +71,7 @@ void shouldKeepMetersWhenMetricsDoNotChange() { } @Test - void closeShouldRemoveAllMeters() { + void closeShouldRemoveAllMetersAndShutdownDefaultScheduler() { // Given Supplier> supplier = () -> { MetricName metricName = new MetricName("a", "b", "c", new LinkedHashMap<>()); @@ -80,9 +83,35 @@ void closeShouldRemoveAllMeters() { kafkaMetrics.bindTo(registry); assertThat(registry.getMeters()).hasSize(1); + assertThat(isDefaultMetricsSchedulerThreadAlive()).isTrue(); kafkaMetrics.close(); assertThat(registry.getMeters()).isEmpty(); + await().until(() -> !isDefaultMetricsSchedulerThreadAlive()); + } + + @Test + void closeShouldRemoveAllMetersAndNotShutdownCustomScheduler() { + // Given + Supplier> supplier = () -> { + MetricName metricName = new MetricName("a", "b", "c", new LinkedHashMap<>()); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + return Collections.singletonMap(metricName, metric); + }; + ScheduledExecutorService customScheduler = Executors.newScheduledThreadPool(1); + kafkaMetrics = new KafkaMetrics(supplier, Collections.emptyList(), customScheduler); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + await().until(() -> !isDefaultMetricsSchedulerThreadAlive()); + + kafkaMetrics.close(); + assertThat(registry.getMeters()).isEmpty(); + assertThat(customScheduler.isShutdown()).isFalse(); + + customScheduler.shutdownNow(); + assertThat(customScheduler.isShutdown()).isTrue(); } @Test @@ -552,4 +581,13 @@ private KafkaMetric createKafkaMetric(MetricName metricName) { return new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); } + private static boolean isDefaultMetricsSchedulerThreadAlive() { + return Thread.getAllStackTraces() + .keySet() + .stream() + .filter(Thread::isAlive) + .map(Thread::getName) + .anyMatch(name -> name.startsWith("micrometer-kafka-metrics")); + } + } diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetricsTest.java index ff1ba3f317..0afd6f119b 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetricsTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetricsTest.java @@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test; import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import static io.micrometer.core.instrument.binder.kafka.KafkaStreamsMetrics.METRIC_NAME_PREFIX; import static org.apache.kafka.streams.StreamsConfig.APPLICATION_ID_CONFIG; @@ -73,6 +75,27 @@ void shouldCreateMetersWithTags() { } } + @Test + void shouldCreateMetersWithTagsAndCustomScheduler() { + try (KafkaStreams kafkaStreams = createStreams()) { + ScheduledExecutorService customScheduler = Executors.newScheduledThreadPool(1); + metrics = new KafkaStreamsMetrics(kafkaStreams, tags, customScheduler); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()).hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + + metrics.close(); + assertThat(customScheduler.isShutdown()).isFalse(); + + customScheduler.shutdownNow(); + assertThat(customScheduler.isShutdown()).isTrue(); + } + } + private KafkaStreams createStreams() { StreamsBuilder builder = new StreamsBuilder(); builder.stream("input").to("output"); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/logging/Log4j2MetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/logging/Log4j2MetricsTest.java index 1916544f1a..a2dcdb18da 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/logging/Log4j2MetricsTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/logging/Log4j2MetricsTest.java @@ -23,6 +23,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationSource; @@ -34,6 +35,7 @@ import java.io.IOException; import java.time.Duration; +import java.util.Iterator; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; @@ -184,4 +186,47 @@ void asyncLogShouldNotBeDuplicated() throws IOException { .until(() -> registry.get("log4j2.events").tags("level", "info").counter().count() == 3); } + @Test + void metricsFilterIsReused() { + LoggerContext loggerContext = new LoggerContext("test"); + + LoggerConfig loggerConfig = new LoggerConfig("com.test", Level.INFO, false); + Configuration configuration = loggerContext.getConfiguration(); + configuration.addLogger("com.test", loggerConfig); + loggerContext.setConfiguration(configuration); + loggerContext.updateLoggers(); + + Logger logger1 = loggerContext.getLogger("com.test.log1"); + loggerContext.getLogger("com.test.log2"); + + new Log4j2Metrics(emptyList(), loggerContext).bindTo(registry); + Iterator rootFilters = loggerContext.getRootLogger().getFilters(); + Log4j2Metrics.MetricsFilter rootFilter = (Log4j2Metrics.MetricsFilter) rootFilters.next(); + assertThat(rootFilters.hasNext()).isFalse(); + + Log4j2Metrics.MetricsFilter logger1Filter = (Log4j2Metrics.MetricsFilter) loggerContext.getConfiguration() + .getLoggerConfig(logger1.getName()) + .getFilter(); + assertThat(logger1Filter).isEqualTo(rootFilter); + } + + @Test + void multipleRegistriesCanBeBound() { + MeterRegistry registry2 = new SimpleMeterRegistry(); + + Log4j2Metrics log4j2Metrics = new Log4j2Metrics(emptyList()); + log4j2Metrics.bindTo(registry); + + Logger logger = LogManager.getLogger(Log4j2MetricsTest.class); + Configurator.setLevel(logger, Level.INFO); + logger.info("Hello, world!"); + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(1); + + log4j2Metrics.bindTo(registry2); + logger.info("Hello, world!"); + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(2); + assertThat(registry2.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(1); + + } + } diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java index 4a14a91588..3e6809089a 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java @@ -86,7 +86,7 @@ void timeNotFound(@WiremockResolver.Wiremock WireMockServer server) throws IOExc client.newCall(request).execute().close(); assertThat(registry.get("okhttp.requests") - .tags("foo", "bar", "status", "404", "outcome", "CLIENT_ERROR", "uri", "NOT_FOUND", "target.host", + .tags("foo", "bar", "status", "404", "outcome", "CLIENT_ERROR", "uri", URI_EXAMPLE_VALUE, "target.host", "localhost", "target.port", String.valueOf(server.port()), "target.scheme", "http") .timer() .count()).isEqualTo(1L); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetricsTest.java index c9ccadf3e5..14609fc677 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetricsTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetricsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import java.lang.management.OperatingSystemMXBean; +import java.util.Locale; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assumptions.assumeFalse; @@ -47,7 +48,7 @@ void fileDescriptorMetricsUnsupportedOsBeanMock() { @Test void unixFileDescriptorMetrics() { - assumeFalse(System.getProperty("os.name").toLowerCase().contains("win")); + assumeFalse(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")); // tag::example[] new FileDescriptorMetrics(Tags.of("some", "tag")).bindTo(registry); @@ -59,7 +60,7 @@ void unixFileDescriptorMetrics() { @Test void windowsFileDescriptorMetrics() { - assumeTrue(System.getProperty("os.name").toLowerCase().contains("win")); + assumeTrue(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")); new FileDescriptorMetrics(Tags.of("some", "tag")).bindTo(registry); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/ProcessorMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/ProcessorMetricsTest.java index ecbae31e12..584ca27592 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/ProcessorMetricsTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/system/ProcessorMetricsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import java.time.Duration; +import java.util.Locale; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.awaitility.Awaitility.await; @@ -49,7 +50,7 @@ void setup() { @Test void cpuMetrics() { assertThat(registry.get("system.cpu.count").gauge().value()).isPositive(); - if (System.getProperty("os.name").toLowerCase().contains("win")) { + if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win")) { assertThat(registry.find("system.load.average.1m").gauge()).describedAs("Not present on windows").isNull(); } else { diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogramTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogramTest.java index 80126ab7f4..ffc2530cd6 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogramTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/distribution/FixedBoundaryHistogramTest.java @@ -58,21 +58,25 @@ void testReset() { fixedBoundaryHistogram.record(1); fixedBoundaryHistogram.record(10); fixedBoundaryHistogram.record(100); - assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 1); + assertThat(fixedBoundaryHistogram.getCountAtBuckets()).isNotEmpty() + .allMatch(countAtBucket -> countAtBucket.count() == 1); fixedBoundaryHistogram.reset(); - assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 0); + assertThat(fixedBoundaryHistogram.getCountAtBuckets()).isNotEmpty() + .allMatch(countAtBucket -> countAtBucket.count() == 0); } @Test - void testCountsAtBucket() { + void testCountAtBuckets() { fixedBoundaryHistogram.record(1); fixedBoundaryHistogram.record(10); fixedBoundaryHistogram.record(100); - assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 1); + assertThat(fixedBoundaryHistogram.getCountAtBuckets()).isNotEmpty() + .allMatch(countAtBucket -> countAtBucket.count() == 1); fixedBoundaryHistogram.reset(); - assertThat(fixedBoundaryHistogram.getCountsAtBucket()).allMatch(countAtBucket -> countAtBucket.count() == 0); + assertThat(fixedBoundaryHistogram.getCountAtBuckets()).isNotEmpty() + .allMatch(countAtBucket -> countAtBucket.count() == 0); fixedBoundaryHistogram.record(0); - assertThat(fixedBoundaryHistogram.getCountsAtBucket()).containsExactly(new CountAtBucket(1.0, 1), + assertThat(fixedBoundaryHistogram.getCountAtBuckets()).containsExactly(new CountAtBucket(1.0, 1), new CountAtBucket(10.0, 0), new CountAtBucket(100.0, 0)); } @@ -82,7 +86,7 @@ void testCumulativeCounts() { fixedBoundaryHistogram.record(1); fixedBoundaryHistogram.record(10); fixedBoundaryHistogram.record(100); - assertThat(fixedBoundaryHistogram.getCountsAtBucket()).containsExactly(new CountAtBucket(1.0, 1), + assertThat(fixedBoundaryHistogram.getCountAtBuckets()).containsExactly(new CountAtBucket(1.0, 1), new CountAtBucket(10.0, 2), new CountAtBucket(100.0, 3)); } diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java index 72fb2d42f2..068b23b8e5 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/logging/LoggingMeterRegistryTest.java @@ -15,15 +15,22 @@ */ package io.micrometer.core.instrument.logging; +import com.google.common.util.concurrent.AtomicDouble; import io.micrometer.core.instrument.*; import io.micrometer.core.instrument.binder.BaseUnits; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; +import static java.util.Collections.emptyList; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; /** @@ -32,11 +39,20 @@ * @author Jon Schneider * @author Johnny Lim * @author Matthieu Borgraeve + * @author Francois Staudt */ class LoggingMeterRegistryTest { private final LoggingMeterRegistry registry = new LoggingMeterRegistry(); + private final ConfigurableLoggingRegistryConfig config = new ConfigurableLoggingRegistryConfig(); + + private final MockClock clock = new MockClock(); + + private final List logs = new ArrayList<>(); + + private final LoggingMeterRegistry spyLogRegistry = new LoggingMeterRegistry(config, clock, logs::add); + @Test void defaultMeterIdPrinter() { LoggingMeterRegistry registry = LoggingMeterRegistry.builder(LoggingRegistryConfig.DEFAULT).build(); @@ -110,22 +126,24 @@ void time() { @Test void writeMeterUnitlessValue() { - final String expectedResult = "meter.1{} value=0"; + final String expectedResult = "meter.1{} value=0, delta_count=30, throughput=0.5/s"; Measurement m1 = new Measurement(() -> 0d, Statistic.VALUE); - Meter meter = Meter.builder("meter.1", Meter.Type.OTHER, Collections.singletonList(m1)).register(registry); + Measurement m2 = new Measurement(() -> 30d, Statistic.COUNT); + Meter meter = Meter.builder("meter.1", Meter.Type.OTHER, List.of(m1, m2)).register(registry); LoggingMeterRegistry.Printer printer = registry.new Printer(meter); assertThat(registry.writeMeter(meter, printer)).isEqualTo(expectedResult); } @Test void writeMeterMultipleValues() { - final String expectedResult = "sheepWatch{color=black} value=5 sheep, max=1023 sheep, total=1.1s"; + final String expectedResult = "sheepWatch{color=black} value=5 sheep, max=1023 sheep, total=1.1s, delta_count=30 sheep, throughput=0.5 sheep/s"; Measurement m1 = new Measurement(() -> 5d, Statistic.VALUE); Measurement m2 = new Measurement(() -> 1023d, Statistic.MAX); Measurement m3 = new Measurement(() -> 1100d, Statistic.TOTAL_TIME); - Meter meter = Meter.builder("sheepWatch", Meter.Type.OTHER, Arrays.asList(m1, m2, m3)) + Measurement m4 = new Measurement(() -> 30d, Statistic.COUNT); + Meter meter = Meter.builder("sheepWatch", Meter.Type.OTHER, List.of(m1, m2, m3, m4)) .tag("color", "black") .description("Meter for shepherds.") .baseUnit("sheep") @@ -136,7 +154,7 @@ void writeMeterMultipleValues() { @Test void writeMeterByteValues() { - final String expectedResult = "bus-throughput{} throughput=5 B/s, value=64 B, value=2.125 KiB, value=8 MiB, value=1 GiB"; + final String expectedResult = "bus-throughput{} delta_count=300 B, throughput=5 B/s, value=64 B, value=2.125 KiB, value=8 MiB, value=1 GiB"; Measurement m1 = new Measurement(() -> 300d, Statistic.COUNT); Measurement m2 = new Measurement(() -> (double) (1 << 6), Statistic.VALUE); @@ -166,4 +184,122 @@ void printerValueWhenGaugeIsInfinityShouldPrintInfinity() { assertThat(printer.value(Double.POSITIVE_INFINITY)).isEqualTo("∞"); } + @Test + void publish_ShouldPrintDeltaCountAndThroughputWithBaseUnit_WhenMeterIsCounter() { + var counter = Counter.builder("my.counter").baseUnit("sheep").register(spyLogRegistry); + counter.increment(30); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(logs).containsExactly("my.counter{} delta_count=30 sheep throughput=0.5 sheep/s"); + } + + @Test + void publish_ShouldPrintDeltaCountAsDecimal_WhenMeterIsCounterAndCountIsDecimal() { + var counter = spyLogRegistry.counter("my.counter"); + counter.increment(0.5); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(logs).containsExactly("my.counter{} delta_count=0.5 throughput=0.008333/s"); + } + + @Test + void publish_ShouldPrintDeltaCountAndThroughput_WhenMeterIsTimer() { + var timer = spyLogRegistry.timer("my.timer"); + IntStream.rangeClosed(1, 30).forEach(t -> timer.record(1, SECONDS)); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(logs).containsExactly("my.timer{} delta_count=30 throughput=0.5/s mean=1s max=1s"); + } + + @Test + void publish_ShouldPrintDeltaCountAndThroughput_WhenMeterIsSummary() { + var summary = spyLogRegistry.summary("my.summary"); + IntStream.rangeClosed(1, 30).forEach(t -> summary.record(1)); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(logs).containsExactly("my.summary{} delta_count=30 throughput=0.5/s mean=1 max=1"); + } + + @Test + void publish_ShouldPrintDeltaCountAndThroughputWithBaseUnit_WhenMeterIsFunctionCounter() { + FunctionCounter.builder("my.function-counter", new AtomicDouble(), d -> 30) + .baseUnit("sheep") + .register(spyLogRegistry); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(logs).containsExactly("my.function-counter{} delta_count=30 sheep throughput=0.5 sheep/s"); + } + + @Test + void publish_ShouldPrintDeltaCountAsDecimal_WhenMeterIsFunctionCounterAndCountIsDecimal() { + spyLogRegistry.more().counter("my.function-counter", emptyList(), new AtomicDouble(), d -> 0.5); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(logs).containsExactly("my.function-counter{} delta_count=0.5 throughput=0.008333/s"); + } + + @Test + void publish_ShouldPrintDeltaCountAndThroughput_WhenMeterIsFunctionTimer() { + spyLogRegistry.more().timer("my.function-timer", emptyList(), new AtomicDouble(), d -> 30, d -> 30, SECONDS); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(logs).containsExactly("my.function-timer{} delta_count=30 throughput=0.5/s mean=1s"); + } + + @Test + void publish_ShouldNotPrintAnything_WhenRegistryIsDisabled() { + config.set("enabled", "false"); + spyLogRegistry.counter("my.counter").increment(); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(spyLogRegistry.getMeters()).hasSize(1); + assertThat(logs).isEmpty(); + } + + @Test + void publish_ShouldNotPrintAnything_WhenStepCountIsZeroAndLogsInactiveIsDisabled() { + spyLogRegistry.counter("my.counter"); + spyLogRegistry.timer("my.timer"); + spyLogRegistry.summary("my.summary"); + spyLogRegistry.more().counter("my.function-counter", emptyList(), new AtomicDouble(), d -> 0); + spyLogRegistry.more().timer("my.function-timer", emptyList(), new AtomicDouble(), d -> 0, d -> 0, SECONDS); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(spyLogRegistry.getMeters()).hasSize(5); + assertThat(logs).isEmpty(); + } + + @Test + void publish_ShouldPrintMetersWithZeroStepCount_WhenLogsInactiveIsEnabled() { + config.set("logInactive", "true"); + spyLogRegistry.counter("my.counter"); + spyLogRegistry.timer("my.timer"); + spyLogRegistry.summary("my.summary"); + spyLogRegistry.more().counter("my.function-counter", emptyList(), new AtomicDouble(), d -> 0); + spyLogRegistry.more().timer("my.function-timer", emptyList(), new AtomicDouble(), d -> 0, d -> 0, SECONDS); + clock.add(config.step()); + spyLogRegistry.publish(); + assertThat(spyLogRegistry.getMeters()).hasSize(5); + assertThat(logs).containsExactlyInAnyOrder("my.counter{} delta_count=0 throughput=0/s", + "my.timer{} delta_count=0 throughput=0/s mean= max=", + "my.summary{} delta_count=0 throughput=0/s mean=0 max=0", + "my.function-counter{} delta_count=0 throughput=0/s", + "my.function-timer{} delta_count=0 throughput=0/s mean="); + } + + private static class ConfigurableLoggingRegistryConfig implements LoggingRegistryConfig { + + private final Map keys = new HashMap<>(); + + @Override + public String get(String key) { + return keys.get(key); + } + + public void set(String key, String value) { + keys.put(prefix() + "." + key, value); + } + + } + } diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/simple/SimpleMeterRegistryTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/simple/SimpleMeterRegistryTest.java index def2adbfc0..1f69dd08dc 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/simple/SimpleMeterRegistryTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/simple/SimpleMeterRegistryTest.java @@ -22,12 +22,17 @@ import io.micrometer.core.instrument.step.StepFunctionCounter; import io.micrometer.core.instrument.step.StepFunctionTimer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import java.math.BigInteger; import java.time.Duration; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import java.util.stream.Stream; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.assertj.core.api.Assertions.assertThat; @@ -165,6 +170,35 @@ void stringRepresentationOfMetersShouldBeOk() { sample.stop(); } + @ParameterizedTest + @MethodSource("getSuppliers") + void newGaugeWhenSupplierProvidesSubClassOfNumberShouldReportCorrectly(Supplier supplier) { + Gauge.builder("temperature", supplier).register(registry); + assertThat(registry.getMeters()).singleElement().satisfies(meter -> { + assertThat(meter.getId().getName()).isEqualTo("temperature"); + assertThat(meter.measure()).singleElement() + .satisfies(measurement -> assertThat(measurement.getValue()).isEqualTo(70)); + }); + } + + @ParameterizedTest + @MethodSource("getSuppliers") + void newTimeGaugeWhenSupplierProvidesSubClassOfNumberShouldReportCorrectly(Supplier supplier) { + TimeGauge.builder("processing.time", supplier, TimeUnit.SECONDS).register(registry); + assertThat(registry.getMeters()).singleElement().satisfies(meter -> { + assertThat(meter.getId().getName()).isEqualTo("processing.time"); + assertThat(meter.getId().getBaseUnit()).isEqualTo("seconds"); + assertThat(meter.measure()).singleElement() + .satisfies(measurement -> assertThat(measurement.getValue()).isEqualTo(70)); + }); + } + + private static Stream> getSuppliers() { + return Stream.of((Supplier) () -> 70, (Supplier) () -> 70.0, (Supplier) () -> 70L, + (Supplier) () -> new AtomicInteger(70), + (Supplier) () -> new BigInteger("70")); + } + private SimpleMeterRegistry createRegistry(CountingMode mode) { return new SimpleMeterRegistry(new SimpleConfig() { diff --git a/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java b/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java index 5a61d08ec3..637efbc89b 100644 --- a/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java +++ b/micrometer-core/src/test/java11/io/micrometer/core/instrument/binder/jdk/MicrometerHttpClientTests.java @@ -15,6 +15,8 @@ */ package io.micrometer.core.instrument.binder.jdk; +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.http.Fault; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import io.micrometer.core.instrument.MeterRegistry; @@ -33,8 +35,10 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; +import java.util.concurrent.CompletionException; import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.BDDAssertions.then; @SuppressWarnings("deprecation") @@ -48,6 +52,8 @@ class MicrometerHttpClientTests { @BeforeEach void setup() { stubFor(any(urlEqualTo("/metrics")).willReturn(ok().withBody("body"))); + stubFor(any(urlEqualTo("/test-fault")) + .willReturn(new ResponseDefinitionBuilder().withFault(Fault.CONNECTION_RESET_BY_PEER))); } @Test @@ -85,6 +91,43 @@ void shouldInstrumentHttpClientWithTimer(WireMockRuntimeInfo wmInfo) throws IOEx thenMeterRegistryContainsHttpClientTags(); } + @Test + void shouldThrowErrorFromSendAsync(WireMockRuntimeInfo wmInfo) { + var client = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry).build(); + + String uri = "/test-fault"; + var request = HttpRequest.newBuilder(URI.create(wmInfo.getHttpBaseUrl() + uri)) + .header(MicrometerHttpClient.URI_PATTERN_HEADER, uri) + .GET() + .build(); + + var response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); + + assertThatThrownBy(response::join).isInstanceOf(CompletionException.class); + assertThatNoException().isThrownBy(() -> meterRegistry.get("http.client.requests") + .tag("method", "GET") + .tag("uri", uri) + .tag("status", "UNKNOWN") + .tag("outcome", "UNKNOWN") + .timer()); + } + + @Test + void sendAsyncShouldSetErrorInContext(WireMockRuntimeInfo wmInfo) { + ObservationRegistry observationRegistry = TestObservationRegistry.create(); + StoreContextObservationHandler storeContextObservationHandler = new StoreContextObservationHandler(); + observationRegistry.observationConfig().observationHandler(storeContextObservationHandler); + + var request = HttpRequest.newBuilder(URI.create(wmInfo.getHttpBaseUrl() + "/test-fault")).GET().build(); + + HttpClient observedClient = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry) + .observationRegistry(observationRegistry) + .build(); + var response = observedClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()); + assertThatThrownBy(response::join).isInstanceOf(CompletionException.class); + assertThat(storeContextObservationHandler.context.getError()).isInstanceOf(CompletionException.class); + } + private void thenMeterRegistryContainsHttpClientTags() { then(meterRegistry.find("http.client.requests") .tag("method", "GET") @@ -109,4 +152,20 @@ public void onStart(HttpClientContext context) { }; } + static class StoreContextObservationHandler implements ObservationHandler { + + HttpClientContext context; + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof HttpClientContext; + } + + @Override + public void onStart(HttpClientContext context) { + this.context = context; + } + + } + } diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsProcessObservationConvention.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsProcessObservationConvention.java index a668f6c6dc..7f650fca9b 100644 --- a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsProcessObservationConvention.java +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsProcessObservationConvention.java @@ -18,7 +18,6 @@ import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; -import jakarta.jms.*; import io.micrometer.jakarta9.instrument.jms.JmsObservationDocumentation.*; diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsPublishObservationConvention.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsPublishObservationConvention.java index 9cbbfa5093..ffd9b9d044 100644 --- a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsPublishObservationConvention.java +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/DefaultJmsPublishObservationConvention.java @@ -18,7 +18,6 @@ import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; -import jakarta.jms.*; import io.micrometer.jakarta9.instrument.jms.JmsObservationDocumentation.*; diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsProcessObservationContext.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsProcessObservationContext.java index 4d8d29a892..3ded504ec1 100644 --- a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsProcessObservationContext.java +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsProcessObservationContext.java @@ -16,8 +16,8 @@ package io.micrometer.jakarta9.instrument.jms; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.observation.transport.ReceiverContext; -import jakarta.jms.JMSException; import jakarta.jms.Message; /** @@ -33,12 +33,16 @@ */ public class JmsProcessObservationContext extends ReceiverContext { + private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(JmsProcessObservationContext.class); + public JmsProcessObservationContext(Message receivedMessage) { super((message, key) -> { try { return message.getStringProperty(key); } - catch (JMSException exc) { + // Some JMS providers throw exceptions other than JMSException + catch (Exception exc) { + logger.log("Failed to get message property.", exc); return null; } }); diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsPublishObservationContext.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsPublishObservationContext.java index f16d2154d9..5883c08c62 100644 --- a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsPublishObservationContext.java +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/jms/JmsPublishObservationContext.java @@ -17,8 +17,8 @@ package io.micrometer.jakarta9.instrument.jms; import io.micrometer.common.lang.Nullable; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; import io.micrometer.observation.transport.SenderContext; -import jakarta.jms.JMSException; import jakarta.jms.Message; /** @@ -33,15 +33,18 @@ */ public class JmsPublishObservationContext extends SenderContext { + private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(JmsPublishObservationContext.class); + public JmsPublishObservationContext(@Nullable Message sendMessage) { super((message, key, value) -> { - try { - if (message != null) { + if (message != null) { + try { message.setStringProperty(key, value); } - } - catch (JMSException exc) { - // ignore + // Some JMS providers throw exceptions other than JMSException + catch (Exception exc) { + logger.log("Failed to set message property.", exc); + } } }); setCarrier(sendMessage); diff --git a/micrometer-java11/build.gradle b/micrometer-java11/build.gradle index 7e5a1f786a..10364ba871 100644 --- a/micrometer-java11/build.gradle +++ b/micrometer-java11/build.gradle @@ -10,7 +10,7 @@ dependencies { } java { - targetCompatibility = 11 + targetCompatibility = JavaVersion.VERSION_11 } tasks.withType(JavaCompile).configureEach { diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/DefaultHttpClientObservationConvention.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/DefaultHttpClientObservationConvention.java index 7be3c81b14..75259f8618 100644 --- a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/DefaultHttpClientObservationConvention.java +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/DefaultHttpClientObservationConvention.java @@ -46,14 +46,14 @@ public KeyValues getLowCardinalityKeyValues(HttpClientContext context) { return KeyValues.of( HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.withValue(httpRequest.method()), HttpClientObservationDocumentation.LowCardinalityKeys.URI - .withValue(getUriTag(httpRequest, context.getResponse(), context.getUriMapper())), + .withValue(getUri(httpRequest, context.getResponse(), context.getUriMapper())), HttpClientObservationDocumentation.LowCardinalityKeys.STATUS .withValue(getStatus(context.getResponse())), HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME .withValue(getOutcome(context.getResponse()))); } - String getUriTag(HttpRequest request, @Nullable HttpResponse httpResponse, + String getUri(HttpRequest request, @Nullable HttpResponse httpResponse, Function uriMapper) { return httpResponse != null && (httpResponse.statusCode() == 404 || httpResponse.statusCode() == 301) ? "NOT_FOUND" : uriMapper.apply(request); diff --git a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClient.java b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClient.java index b3cbb5ce05..23970ce08d 100644 --- a/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClient.java +++ b/micrometer-java11/src/main/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClient.java @@ -235,7 +235,7 @@ private void stopObservationOrTimer( instrumentation.stop(DefaultHttpClientObservationConvention.INSTANCE.getName(), "Timer for JDK's HttpClient", () -> Tags.of(HttpClientObservationDocumentation.LowCardinalityKeys.METHOD.asString(), request.method(), HttpClientObservationDocumentation.LowCardinalityKeys.URI.asString(), - DefaultHttpClientObservationConvention.INSTANCE.getUriTag(request, res, uriMapper), + DefaultHttpClientObservationConvention.INSTANCE.getUri(request, res, uriMapper), HttpClientObservationDocumentation.LowCardinalityKeys.STATUS.asString(), DefaultHttpClientObservationConvention.INSTANCE.getStatus(res), HttpClientObservationDocumentation.LowCardinalityKeys.OUTCOME.asString(), @@ -265,15 +265,16 @@ public CompletableFuture> sendAsync(HttpRequest httpRequest, httpRequestBuilder); HttpRequest request = httpRequestBuilder.build(); return client.sendAsync(request, bodyHandler, pushPromiseHandler).handle((response, throwable) -> { - if (throwable != null) { - instrumentation.setThrowable(throwable); - } instrumentation.setResponse(response); - stopObservationOrTimer(instrumentation, request, response); if (throwable != null) { + instrumentation.setThrowable(throwable); + stopObservationOrTimer(instrumentation, request, response); throw new CompletionException(throwable); } - return response; + else { + stopObservationOrTimer(instrumentation, request, response); + return response; + } }); } diff --git a/micrometer-java11/src/test/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClientTests.java b/micrometer-java11/src/test/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClientTests.java index bfb7242366..0c867b9990 100644 --- a/micrometer-java11/src/test/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClientTests.java +++ b/micrometer-java11/src/test/java/io/micrometer/java11/instrument/binder/jdk/MicrometerHttpClientTests.java @@ -39,8 +39,7 @@ import java.util.concurrent.CompletionException; import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.BDDAssertions.then; @WireMockTest @@ -104,20 +103,39 @@ void shouldInstrumentHttpClientWithTimer(WireMockRuntimeInfo wmInfo) throws IOEx void shouldThrowErrorFromSendAsync(WireMockRuntimeInfo wmInfo) { var client = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry).build(); - var request = HttpRequest.newBuilder(URI.create(wmInfo.getHttpBaseUrl() + "/test-fault")).GET().build(); + String uri = "/test-fault"; + var request = HttpRequest.newBuilder(URI.create(wmInfo.getHttpBaseUrl() + uri)) + .header(MicrometerHttpClient.URI_PATTERN_HEADER, uri) + .GET() + .build(); var response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); assertThatThrownBy(response::join).isInstanceOf(CompletionException.class); assertThatNoException().isThrownBy(() -> meterRegistry.get("http.client.requests") .tag("method", "GET") - // TODO fix status/uri in exceptional cases where response may be null - .tag("uri", "UNKNOWN") + .tag("uri", uri) .tag("status", "UNKNOWN") .tag("outcome", "UNKNOWN") .timer()); } + @Test + void sendAsyncShouldSetErrorInContext(WireMockRuntimeInfo wmInfo) { + ObservationRegistry observationRegistry = TestObservationRegistry.create(); + StoreContextObservationHandler storeContextObservationHandler = new StoreContextObservationHandler(); + observationRegistry.observationConfig().observationHandler(storeContextObservationHandler); + + var request = HttpRequest.newBuilder(URI.create(wmInfo.getHttpBaseUrl() + "/test-fault")).GET().build(); + + HttpClient observedClient = MicrometerHttpClient.instrumentationBuilder(httpClient, meterRegistry) + .observationRegistry(observationRegistry) + .build(); + var response = observedClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()); + assertThatThrownBy(response::join).isInstanceOf(CompletionException.class); + assertThat(storeContextObservationHandler.context.getError()).isInstanceOf(CompletionException.class); + } + private void thenMeterRegistryContainsHttpClientTags() { then(meterRegistry.find("http.client.requests") .tag("method", "GET") @@ -142,4 +160,20 @@ public void onStart(HttpClientContext context) { }; } + static class StoreContextObservationHandler implements ObservationHandler { + + HttpClientContext context; + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof HttpClientContext; + } + + @Override + public void onStart(HttpClientContext context) { + this.context = context; + } + + } + } diff --git a/micrometer-java21/build.gradle b/micrometer-java21/build.gradle index c8700350f4..ffec0729c9 100644 --- a/micrometer-java21/build.gradle +++ b/micrometer-java21/build.gradle @@ -28,8 +28,13 @@ task reflectiveTests(type: Test) { includeTags 'reflective' } - // This hack is needed since VirtualThreadMetricsReflectiveTests utilizes reflection against java.lang, see its javadoc - jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'] + // This hack is needed for the following tests: + // - VirtualThreadMetricsReflectiveTests utilizes reflection against java.lang. + // - ExecutorServiceMetricsReflectiveTests utilizes reflection against java.util.concurrent. + jvmArgs += [ + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED' + ] } test { diff --git a/micrometer-java21/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsReflectiveTests.java b/micrometer-java21/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsReflectiveTests.java new file mode 100644 index 0000000000..0b79c6ac4c --- /dev/null +++ b/micrometer-java21/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsReflectiveTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed 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 + * + * https://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. + */ +package io.micrometer.core.instrument.binder.jvm; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.*; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ExecutorServiceMetrics} with reflection enabled. + * + * @author Tommy Ludwig + */ +@Tag("reflective") +class ExecutorServiceMetricsReflectiveTests { + + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + + @Test + void threadPoolMetricsWith_AutoShutdownDelegatedExecutorService() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + ExecutorService unmonitored = Executors.newSingleThreadExecutor(); + assertThat(unmonitored.getClass().getName()) + .isEqualTo("java.util.concurrent.Executors$AutoShutdownDelegatedExecutorService"); + ExecutorService monitored = ExecutorServiceMetrics.monitor(registry, unmonitored, "test"); + monitored.execute(latch::countDown); + assertThat(latch.await(100, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(registry.get("executor.completed").tag("name", "test").functionCounter().count()).isEqualTo(1L); + } + +} diff --git a/micrometer-jetty12/build.gradle b/micrometer-jetty12/build.gradle index 56b9c09d7f..4954bce801 100644 --- a/micrometer-jetty12/build.gradle +++ b/micrometer-jetty12/build.gradle @@ -24,7 +24,7 @@ dependencies { } java { - targetCompatibility = 17 + targetCompatibility = JavaVersion.VERSION_17 } compileJava { diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java index cd22299d16..d95f3c0dc4 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/InvalidObservationException.java @@ -31,6 +31,8 @@ */ public class InvalidObservationException extends RuntimeException { + private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; + private final Context context; private final List history; @@ -63,14 +65,14 @@ public static class HistoryElement { HistoryElement(EventName eventName) { this.eventName = eventName; - StackTraceElement[] currentStackTrace = Thread.getAllStackTraces().get(Thread.currentThread()); + StackTraceElement[] currentStackTrace = Thread.currentThread().getStackTrace(); this.stackTrace = findRelevantStackTraceElements(currentStackTrace); } private StackTraceElement[] findRelevantStackTraceElements(StackTraceElement[] stackTrace) { int index = findFirstRelevantStackTraceElementIndex(stackTrace); if (index == -1) { - return new StackTraceElement[0]; + return EMPTY_STACK_TRACE; } else { return Arrays.copyOfRange(stackTrace, index, stackTrace.length); diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationContextAssert.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationContextAssert.java index 2d491416f8..aa8a78e7b8 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationContextAssert.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationContextAssert.java @@ -166,7 +166,8 @@ public SELF hasKeyValuesCount(int size) { isNotNull(); long actualSize = this.actual.getAllKeyValues().stream().count(); if (actualSize != size) { - failWithMessage("Observation expected to have <%s> keys but has <%s>.", size, actualSize); + failWithActualExpectedAndMessage(actualSize, size, "Observation expected to have <%s> keys but has <%s>.", + size, actualSize); } return (SELF) this; } @@ -388,7 +389,8 @@ public SELF hasMapEntry(Object key, Object value) { isNotNull(); Object mapValue = this.actual.get(key); if (!Objects.equals(mapValue, value)) { - failWithMessage("Observation should have an entry for key <%s> with value <%s>. Value was <%s>", key, value, + failWithActualExpectedAndMessage(mapValue, value, + "Observation should have an entry for key <%s> with value <%s>. Value was <%s>", key, value, mapValue); } return (SELF) this; diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryAssert.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryAssert.java index 38c10c0da3..752c23109a 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryAssert.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryAssert.java @@ -116,8 +116,14 @@ public SELF doesNotHaveRemainingCurrentObservationSameAs(Observation observation public SELF hasRemainingCurrentObservationSameAs(Observation observation) { isNotNull(); Observation current = actual.getCurrentObservation(); + if (current == null) { + failWithMessage( + "Expected current observation in the registry to be same as <%s> but there was no current observation", + observation); + } if (current != observation) { - failWithMessage("Expected current observation in the registry to be same as <%s> but was <%s>", observation, + failWithActualExpectedAndMessage(current, observation, + "Expected current observation in the registry to be same as <%s> but was <%s>", observation, current); } return (SELF) this; @@ -183,16 +189,17 @@ public SELF doesNotHaveRemainingCurrentScopeSameAs(Observation.Scope scope) { public SELF hasRemainingCurrentScopeSameAs(Observation.Scope scope) { isNotNull(); Observation.Scope current = actual.getCurrentObservationScope(); + String expectedContextName = scope.getCurrentObservation().getContext().getName(); if (current == null) { failWithMessage( "Expected current Scope in the registry to be same as a provided Scope tied to observation named <%s> but there was no current scope", - scope.getCurrentObservation().getContext().getName()); + expectedContextName); } if (current != scope) { - failWithMessage( + String actualContextName = current.getCurrentObservation().getContext().getName(); + failWithActualExpectedAndMessage(actualContextName, expectedContextName, "Expected current Scope in the registry to be same as a provided Scope tied to observation named <%s> but was a different one (tied to observation named <%s>)", - scope.getCurrentObservation().getContext().getName(), - current.getCurrentObservation().getContext().getName()); + expectedContextName, actualContextName); } return (SELF) this; } diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java index 1c701bfee0..5d94e33a99 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationValidator.java @@ -117,10 +117,7 @@ public boolean supportsContext(Context context) { } private void addHistoryElement(Context context, EventName eventName) { - if (!context.containsKey(History.class)) { - context.put(History.class, new History()); - } - History history = context.get(History.class); + History history = context.computeIfAbsent(History.class, clazz -> new History()); history.addHistoryElement(eventName); } @@ -147,7 +144,7 @@ private Status checkIfObservationWasStartedButNotStopped(String prefix, Context } private static void throwInvalidObservationException(ValidationResult validationResult) { - History history = validationResult.getContext().getOrDefault(History.class, new History()); + History history = validationResult.getContext().getOrDefault(History.class, () -> new History()); throw new InvalidObservationException(validationResult.getMessage(), validationResult.getContext(), history.getHistoryElements()); } diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java index 5c287d1ed3..48bb0a42ef 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistry.java @@ -19,8 +19,10 @@ import io.micrometer.observation.ObservationHandler; import io.micrometer.observation.ObservationRegistry; +import java.util.HashSet; import java.util.Objects; import java.util.Queue; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.AssertProvider; @@ -118,6 +120,14 @@ public boolean supportsContext(Observation.Context context) { return true; } + @Override + public void onEvent(Observation.Event event, Observation.Context context) { + this.contexts.stream() + .filter(testContext -> testContext.getContext() == context) + .findFirst() + .ifPresent(testContext -> testContext.addEvent(event)); + } + } static class TestObservationContext { @@ -128,6 +138,8 @@ static class TestObservationContext { private boolean observationStopped; + private final Set contextEvents = new HashSet<>(); + TestObservationContext(Observation.Context context) { this.context = context; } @@ -179,6 +191,37 @@ Observation.Context getContext() { return this.context; } + /** + * Stores an {@link Observation.Event} in this context. + * @param event the event to store + */ + void addEvent(Observation.Event event) { + this.contextEvents.add(event); + } + + /** + * Check if an {@link Observation.Event} with the given name was stored in this + * context. + * @param name name of the event to check + * @return {@code true} if an event was stored under the given name + */ + boolean hasEvent(String name) { + return this.contextEvents.stream().anyMatch(event -> event.getName().equals(name)); + } + + /** + * Check if an {@link Observation.Event} with the given name and contextual name + * was stored in this context. + * @param name name of the event to check + * @param contextualName contextual name of the event to check + * @return {@code true} if an event was stored under the given name and contextual + * name + */ + boolean hasEvent(String name, String contextualName) { + return this.contextEvents.stream() + .anyMatch(event -> event.getName().equals(name) && event.getContextualName().equals(contextualName)); + } + } } diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java index 9ac2e37de9..1844556cef 100644 --- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java +++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/TestObservationRegistryAssert.java @@ -240,9 +240,11 @@ public TestObservationRegistryAssert forAllObservationsWithNameEqualToIgnoreCase */ public TestObservationRegistryAssert hasNumberOfObservationsEqualTo(int expectedNumberOfObservations) { isNotNull(); - if (this.actual.getContexts().size() != expectedNumberOfObservations) { - failWithMessage("There should be <%s> Observations but there were <%s>. Found following Observations:\n%s", - expectedNumberOfObservations, this.actual.getContexts().size(), + int actualNumberOfObservations = this.actual.getContexts().size(); + if (actualNumberOfObservations != expectedNumberOfObservations) { + failWithActualExpectedAndMessage(actualNumberOfObservations, expectedNumberOfObservations, + "There should be <%s> Observations but there were <%s>. Found following Observations:\n%s", + expectedNumberOfObservations, actualNumberOfObservations, observationNames(this.actual.getContexts())); } return this; @@ -268,14 +270,14 @@ public TestObservationRegistryAssert hasNumberOfObservationsEqualTo(int expected public TestObservationRegistryAssert hasNumberOfObservationsWithNameEqualTo(String observationName, int expectedNumberOfObservations) { isNotNull(); - long observationsWithNameSize = this.actual.getContexts() + long actualNumberOfObservations = this.actual.getContexts() .stream() .filter(f -> observationName.equals(f.getContext().getName())) .count(); - if (observationsWithNameSize != expectedNumberOfObservations) { - failWithMessage( + if (actualNumberOfObservations != expectedNumberOfObservations) { + failWithActualExpectedAndMessage(actualNumberOfObservations, expectedNumberOfObservations, "There should be <%s> Observations with name <%s> but there were <%s>. Found following Observations:\n%s", - expectedNumberOfObservations, observationName, observationsWithNameSize, + expectedNumberOfObservations, observationName, actualNumberOfObservations, observationNames(this.actual.getContexts())); } return this; @@ -302,14 +304,14 @@ public TestObservationRegistryAssert hasNumberOfObservationsWithNameEqualTo(Stri public TestObservationRegistryAssert hasNumberOfObservationsWithNameEqualToIgnoreCase(String observationName, int expectedNumberOfObservations) { isNotNull(); - long observationsWithNameSize = this.actual.getContexts() + long actualNumberOfObservations = this.actual.getContexts() .stream() .filter(f -> observationName.equalsIgnoreCase(f.getContext().getName())) .count(); - if (observationsWithNameSize != expectedNumberOfObservations) { - failWithMessage( + if (actualNumberOfObservations != expectedNumberOfObservations) { + failWithActualExpectedAndMessage(actualNumberOfObservations, expectedNumberOfObservations, "There should be <%s> Observations with name (ignoring case) <%s> but there were <%s>. Found following Observations:\n%s", - expectedNumberOfObservations, observationName, observationsWithNameSize, + expectedNumberOfObservations, observationName, actualNumberOfObservations, observationNames(this.actual.getContexts())); } return this; @@ -573,6 +575,79 @@ public TestObservationRegistryAssert backToTestObservationRegistry() { return this.originalAssert; } + /** + * Verifies that the {@link Observation} has an event with the given name. + * @param name event name + * @return this + * @throws AssertionError if the {@link Observation} does not have an event with + * the given name + * @since 1.15.0 + */ + public TestObservationRegistryAssertReturningObservationContextAssert hasEvent(String name) { + isNotNull(); + if (!this.testContext.hasEvent(name)) { + failWithMessage("Observation should have an event with name <%s>", name); + } + return this; + } + + /** + * Verifies that the {@link Observation} has an event with the given name and + * contextual name. + * @param name event name + * @param contextualName contextual name + * @return this + * @throws AssertionError if the {@link Observation} does not have an event with + * the given name and contextual name + * @since 1.15.0 + */ + public TestObservationRegistryAssertReturningObservationContextAssert hasEvent(String name, + String contextualName) { + isNotNull(); + if (!this.testContext.hasEvent(name, contextualName)) { + failWithMessage("Observation should have an event with name <%s> and contextual name <%s>", name, + contextualName); + } + return this; + } + + /** + * Verifies that the {@link Observation} does not have an event with the given + * name. + * @param name event name + * @return this + * @throws AssertionError if the {@link Observation} has an event with the given + * name + * @since 1.15.0 + */ + public TestObservationRegistryAssertReturningObservationContextAssert doesNotHaveEvent(String name) { + isNotNull(); + if (this.testContext.hasEvent(name)) { + failWithMessage("Observation should not have an event with name <%s>", name); + } + return this; + } + + /** + * Verifies that the {@link Observation} does not have an event with the given + * name and contextual name. + * @param name event name + * @param contextualName contextual name + * @return this + * @throws AssertionError if the {@link Observation} has an event with the given + * name and contextual name + * @since 1.15.0 + */ + public TestObservationRegistryAssertReturningObservationContextAssert doesNotHaveEvent(String name, + String contextualName) { + isNotNull(); + if (this.testContext.hasEvent(name, contextualName)) { + failWithMessage("Observation should not have an event with name <%s> and contextual name <%s>", name, + contextualName); + } + return this; + } + } } diff --git a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationContextAssertTests.java b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationContextAssertTests.java index 84c3577a3e..225afc26b5 100644 --- a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationContextAssertTests.java +++ b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationContextAssertTests.java @@ -201,10 +201,18 @@ void should_throw_exception_when_key_count_differs() { observation.highCardinalityKeyValue("high", "bar"); thenThrownBy(() -> assertThat(context).hasKeyValuesCount(1)).isInstanceOf(AssertionError.class) - .hasMessage("Observation expected to have <1> keys but has <2>."); + .hasMessage("Observation expected to have <1> keys but has <2>.") + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("2"); + then(error.getExpected().getStringRepresentation()).isEqualTo("1"); + }); thenThrownBy(() -> assertThat(context).hasKeyValuesCount(3)).isInstanceOf(AssertionError.class) - .hasMessage("Observation expected to have <3> keys but has <2>."); + .hasMessage("Observation expected to have <3> keys but has <2>.") + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("2"); + then(error.getExpected().getStringRepresentation()).isEqualTo("3"); + }); } @Test @@ -414,7 +422,11 @@ void should_throw_exception_when_tags_present() { void should_throw_exception_when_map_entry_missing() { context.put("foo", "bar"); - thenThrownBy(() -> assertThat(context).hasMapEntry("foo", "baz")).isInstanceOf(AssertionError.class); + thenThrownBy(() -> assertThat(context).hasMapEntry("foo", "baz")) + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("bar"); + then(error.getExpected().getStringRepresentation()).isEqualTo("baz"); + }); } @Test diff --git a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationRegistryAssertTests.java b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationRegistryAssertTests.java index 73d972c68a..42d246319f 100644 --- a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationRegistryAssertTests.java +++ b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationRegistryAssertTests.java @@ -19,8 +19,10 @@ import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.BDDAssertions.then; @SuppressWarnings("rawtypes") class ObservationRegistryAssertTests { @@ -76,7 +78,17 @@ void assertionErrorThrownWhenRemainingObservationNotSameAs() { assertThatThrownBy(() -> this.registryAssert.hasRemainingCurrentObservationSameAs(observation)) .isInstanceOf(AssertionError.class) - .hasMessageContaining("Expected current observation in the registry to be same as <{name=foo"); + .hasMessageContaining("but there was no current observation"); + + Observation anotherObservation = Observation.start("bar", this.registry); + try (Observation.Scope scope = anotherObservation.openScope()) { + assertThatThrownBy(() -> this.registryAssert.hasRemainingCurrentObservationSameAs(observation)) + .hasMessageContaining("Expected current observation in the registry to be same as <{name=foo") + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).contains("bar"); + then(error.getExpected().getStringRepresentation()).contains("foo"); + }); + } } @Test @@ -148,11 +160,16 @@ void failsHasRemainingCurrentScopeSameAs() { Observation.Scope scope1 = o1.openScope(); scope1.close(); try (Observation.Scope scope2 = o2.openScope()) { - assertThatExceptionOfType(AssertionError.class) + assertThatExceptionOfType(AssertionFailedError.class) .isThrownBy(() -> this.registryAssert.hasRemainingCurrentScopeSameAs(scope1)) .withMessage( "Expected current Scope in the registry to be same as a provided Scope tied to observation named " - + " but was a different one (tied to observation named )"); + + " but was a different one (tied to observation named )") + .satisfies(error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("new"); + then(error.getExpected().getStringRepresentation()).isEqualTo("old"); + }); + ; } } diff --git a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java index eeb5c5b06e..14a181a1c1 100644 --- a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java +++ b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/ObservationValidatorTests.java @@ -42,8 +42,8 @@ void doubleStartShouldBeInvalid() { .hasMessage("Invalid start: Observation 'test' has already been started") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid start: Observation 'test' has already been started\n" - + "START: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "START: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "START: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "START: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -54,7 +54,7 @@ void stopBeforeStartShouldBeInvalid() { .hasMessage("Invalid stop: Observation 'test' has not been started yet") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid stop: Observation 'test' has not been started yet\n" - + "STOP: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$stopBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "STOP: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$stopBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -65,7 +65,7 @@ void errorBeforeStartShouldBeInvalid() { .hasMessage("Invalid error signal: Observation 'test' has not been started yet") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid error signal: Observation 'test' has not been started yet\n" - + "ERROR: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "ERROR: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -76,7 +76,7 @@ void eventBeforeStartShouldBeInvalid() { .hasMessage("Invalid event signal: Observation 'test' has not been started yet") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid event signal: Observation 'test' has not been started yet\n" - + "EVENT: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "EVENT: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -89,7 +89,7 @@ void scopeBeforeStartShouldBeInvalid() { .hasMessage("Invalid scope opening: Observation 'test' has not been started yet") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid scope opening: Observation 'test' has not been started yet\n" - + "SCOPE_OPEN: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$scopeBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "SCOPE_OPEN: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$scopeBeforeStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -100,8 +100,8 @@ void observeAfterStartShouldBeInvalid() { .hasMessage("Invalid start: Observation 'test' has already been started") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid start: Observation 'test' has already been started\n" - + "START: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$observeAfterStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "START: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$observeAfterStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "START: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$observeAfterStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "START: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$observeAfterStartShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -115,9 +115,9 @@ void doubleStopShouldBeInvalid() { .hasMessage("Invalid stop: Observation 'test' has already been stopped") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid stop: Observation 'test' has already been stopped\n" - + "START: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "STOP: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "STOP: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "START: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "STOP: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "STOP: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$doubleStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -131,9 +131,9 @@ void errorAfterStopShouldBeInvalid() { .hasMessage("Invalid error signal: Observation 'test' has already been stopped") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid error signal: Observation 'test' has already been stopped\n" - + "START: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "STOP: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "ERROR: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "START: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "STOP: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "ERROR: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$errorAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test @@ -147,9 +147,9 @@ void eventAfterStopShouldBeInvalid() { .hasMessage("Invalid event signal: Observation 'test' has already been stopped") .satisfies(exception -> assertThat(exception.toString()).matches( "(?s)^io\\.micrometer\\.observation\\.tck\\.InvalidObservationException: Invalid event signal: Observation 'test' has already been stopped\n" - + "START: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "STOP: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" - + "EVENT: app//io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests.java:\\d+\\)$")); + + "START: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "STOP: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)\n" + + "EVENT: io\\.micrometer\\.observation\\.tck\\.ObservationValidatorTests\\.lambda\\$eventAfterStopShouldBeInvalid\\$\\d+\\(ObservationValidatorTests\\.java:\\d+\\)$")); } @Test diff --git a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java index a3ec4cfa9e..6006d2011b 100644 --- a/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java +++ b/micrometer-observation-test/src/test/java/io/micrometer/observation/tck/TestObservationRegistryAssertTests.java @@ -23,12 +23,13 @@ import org.assertj.core.api.BDDAssertions; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.BDDAssertions.thenNoException; -import static org.assertj.core.api.BDDAssertions.thenThrownBy; +import static org.assertj.core.api.BDDAssertions.*; +import static org.assertj.core.api.BDDAssertions.then; class TestObservationRegistryAssertTests { @@ -228,7 +229,12 @@ void should_not_fail_when_all_observations_match_the_assertion_ignore_case() { void should_fail_when_number_of_observations_does_not_match() { Observation.createNotStarted("FOO", registry).start().stop(); - thenThrownBy(() -> assertThat(registry).hasNumberOfObservationsEqualTo(0)).isInstanceOf(AssertionError.class); + thenThrownBy(() -> assertThat(registry).hasNumberOfObservationsEqualTo(0)) + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("1"); + then(error.getExpected().getStringRepresentation()).isEqualTo("0"); + }); + ; } @Test @@ -243,7 +249,10 @@ void should_fail_when_names_match_but_number_is_incorrect() { Observation.createNotStarted("foo", registry).start().stop(); thenThrownBy(() -> assertThat(registry).hasNumberOfObservationsWithNameEqualTo("foo", 0)) - .isInstanceOf(AssertionError.class); + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("1"); + then(error.getExpected().getStringRepresentation()).isEqualTo("0"); + }); } @Test @@ -251,7 +260,10 @@ void should_fail_when_number_is_correct_but_names_do_not_match() { Observation.createNotStarted("foo", registry).start().stop(); thenThrownBy(() -> assertThat(registry).hasNumberOfObservationsWithNameEqualTo("bar", 1)) - .isInstanceOf(AssertionError.class); + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("0"); + then(error.getExpected().getStringRepresentation()).isEqualTo("1"); + }); } @Test @@ -266,7 +278,10 @@ void should_fail_when_names_match_but_number_is_incorrect_ignore_case() { Observation.createNotStarted("FOO", registry).start().stop(); thenThrownBy(() -> assertThat(registry).hasNumberOfObservationsWithNameEqualToIgnoreCase("foo", 0)) - .isInstanceOf(AssertionError.class); + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("1"); + then(error.getExpected().getStringRepresentation()).isEqualTo("0"); + }); } @Test @@ -274,7 +289,10 @@ void should_fail_when_number_is_correct_but_names_do_not_match_ignore_case() { Observation.createNotStarted("FOO", registry).start().stop(); thenThrownBy(() -> assertThat(registry).hasNumberOfObservationsWithNameEqualToIgnoreCase("bar", 1)) - .isInstanceOf(AssertionError.class); + .isInstanceOfSatisfying(AssertionFailedError.class, error -> { + then(error.getActual().getStringRepresentation()).isEqualTo("0"); + then(error.getExpected().getStringRepresentation()).isEqualTo("1"); + }); } @Test @@ -383,6 +401,110 @@ void should_jump_to_and_back_from_context_assert() { .doesNotHaveAnyRemainingCurrentObservation()); } + @Test + void should_not_fail_when_event_matched_on_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1")).stop(); + + thenNoException() + .isThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().hasEvent("event1")); + } + + @Test + void should_not_fail_when_contextual_event_matched_on_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1", "ctx1")).stop(); + + thenNoException() + .isThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().hasEvent("event1")); + } + + @Test + void should_not_fail_when_event_matched_on_name_and_contextual_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1", "ctx1")).stop(); + + thenNoException().isThrownBy( + () -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().hasEvent("event1", "ctx1")); + } + + @Test + void should_not_fail_when_event_not_matched_on_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1")).stop(); + + thenNoException().isThrownBy( + () -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().doesNotHaveEvent("event2")); + } + + @Test + void should_not_fail_when_contextual_event_not_matched_on_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1", "ctx1")).stop(); + + thenNoException().isThrownBy( + () -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().doesNotHaveEvent("event2")); + } + + @Test + void should_not_fail_when_event_not_matched_on_name_and_contextual_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1")).stop(); + + thenNoException().isThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO") + .that() + .doesNotHaveEvent("event2", "ctx1")); + } + + @Test + void should_not_fail_when_contextual_event_not_matched_on_name_and_contextual_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1", "ctx1")).stop(); + + thenNoException().isThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO") + .that() + .doesNotHaveEvent("event2", "ctx1")); + } + + @Test + void should_fail_when_event_matched_on_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1")).stop(); + + thenThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().doesNotHaveEvent("event1")) + .isInstanceOf(AssertionError.class) + .hasMessage("Observation should not have an event with name "); + } + + @Test + void should_fail_when_event_matched_on_name_and_contextual_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1", "ctx1")).stop(); + + thenThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO") + .that() + .doesNotHaveEvent("event1", "ctx1")).isInstanceOf(AssertionError.class) + .hasMessage("Observation should not have an event with name and contextual name "); + } + + @Test + void should_fail_when_event_not_matched_on_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1")).stop(); + + thenThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().hasEvent("event2")) + .isInstanceOf(AssertionError.class) + .hasMessage("Observation should have an event with name "); + } + + @Test + void should_fail_when_contextual_event_not_matched_on_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1", "ctx1")).stop(); + + thenThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().hasEvent("event2")) + .isInstanceOf(AssertionError.class) + .hasMessage("Observation should have an event with name "); + } + + @Test + void should_fail_when_event_not_matched_on_name_and_contextual_name() { + Observation.createNotStarted("FOO", registry).start().event(Observation.Event.of("event1", "ctx1")).stop(); + + thenThrownBy(() -> assertThat(registry).hasObservationWithNameEqualTo("FOO").that().hasEvent("event2", "ctx2")) + .isInstanceOf(AssertionError.class) + .hasMessage("Observation should have an event with name and contextual name "); + } + static class Example { private final ObservationRegistry registry; diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java b/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java index d3801305d3..f1aa6ef1de 100644 --- a/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java +++ b/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java @@ -21,7 +21,6 @@ import io.micrometer.common.lang.Nullable; import io.micrometer.common.util.internal.logging.InternalLoggerFactory; -import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; @@ -925,9 +924,9 @@ class Context implements ContextView { @Nullable private ObservationView parentObservation; - private final Map lowCardinalityKeyValues = new LinkedHashMap<>(); + private final Map lowCardinalityKeyValues = new ConcurrentHashMap<>(); - private final Map highCardinalityKeyValues = new LinkedHashMap<>(); + private final Map highCardinalityKeyValues = new ConcurrentHashMap<>(); /** * The observation name. diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java index 080692ee1a..c6d7d4fdb8 100644 --- a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java +++ b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java @@ -70,6 +70,7 @@ * * @author Jonatan Ivanov * @author Yanming Zhou + * @author Jeonggi Kim * @since 1.10.0 */ @Aspect @@ -148,8 +149,15 @@ private Object observe(ProceedingJoinPoint pjp, Method method, Observed observed observation.start(); Observation.Scope scope = observation.openScope(); try { - return ((CompletionStage) pjp.proceed()) - .whenComplete((result, error) -> stopObservation(observation, scope, error)); + Object result = pjp.proceed(); + if (result == null) { + stopObservation(observation, scope, null); + return result; + } + else { + CompletionStage stage = (CompletionStage) result; + return stage.whenComplete((res, error) -> stopObservation(observation, scope, error)); + } } catch (Throwable error) { stopObservation(observation, scope, error); diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspectObservationDocumentation.java b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspectObservationDocumentation.java index c083e1c2bb..5a08ff772d 100644 --- a/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspectObservationDocumentation.java +++ b/micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspectObservationDocumentation.java @@ -33,7 +33,6 @@ * An {@link ObservationDocumentation} for {@link ObservedAspect}. * * @author Jonatan Ivanov - * @since 1.10.0 */ enum ObservedAspectObservationDocumentation implements ObservationDocumentation { diff --git a/micrometer-osgi-test/build.gradle b/micrometer-osgi-test/build.gradle index d6ffd9152b..030623b352 100644 --- a/micrometer-osgi-test/build.gradle +++ b/micrometer-osgi-test/build.gradle @@ -26,8 +26,9 @@ dependencies { def testingBundle = tasks.register('testingBundle', Bundle) { archiveClassifier = 'tests' from sourceSets.test.output - sourceSet = sourceSets.test - + if (javaLanguageVersion.asInt() < 17) { + sourceSet = sourceSets.test + } bundle { bnd """\ Bundle-SymbolicName: \${task.archiveBaseName}-\${task.archiveClassifier} diff --git a/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpClientTimingInstrumentationVerificationTests.java b/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpClientTimingInstrumentationVerificationTests.java index 945c3e18fb..67a0bd0a94 100644 --- a/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpClientTimingInstrumentationVerificationTests.java +++ b/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpClientTimingInstrumentationVerificationTests.java @@ -144,7 +144,7 @@ protected String substitutePathVariables(String templatedPath, String... pathVar } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void getTemplatedPathForUri(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) { checkAndSetupTestForTestType(testType); @@ -162,7 +162,7 @@ void getTemplatedPathForUri(TestType testType, WireMockRuntimeInfo wmRuntimeInfo } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource @Disabled("apache/jetty http client instrumentation currently fails this test") void timedWhenServerIsMissing(TestType testType) throws IOException { checkAndSetupTestForTestType(testType); @@ -186,7 +186,7 @@ void timedWhenServerIsMissing(TestType testType) throws IOException { } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void serverException(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) { checkAndSetupTestForTestType(testType); @@ -203,7 +203,7 @@ void serverException(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) { } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void clientException(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) { checkAndSetupTestForTestType(testType); @@ -223,7 +223,7 @@ void clientException(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) { // TODO this test doesn't need to be parameterized but the custom resolver for // Before/After methods doesn't like when it isn't. @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void headerIsPropagatedFromContext(TestType testType, WireMockRuntimeInfo wmRuntimeInfo) { checkAndSetupTestForTestType(testType); diff --git a/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpServerTimingInstrumentationVerificationTests.java b/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpServerTimingInstrumentationVerificationTests.java index 2b304d9aec..075fe118c9 100644 --- a/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpServerTimingInstrumentationVerificationTests.java +++ b/micrometer-test/src/main/java/io/micrometer/core/instrument/HttpServerTimingInstrumentationVerificationTests.java @@ -120,14 +120,14 @@ void afterEach() throws Exception { } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void uriIsNotFound_whenRouteIsUnmapped(TestType testType) throws Throwable { sender.get(baseUri + "notFound").send(); checkTimer(rs -> rs.tags("uri", "NOT_FOUND", "status", "404", "method", "GET").timer().count() == 1); } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void uriTemplateIsTagged(TestType testType) throws Throwable { sender.get(baseUri + "hello/world").send(); checkTimer(rs -> rs.tags("uri", InstrumentedRoutes.TEMPLATED_ROUTE, "status", "200", "method", "GET") @@ -136,7 +136,7 @@ void uriTemplateIsTagged(TestType testType) throws Throwable { } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void redirect(TestType testType) throws Throwable { sender.get(baseUri + "foundRedirect").send(); checkTimer(rs -> rs.tags("uri", InstrumentedRoutes.REDIRECT, "status", "302", "method", "GET") @@ -145,7 +145,7 @@ void redirect(TestType testType) throws Throwable { } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void errorResponse(TestType testType) throws Throwable { sender.post(baseUri + "error").send(); checkTimer( @@ -153,7 +153,7 @@ void errorResponse(TestType testType) throws Throwable { } @ParameterizedTest - @EnumSource(TestType.class) + @EnumSource void canExtractContextFromHeaders(TestType testType) throws Throwable { sender.get(baseUri + "hello/micrometer").withHeader("Test-Propagation", "someValue").send(); diff --git a/micrometer-test/src/main/java/io/micrometer/core/ipc/http/HttpSenderCompatibilityKit.java b/micrometer-test/src/main/java/io/micrometer/core/ipc/http/HttpSenderCompatibilityKit.java index a81ecc24ea..5cfbd5d84a 100644 --- a/micrometer-test/src/main/java/io/micrometer/core/ipc/http/HttpSenderCompatibilityKit.java +++ b/micrometer-test/src/main/java/io/micrometer/core/ipc/http/HttpSenderCompatibilityKit.java @@ -56,7 +56,7 @@ void httpSenderIsNotNull() { @ParameterizedTest @DisplayName("successfully send a request with NO body and receive a response with NO body") - @EnumSource(HttpSender.Method.class) + @EnumSource void successfulRequestSentWithNoBody(HttpSender.Method method, @WiremockResolver.Wiremock WireMockServer server) throws Throwable { server.stubFor(any(urlEqualTo("/metrics"))); @@ -104,7 +104,7 @@ void successfulRequestSentWithBody(HttpSender.Method method, @WiremockResolver.W @ParameterizedTest @DisplayName("receive an error response") - @EnumSource(HttpSender.Method.class) + @EnumSource void errorResponseReceived(HttpSender.Method method, @WiremockResolver.Wiremock WireMockServer server) throws Throwable { server.stubFor(any(urlEqualTo("/metrics")).willReturn(badRequest().withBody("Error processing metrics"))); @@ -121,7 +121,7 @@ void errorResponseReceived(HttpSender.Method method, @WiremockResolver.Wiremock } @ParameterizedTest - @EnumSource(HttpSender.Method.class) + @EnumSource void basicAuth(HttpSender.Method method, @WiremockResolver.Wiremock WireMockServer server) throws Throwable { server.stubFor(any(urlEqualTo("/metrics")).willReturn(unauthorized())); @@ -140,7 +140,7 @@ void basicAuth(HttpSender.Method method, @WiremockResolver.Wiremock WireMockServ } @ParameterizedTest - @EnumSource(HttpSender.Method.class) + @EnumSource void customHeader(HttpSender.Method method, @WiremockResolver.Wiremock WireMockServer server) throws Throwable { server.stubFor(any(urlEqualTo("/metrics")).willReturn(unauthorized())); diff --git a/micrometer-test/src/main/java/io/micrometer/core/tck/MeterRegistryCompatibilityKit.java b/micrometer-test/src/main/java/io/micrometer/core/tck/MeterRegistryCompatibilityKit.java index 51ab9b4e5f..00d80ef3c9 100644 --- a/micrometer-test/src/main/java/io/micrometer/core/tck/MeterRegistryCompatibilityKit.java +++ b/micrometer-test/src/main/java/io/micrometer/core/tck/MeterRegistryCompatibilityKit.java @@ -19,6 +19,7 @@ import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.distribution.CountAtBucket; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.distribution.HistogramSnapshot; @@ -158,7 +159,7 @@ void functionTimerUnits() { FunctionTimer ft = registry.get("function.timer").functionTimer(); clock(registry).add(step()); assertThat(ft.measure()).anySatisfy(ms -> { - TimeUnit baseUnit = TimeUnit.valueOf(requireNonNull(ft.getId().getBaseUnit()).toUpperCase()); + TimeUnit baseUnit = TimeUnit.valueOf(requireNonNull(ft.getId().getBaseUnit()).toUpperCase(Locale.ROOT)); assertThat(ms.getStatistic()).isEqualTo(Statistic.TOTAL_TIME); assertThat(TimeUtils.convert(ms.getValue(), baseUnit, TimeUnit.MILLISECONDS)).isEqualTo(1); }); @@ -444,6 +445,31 @@ void strongReferenceGauges() { assertThat(registry.get("strong.ref").gauge().value()).isEqualTo(1.0); } + @Test + @DisplayName("gauges cannot be registered twice") + void gaugesCannotBeRegisteredTwice() { + AtomicInteger n1 = registry.gauge("my.gauge", new AtomicInteger(1)); + AtomicInteger n2 = registry.gauge("my.gauge", new AtomicInteger(2)); + + assertThat(registry.get("my.gauge").gauges()).hasSize(1); + assertThat(registry.get("my.gauge").gauge().value()).isEqualTo(1); + assertThat(n1).isNotNull().hasValue(1); + assertThat(n2).isNotNull().hasValue(2); + } + + @Test + @DisplayName("gauges cannot be registered effectively twice") + void gaugesCannotBeRegisteredEffectivelyTwice() { + registry.config().meterFilter(MeterFilter.ignoreTags("ignored")); + AtomicInteger n1 = registry.gauge("my.gauge", Tags.of("ignored", "1"), new AtomicInteger(1)); + AtomicInteger n2 = registry.gauge("my.gauge", Tags.of("ignored", "2"), new AtomicInteger(2)); + + assertThat(registry.get("my.gauge").gauges()).hasSize(1); + assertThat(registry.get("my.gauge").gauge().value()).isEqualTo(1); + assertThat(n1).isNotNull().hasValue(1); + assertThat(n2).isNotNull().hasValue(2); + } + } @DisplayName("long task timers") diff --git a/micrometer-test/src/test/java/io/micrometer/core/instrument/ApacheAsyncHttpClientTimingInstrumentationVerificationTests.java b/micrometer-test/src/test/java/io/micrometer/core/instrument/ApacheAsyncHttpClientTimingInstrumentationVerificationTests.java index 44706a7158..565bd32035 100644 --- a/micrometer-test/src/test/java/io/micrometer/core/instrument/ApacheAsyncHttpClientTimingInstrumentationVerificationTests.java +++ b/micrometer-test/src/test/java/io/micrometer/core/instrument/ApacheAsyncHttpClientTimingInstrumentationVerificationTests.java @@ -30,7 +30,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -@SuppressWarnings("deprecation") +@Deprecated class ApacheAsyncHttpClientTimingInstrumentationVerificationTests extends HttpClientTimingInstrumentationVerificationTests { diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java index 5bfa6f1523..a226e75f51 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/utils/SampleRegistries.java @@ -52,8 +52,6 @@ import io.micrometer.newrelic.NewRelicMeterRegistry; import io.micrometer.prometheusmetrics.PrometheusConfig; import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; -import io.micrometer.signalfx.SignalFxConfig; -import io.micrometer.signalfx.SignalFxMeterRegistry; import io.micrometer.stackdriver.StackdriverConfig; import io.micrometer.stackdriver.StackdriverMeterRegistry; import io.micrometer.statsd.StatsdConfig; @@ -364,26 +362,6 @@ public String get(String k) { }, Clock.SYSTEM); } - public static SignalFxMeterRegistry signalFx(String accessToken) { - return new SignalFxMeterRegistry(new SignalFxConfig() { - @Override - public String accessToken() { - return accessToken; - } - - @Override - public Duration step() { - return Duration.ofSeconds(10); - } - - @Override - @Nullable - public String get(String k) { - return null; - } - }, Clock.SYSTEM); - } - public static WavefrontMeterRegistry wavefront() { return new WavefrontMeterRegistry(WavefrontConfig.DEFAULT_PROXY, Clock.SYSTEM); } diff --git a/samples/micrometer-samples-spring-framework6/build.gradle b/samples/micrometer-samples-spring-framework6/build.gradle index 0d98be0ce6..cce4ae9bfd 100644 --- a/samples/micrometer-samples-spring-framework6/build.gradle +++ b/samples/micrometer-samples-spring-framework6/build.gradle @@ -13,6 +13,7 @@ dependencies { implementation project(":micrometer-observation") testImplementation project(":micrometer-observation-test") + testImplementation project(":micrometer-test") testImplementation(libs.aspectjweaver) testImplementation libs.awaitility testImplementation(libs.contextPropagation) diff --git a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/CountedAspectTest.java b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/CountedAspectTest.java index d41b84c12c..f5cc905c9e 100644 --- a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/CountedAspectTest.java +++ b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/CountedAspectTest.java @@ -17,21 +17,25 @@ import io.micrometer.common.annotation.ValueExpressionResolver; import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.core.Issue; import io.micrometer.core.annotation.Counted; -import io.micrometer.core.aop.CountedAspect; -import io.micrometer.core.aop.CountedMeterTagAnnotationHandler; -import io.micrometer.core.aop.MeterTag; +import io.micrometer.core.aop.*; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.search.MeterNotFoundException; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import java.util.function.Predicate; import static java.util.concurrent.CompletableFuture.supplyAsync; @@ -202,6 +206,80 @@ void countedWithEmptyMetricNamesWhenCompleted() { assertThat(meterRegistry.get("method.counted").tag("result", "failure").counter().count()).isOne(); } + @Test + @Issue("#2461") + void countedWithJoinPoint() { + CountedService countedService = getAdvisedService(new CountedService(), jp -> Tags.of("extra", "override")); + countedService.succeedWithMetrics(); + + Counter counter = meterRegistry.get("metric.success") + .tag("extra", "override") + .tag("result", "success") + .counter(); + + assertThat(counter.count()).isOne(); + assertThat(counter.getId().getDescription()).isNull(); + } + + @Test + @Issue("#2461") + void countedWithJoinPointWhenCompleted() { + AsyncCountedService asyncCountedService = getAdvisedService(new AsyncCountedService(), + jp -> Tags.of("extra", "override")); + GuardedResult guardedResult = new GuardedResult(); + CompletableFuture completableFuture = asyncCountedService.succeedWithMetrics(guardedResult); + + assertThat(meterRegistry.find("metric.success").counters()).isEmpty(); + + guardedResult.complete(); + completableFuture.join(); + + Counter counter = meterRegistry.get("metric.success") + .tag("extra", "override") + .tag("exception", "none") + .tag("result", "success") + .counter(); + + assertThat(counter.count()).isOne(); + assertThat(counter.getId().getDescription()).isNull(); + } + + @Test + @Issue("#5584") + void pjpFunctionThrows() { + CountedService countedService = getAdvisedService(new CountedService(), + new CountedAspect(meterRegistry, (Function>) jp -> { + throw new RuntimeException("test"); + })); + countedService.succeedWithMetrics(); + + Counter counter = meterRegistry.get("metric.success").tag("extra", "tag").tag("result", "success").counter(); + + assertThat(counter.count()).isOne(); + assertThat(counter.getId().getDescription()).isNull(); + } + + @Test + void countedWithSuccessfulMetricsWhenReturnsCompletionStageNull() { + CompletableFuture completableFuture = asyncCountedService.successButNull(); + assertThat(completableFuture).isNull(); + assertThat(meterRegistry.get("metric.success") + .tag("method", "successButNull") + .tag("class", getClass().getName() + "$AsyncCountedService") + .tag("extra", "tag") + .tag("exception", "none") + .tag("result", "success") + .counter() + .count()).isOne(); + } + + @Test + void countedWithoutSuccessfulMetricsWhenReturnsCompletionStageNull() { + CompletableFuture completableFuture = asyncCountedService.successButNullWithoutMetrics(); + assertThat(completableFuture).isNull(); + assertThatThrownBy(() -> meterRegistry.get("metric.none").counter()).isInstanceOf(MeterNotFoundException.class); + } + static class CountedService { @Counted(value = "metric.none", recordFailuresOnly = true) @@ -241,6 +319,10 @@ private T getAdvisedService(T countedService, CountedAspect countedAspect) { return proxyFactory.getProxy(); } + private T getAdvisedService(T countedService, Function> joinPoint) { + return getAdvisedService(countedService, new CountedAspect(meterRegistry, joinPoint)); + } + static class AsyncCountedService { @Counted(value = "metric.none", recordFailuresOnly = true) @@ -263,6 +345,16 @@ CompletableFuture emptyMetricName(GuardedResult guardedResult) { return supplyAsync(guardedResult::get); } + @Counted(value = "metric.success", extraTags = { "extra", "tag" }) + CompletableFuture successButNull() { + return null; + } + + @Counted(value = "metric.none", recordFailuresOnly = true) + CompletableFuture successButNullWithoutMetrics() { + return null; + } + } static class GuardedResult { @@ -370,7 +462,8 @@ String greet() { } - static class MeterTagsTests { + @Nested + class MeterTagsTests { ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]"; @@ -379,17 +472,21 @@ static class MeterTagsTests { CountedMeterTagAnnotationHandler meterTagAnnotationHandler = new CountedMeterTagAnnotationHandler( aClass -> valueResolver, aClass -> valueExpressionResolver); - @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) - void meterTagsWithText(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - CountedAspect countedAspect = new CountedAspect(registry); - countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + MeterRegistry registry; - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(countedAspect); + CountedAspect countedAspect; - MeterTagClassInterface service = pf.getProxy(); + @BeforeEach + void setup() { + registry = new SimpleMeterRegistry(); + countedAspect = new CountedAspect(registry); + countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + } + + @ParameterizedTest + @EnumSource + void meterTagsWithText(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithCountedAspect(annotatedClass.newInstance()); service.getAnnotationForArgumentToString(15L); @@ -397,8 +494,31 @@ void meterTagsWithText(AnnotatedTestClass annotatedClass) { } @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) + @EnumSource void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithCountedAspect(annotatedClass.newInstance()); + + service.getAnnotationForTagValueResolver("foo"); + + assertThat(registry.get("method.counted") + .tag("test", "Value from myCustomTagValueResolver [foo]") + .counter() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource + void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithCountedAspect(annotatedClass.newInstance()); + + service.getAnnotationForTagValueExpression("15L"); + + assertThat(registry.get("method.counted").tag("test", "hello characters").counter().count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource + void multipleMeterTagsWithExpression(AnnotatedTestClass annotatedClass) { MeterRegistry registry = new SimpleMeterRegistry(); CountedAspect countedAspect = new CountedAspect(registry); countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); @@ -408,17 +528,18 @@ void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { MeterTagClassInterface service = pf.getProxy(); - service.getAnnotationForTagValueResolver("foo"); + service.getMultipleAnnotationsForTagValueExpression(new DataHolder("zxe", "qwe")); assertThat(registry.get("method.counted") - .tag("test", "Value from myCustomTagValueResolver [foo]") + .tag("value1", "value1: zxe") + .tag("value2", "value2.overridden: qwe") .counter() .count()).isEqualTo(1); } @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) - void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { + @EnumSource + void multipleMeterTagsWithinContainerWithExpression(AnnotatedTestClass annotatedClass) { MeterRegistry registry = new SimpleMeterRegistry(); CountedAspect countedAspect = new CountedAspect(registry); countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); @@ -428,17 +549,18 @@ void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { MeterTagClassInterface service = pf.getProxy(); - service.getAnnotationForTagValueExpression("15L"); + service.getMultipleAnnotationsWithContainerForTagValueExpression(new DataHolder("zxe", "qwe")); - assertThat(registry.get("method.counted").tag("test", "hello characters").counter().count()).isEqualTo(1); + assertThat(registry.get("method.counted") + .tag("value1", "value1: zxe") + .tag("value2", "value2: qwe") + .tag("value3", "value3.overridden: ZXEQWE") + .counter() + .count()).isEqualTo(1); } @Test void meterTagOnPackagePrivateMethod() { - MeterRegistry registry = new SimpleMeterRegistry(); - CountedAspect countedAspect = new CountedAspect(registry); - countedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); - AspectJProxyFactory pf = new AspectJProxyFactory(new MeterTagClass()); pf.setProxyTargetClass(true); pf.addAspect(countedAspect); @@ -450,6 +572,58 @@ void meterTagOnPackagePrivateMethod() { assertThat(registry.get("method.counted").tag("foo", "bar").counter().count()).isEqualTo(1); } + @ParameterizedTest + @EnumSource + void meterTagsOnReturnValueWithText(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithCountedAspect(annotatedClass.newInstance()); + + service.getAnnotationForArgumentToString(); + + assertThat(registry.get("method.counted").tag("test", "15").counter().count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource + void meterTagsOnReturnValueWithResolver(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithCountedAspect(annotatedClass.newInstance()); + + service.getAnnotationForTagValueResolver(); + + assertThat(registry.get("method.counted") + .tag("test", "Value from myCustomTagValueResolver [foo]") + .counter() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource + void meterTagsOnReturnValueWithExpression(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithCountedAspect(annotatedClass.newInstance()); + + service.getAnnotationForTagValueExpression(); + + assertThat(registry.get("method.counted").tag("test", "hello characters").counter().count()).isEqualTo(1); + } + + @Test + void meterTagOnReturnValueOnPackagePrivateMethod() { + AspectJProxyFactory pf = new AspectJProxyFactory(new MeterTagClass()); + pf.setProxyTargetClass(true); + pf.addAspect(countedAspect); + + MeterTagClass service = pf.getProxy(); + + service.getAnnotationForPackagePrivateMethod(); + + assertThat(registry.get("method.counted").tag("foo", "bar").counter().count()).isEqualTo(1); + } + + private T getProxyWithCountedAspect(T object) { + AspectJProxyFactory pf = new AspectJProxyFactory(object); + pf.addAspect(countedAspect); + return pf.getProxy(); + } + enum AnnotatedTestClass { CLASS_WITHOUT_INTERFACE(MeterTagClass.class), CLASS_WITH_INTERFACE(MeterTagClassChild.class); @@ -477,13 +651,36 @@ interface MeterTagClassInterface { @Counted void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test); + @Counted + @MeterTag(key = "test", resolver = ValueResolver.class) + String getAnnotationForTagValueResolver(); + @Counted void getAnnotationForTagValueExpression( @MeterTag(key = "test", expression = "'hello' + ' characters'") String test); + @Counted + @MeterTag(key = "test", expression = "'hello' + ' characters'") + String getAnnotationForTagValueExpression(); + @Counted void getAnnotationForArgumentToString(@MeterTag("test") Long param); + @Counted + @MeterTag("test") + Long getAnnotationForArgumentToString(); + + @Counted + void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", + expression = "'value2: ' + value2") DataHolder param); + + @Counted + void getMultipleAnnotationsWithContainerForTagValueExpression(@MeterTags({ + @MeterTag(key = "value1", expression = "'value1: ' + value1"), + @MeterTag(key = "value2", expression = "'value2: ' + value2"), @MeterTag(key = "value3", + expression = "'value3: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param); + } static class MeterTagClass implements MeterTagClassInterface { @@ -494,21 +691,63 @@ public void getAnnotationForTagValueResolver( @MeterTag(key = "test", resolver = ValueResolver.class) String test) { } + @Counted + @MeterTag(key = "test", resolver = ValueResolver.class) + @Override + public String getAnnotationForTagValueResolver() { + return "foo"; + } + @Counted @Override public void getAnnotationForTagValueExpression( @MeterTag(key = "test", expression = "'hello' + ' characters'") String test) { } + @Counted + @MeterTag(key = "test", expression = "'hello' + ' characters'") + @Override + public String getAnnotationForTagValueExpression() { + return "15L"; + } + @Counted @Override public void getAnnotationForArgumentToString(@MeterTag("test") Long param) { } + @Counted + @MeterTag("test") + @Override + public Long getAnnotationForArgumentToString() { + return 15L; + } + @Counted void getAnnotationForPackagePrivateMethod(@MeterTag("foo") String foo) { } + @MeterTag("foo") + @Counted + String getAnnotationForPackagePrivateMethod() { + return "bar"; + } + + @Counted + @Override + public void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", + expression = "'value2.overridden: ' + value2") DataHolder param) { + } + + @Counted + @Override + public void getMultipleAnnotationsWithContainerForTagValueExpression(@MeterTags({ + @MeterTag(key = "value1", expression = "'value1: ' + value1"), + @MeterTag(key = "value2", expression = "'value2: ' + value2"), @MeterTag(key = "value3", + expression = "'value3.overridden: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param) { + } + } static class MeterTagClassChild implements MeterTagClassInterface { @@ -518,16 +757,67 @@ static class MeterTagClassChild implements MeterTagClassInterface { public void getAnnotationForTagValueResolver(String test) { } + @Counted + @Override + public String getAnnotationForTagValueResolver() { + return "foo"; + } + @Counted @Override public void getAnnotationForTagValueExpression(String test) { } + @Counted + @Override + public String getAnnotationForTagValueExpression() { + return "15L"; + } + @Counted @Override public void getAnnotationForArgumentToString(Long param) { } + @Counted + @Override + public Long getAnnotationForArgumentToString() { + return 15L; + } + + @Counted + @Override + public void getMultipleAnnotationsForTagValueExpression( + @MeterTag(key = "value2", expression = "'value2.overridden: ' + value2") DataHolder param) { + } + + @Counted + @Override + public void getMultipleAnnotationsWithContainerForTagValueExpression(@MeterTag(key = "value3", + expression = "'value3.overridden: ' + value1.toUpperCase + value2.toUpperCase") DataHolder param) { + } + + } + + static class DataHolder { + + private final String value1; + + private final String value2; + + private DataHolder(String value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + + public String getValue1() { + return value1; + } + + public String getValue2() { + return value2; + } + } } diff --git a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java index c3476d4b8e..a01ef02ac5 100644 --- a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java +++ b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/ObservedAspectTests.java @@ -357,6 +357,25 @@ void ignoreClassLevelAnnotationIfMethodLevelPresent() { .hasContextualNameEqualTo("test.class#annotatedOnMethod"); } + @Test + void annotatedAsyncClassCallWithNullShouldBeObserved() { + registry.observationConfig().observationHandler(new ObservationTextPublisher()); + AspectJProxyFactory pf = new AspectJProxyFactory(new ObservedClassLevelAnnotatedService()); + pf.addAspect(new ObservedAspect(registry)); + ObservedClassLevelAnnotatedService service = pf.getProxy(); + CompletableFuture asyncResult = service.asyncNull(); + assertThat(asyncResult).isNull(); + assertThat(registry).doesNotHaveAnyRemainingCurrentObservation() + .hasSingleObservationThat() + .hasNameEqualTo("test.class") + .hasContextualNameEqualTo("test.class#call") + .hasLowCardinalityKeyValue("abc", "123") + .hasLowCardinalityKeyValue("test", "42") + .hasLowCardinalityKeyValue("class", ObservedClassLevelAnnotatedService.class.getName()) + .hasLowCardinalityKeyValue("method", "asyncNull") + .doesNotHaveError(); + } + static class ObservedService { @Observed(name = "test.call", contextualName = "test#call", @@ -445,6 +464,10 @@ CompletableFuture async(FakeAsyncTask fakeAsyncTask) { void annotatedOnMethod() { } + CompletableFuture asyncNull() { + return null; + } + } static class FakeAsyncTask implements Supplier { diff --git a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/TimedAspectTest.java b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/TimedAspectTest.java index 343a5012a7..5ee04f4d53 100644 --- a/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/TimedAspectTest.java +++ b/samples/micrometer-samples-spring-framework6/src/test/java/io/micrometer/samples/spring6/aop/TimedAspectTest.java @@ -18,12 +18,11 @@ import io.micrometer.common.annotation.ValueExpressionResolver; import io.micrometer.common.annotation.ValueResolver; import io.micrometer.common.lang.NonNull; +import io.micrometer.core.Issue; import io.micrometer.core.annotation.Timed; import io.micrometer.core.aop.*; -import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.*; import io.micrometer.core.instrument.Meter.Id; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.distribution.CountAtBucket; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.distribution.ValueAtPercentile; @@ -31,6 +30,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.instrument.util.TimeUtils; import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -41,6 +41,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Predicate; import static java.util.concurrent.CompletableFuture.supplyAsync; @@ -264,6 +265,24 @@ void timeMethodWhenCompletedExceptionally() { .count()).isEqualTo(1); } + @Test + void timeMethodWhenReturnCompletionStageNull() { + MeterRegistry registry = new SimpleMeterRegistry(); + AspectJProxyFactory pf = new AspectJProxyFactory(new AsyncTimedService()); + pf.addAspect(new TimedAspect(registry)); + AsyncTimedService service = pf.getProxy(); + CompletableFuture completableFuture = service.callNull(); + assertThat(completableFuture).isNull(); + assertThat(registry.getMeters()).isNotEmpty(); + assertThat(registry.get("callNull") + .tag("class", getClass().getName() + "$AsyncTimedService") + .tag("method", "callNull") + .tag("extra", "tag") + .tag("exception", "none") + .timer() + .count()).isEqualTo(1); + } + @Test void timeMethodWithLongTaskTimerWhenCompleted() { MeterRegistry registry = new SimpleMeterRegistry(); @@ -324,6 +343,27 @@ void timeMethodWithLongTaskTimerWhenCompletedExceptionally() { .activeTasks()).isEqualTo(0); } + @Test + void timeMethodWithLongTaskTimerWhenReturnCompletionStageNull() { + MeterRegistry registry = new SimpleMeterRegistry(); + AspectJProxyFactory pf = new AspectJProxyFactory(new AsyncTimedService()); + pf.addAspect(new TimedAspect(registry)); + AsyncTimedService service = pf.getProxy(); + CompletableFuture completableFuture = service.longCallNull(); + assertThat(completableFuture).isNull(); + assertThat(registry.get("longCallNull") + .tag("class", getClass().getName() + "$AsyncTimedService") + .tag("method", "longCallNull") + .tag("extra", "tag") + .longTaskTimers()).hasSize(1); + assertThat(registry.find("longCallNull") + .tag("class", getClass().getName() + "$AsyncTimedService") + .tag("method", "longCallNull") + .tag("extra", "tag") + .longTaskTimer() + .activeTasks()).isEqualTo(0); + } + @Test void timeMethodFailureWhenCompletedExceptionally() { MeterRegistry failingRegistry = new FailingMeterRegistry(); @@ -424,6 +464,22 @@ void timeClassFailure() { assertThat(failingRegistry.getMeters()).isEmpty(); } + @Issue("#5584") + void pjpFunctionThrows() { + MeterRegistry registry = new SimpleMeterRegistry(); + + AspectJProxyFactory pf = new AspectJProxyFactory(new TimedService()); + pf.addAspect(new TimedAspect(registry, (Function>) jp -> { + throw new RuntimeException("test"); + })); + + TimedService service = pf.getProxy(); + + service.call(); + + assertThat(registry.get("call").tag("extra", "tag").timer().count()).isEqualTo(1); + } + @Test void ignoreClassLevelAnnotationIfMethodLevelPresent() { MeterRegistry registry = new SimpleMeterRegistry(); @@ -444,6 +500,22 @@ void ignoreClassLevelAnnotationIfMethodLevelPresent() { .count()).isEqualTo(1); } + @Test + @Issue("#2461") + void timeMethodWithJoinPoint() { + MeterRegistry registry = new SimpleMeterRegistry(); + + AspectJProxyFactory pf = new AspectJProxyFactory(new TimedService()); + pf.addAspect(new TimedAspect(registry, + (Function>) jp -> Tags.of("extra", "override"))); + + TimedService service = pf.getProxy(); + + service.call(); + + assertThat(registry.get("call").tag("extra", "override").timer().count()).isEqualTo(1); + } + @Nested class MeterTagsTests { @@ -454,34 +526,33 @@ class MeterTagsTests { MeterTagAnnotationHandler meterTagAnnotationHandler = new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver); - @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) - void meterTagsWithText(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + MeterRegistry registry; + TimedAspect timedAspect; - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); + @BeforeEach + void setup() { + registry = new SimpleMeterRegistry(); + timedAspect = new TimedAspect(registry); + timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + } - MeterTagClassInterface service = pf.getProxy(); + @ParameterizedTest + @EnumSource + void meterTagsWithText(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); service.getAnnotationForArgumentToString(15L); - assertThat(registry.get("method.timed").tag("test", "15").timer().count()).isEqualTo(1); + assertThat(registry.get("method.timed") + .tag("test", "15") + .timer() + .count()).isEqualTo(1); } @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) + @EnumSource void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); - - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); - - MeterTagClassInterface service = pf.getProxy(); + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); service.getAnnotationForTagValueResolver("foo"); @@ -492,72 +563,49 @@ void meterTagsWithResolver(AnnotatedTestClass annotatedClass) { } @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) + @EnumSource void meterTagsWithExpression(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); - - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); - - MeterTagClassInterface service = pf.getProxy(); + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); service.getAnnotationForTagValueExpression("15L"); - assertThat(registry.get("method.timed").tag("test", "hello characters. overridden").timer().count()) - .isEqualTo(1); + assertThat(registry.get("method.timed") + .tag("test", "hello characters.overridden") + .timer() + .count()).isEqualTo(1); } @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) + @EnumSource void multipleMeterTagsWithExpression(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); - - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); - - MeterTagClassInterface service = pf.getProxy(); + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); service.getMultipleAnnotationsForTagValueExpression(new DataHolder("zxe", "qwe")); assertThat(registry.get("method.timed") .tag("value1", "value1: zxe") - .tag("value2", "value2. overridden: qwe") + .tag("value2", "value2.overridden: qwe") .timer() .count()).isEqualTo(1); } @ParameterizedTest - @EnumSource(AnnotatedTestClass.class) + @EnumSource void multipleMeterTagsWithinContainerWithExpression(AnnotatedTestClass annotatedClass) { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); - - AspectJProxyFactory pf = new AspectJProxyFactory(annotatedClass.newInstance()); - pf.addAspect(timedAspect); - - MeterTagClassInterface service = pf.getProxy(); + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); service.getMultipleAnnotationsWithContainerForTagValueExpression(new DataHolder("zxe", "qwe")); assertThat(registry.get("method.timed") .tag("value1", "value1: zxe") .tag("value2", "value2: qwe") - .tag("value3", "value3. overridden: ZXEQWE") + .tag("value3", "value3.overridden: ZXEQWE") .timer() .count()).isEqualTo(1); } @Test void meterTagOnPackagePrivateMethod() { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); - AspectJProxyFactory pf = new AspectJProxyFactory(new MeterTagClass()); pf.setProxyTargetClass(true); pf.addAspect(timedAspect); @@ -566,24 +614,124 @@ void meterTagOnPackagePrivateMethod() { service.getAnnotationForPackagePrivateMethod("bar"); - assertThat(registry.get("method.timed").tag("foo", "bar").timer().count()).isEqualTo(1); + assertThat(registry.get("method.timed") + .tag("foo", "bar") + .timer() + .count()).isEqualTo(1); } @Test void meterTagOnSuperClass() { - MeterRegistry registry = new SimpleMeterRegistry(); - TimedAspect timedAspect = new TimedAspect(registry); - timedAspect.setMeterTagAnnotationHandler(meterTagAnnotationHandler); + MeterTagSub service = getProxyWithTimedAspect(new MeterTagSub()); + + service.superMethod("someValue"); + + assertThat(registry.get("method.timed") + .tag("superTag", "someValue") + .timer() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsOnReturnValueWithText(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); + + service.getAnnotationForArgumentToString(); + + assertThat(registry.get("method.timed") + .tag("test", "15") + .timer() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsOnReturnValueWithResolver(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); + + service.getAnnotationForTagValueResolver(); + + assertThat(registry.get("method.timed") + .tag("test", "Value from myCustomTagValueResolver [foo]") + .timer() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void meterTagsOnReturnValueWithExpression(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); + + service.getAnnotationForTagValueExpression(); + + assertThat(registry.get("method.timed") + .tag("test", "hello characters. overridden") + .timer() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void multipleMeterTagsOnReturnValueWithExpression(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); + + service.getMultipleAnnotationsForTagValueExpression(); + + assertThat(registry.get("method.timed") + .tag("value1", "value1: zxe") + .tag("value2", "value2. overridden: qwe") + .timer() + .count()).isEqualTo(1); + } + + @ParameterizedTest + @EnumSource(AnnotatedTestClass.class) + void multipleMeterTagsOnReturnValueWithinContainerWithExpression(AnnotatedTestClass annotatedClass) { + MeterTagClassInterface service = getProxyWithTimedAspect(annotatedClass.newInstance()); + + service.getMultipleAnnotationsWithContainerForTagValueExpression(); + + assertThat(registry.get("method.timed") + .tag("value1", "value1: zxe") + .tag("value2", "value2: qwe") + .tag("value3", "value3. overridden: ZXEQWE") + .timer() + .count()).isEqualTo(1); + } - AspectJProxyFactory pf = new AspectJProxyFactory(new MeterTagSub()); + @Test + void meterTagOnReturnValueOnPackagePrivateMethod() { + AspectJProxyFactory pf = new AspectJProxyFactory(new MeterTagClass()); pf.setProxyTargetClass(true); pf.addAspect(timedAspect); - MeterTagSub service = pf.getProxy(); + MeterTagClass service = pf.getProxy(); - service.superMethod("someValue"); + service.getAnnotationForPackagePrivateMethod(); - assertThat(registry.get("method.timed").tag("superTag", "someValue").timer().count()).isEqualTo(1); + assertThat(registry.get("method.timed") + .tag("foo", "bar") + .timer() + .count()).isEqualTo(1); + } + + @Test + void meterTagOnReturnValueOnSuperClass() { + MeterTagSub service = getProxyWithTimedAspect(new MeterTagSub()); + + service.superMethod(); + + assertThat(registry.get("method.timed") + .tag("superTag", "someValue") + .timer() + .count()).isEqualTo(1); + } + + private T getProxyWithTimedAspect(T object) { + AspectJProxyFactory pf = new AspectJProxyFactory(object); + pf.addAspect(timedAspect); + return pf.getProxy(); } } @@ -615,24 +763,48 @@ interface MeterTagClassInterface { @Timed void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test); + @Timed + @MeterTag(key = "test", resolver = ValueResolver.class) + String getAnnotationForTagValueResolver(); + @Timed void getAnnotationForTagValueExpression( @MeterTag(key = "test", expression = "'hello' + ' characters'") String test); + @Timed + @MeterTag(key = "test", expression = "'hello' + ' characters'") + String getAnnotationForTagValueExpression(); + @Timed void getAnnotationForArgumentToString(@MeterTag("test") Long param); + @Timed + @MeterTag("test") + Long getAnnotationForArgumentToString(); + @Timed void getMultipleAnnotationsForTagValueExpression( @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", expression = "'value2: ' + value2") DataHolder param); + @Timed + @MeterTag(key = "value1", expression = "'value1: ' + value1") + @MeterTag(key = "value2", expression = "'value2: ' + value2") + DataHolder getMultipleAnnotationsForTagValueExpression(); + @Timed void getMultipleAnnotationsWithContainerForTagValueExpression(@MeterTags({ @MeterTag(key = "value1", expression = "'value1: ' + value1"), @MeterTag(key = "value2", expression = "'value2: ' + value2"), @MeterTag(key = "value3", expression = "'value3: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param); + @Timed + @MeterTags({ + @MeterTag(key = "value1", expression = "'value1: ' + value1"), + @MeterTag(key = "value2", expression = "'value2: ' + value2"), @MeterTag(key = "value3", + expression = "'value3: ' + value1.toUpperCase + value2.toUpperCase") }) + DataHolder getMultipleAnnotationsWithContainerForTagValueExpression(); + } static class MeterTagClass implements MeterTagClassInterface { @@ -643,10 +815,24 @@ public void getAnnotationForTagValueResolver( @MeterTag(key = "test", resolver = ValueResolver.class) String test) { } + @Timed + @MeterTag(key = "test", resolver = ValueResolver.class) + @Override + public String getAnnotationForTagValueResolver() { + return "foo"; + } + @Timed @Override public void getAnnotationForTagValueExpression( - @MeterTag(key = "test", expression = "'hello' + ' characters. overridden'") String test) { + @MeterTag(key = "test", expression = "'hello' + ' characters.overridden'") String test) { + } + + @Timed + @MeterTag(key = "test", expression = "'hello' + ' characters. overridden'") + @Override + public String getAnnotationForTagValueExpression() { + return "foo"; } @Timed @@ -654,24 +840,56 @@ public void getAnnotationForTagValueExpression( public void getAnnotationForArgumentToString(@MeterTag("test") Long param) { } + @Timed + @MeterTag("test") + @Override + public Long getAnnotationForArgumentToString() { + return 15L; + } + @Timed void getAnnotationForPackagePrivateMethod(@MeterTag("foo") String foo) { } + @Timed + @MeterTag("foo") + String getAnnotationForPackagePrivateMethod() { + return "bar"; + } + @Timed @Override public void getMultipleAnnotationsForTagValueExpression( @MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2", - expression = "'value2. overridden: ' + value2") DataHolder param) { + expression = "'value2.overridden: ' + value2") DataHolder param) { } + + @Timed + @MeterTag(key = "value1", expression = "'value1: ' + value1") + @MeterTag(key = "value2", expression = "'value2. overridden: ' + value2") + @Override + public DataHolder getMultipleAnnotationsForTagValueExpression() { + return new DataHolder("zxe", "qwe"); + } + @Timed @Override public void getMultipleAnnotationsWithContainerForTagValueExpression(@MeterTags({ @MeterTag(key = "value1", expression = "'value1: ' + value1"), @MeterTag(key = "value2", expression = "'value2: ' + value2"), @MeterTag(key = "value3", - expression = "'value3. overridden: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param) { + expression = "'value3.overridden: ' + value1.toUpperCase + value2.toUpperCase") }) DataHolder param) { + } + + @Timed + @MeterTags({ + @MeterTag(key = "value1", expression = "'value1: ' + value1"), + @MeterTag(key = "value2", expression = "'value2: ' + value2"), @MeterTag(key = "value3", + expression = "'value3. overridden: ' + value1.toUpperCase + value2.toUpperCase") }) + @Override + public DataHolder getMultipleAnnotationsWithContainerForTagValueExpression() { + return new DataHolder("zxe", "qwe"); } } @@ -683,10 +901,23 @@ static class MeterTagClassChild implements MeterTagClassInterface { public void getAnnotationForTagValueResolver(String test) { } + @Timed + @Override + public String getAnnotationForTagValueResolver() { + return "foo"; + } + @Timed @Override public void getAnnotationForTagValueExpression( - @MeterTag(key = "test", expression = "'hello' + ' characters. overridden'") String test) { + @MeterTag(key = "test", expression = "'hello' + ' characters.overridden'") String test) { + } + + @Timed + @MeterTag(key = "test", expression = "'hello' + ' characters. overridden'") + @Override + public String getAnnotationForTagValueExpression() { + return "foo"; } @Timed @@ -694,16 +925,36 @@ public void getAnnotationForTagValueExpression( public void getAnnotationForArgumentToString(Long param) { } + @Timed + @Override + public Long getAnnotationForArgumentToString() { + return 15L; + } + @Timed @Override public void getMultipleAnnotationsForTagValueExpression( - @MeterTag(key = "value2", expression = "'value2. overridden: ' + value2") DataHolder param) { + @MeterTag(key = "value2", expression = "'value2.overridden: ' + value2") DataHolder param) { + } + + @Timed + @Override + @MeterTag(key = "value2", expression = "'value2. overridden: ' + value2") + public DataHolder getMultipleAnnotationsForTagValueExpression() { + return new DataHolder("zxe", "qwe"); } @Timed @Override public void getMultipleAnnotationsWithContainerForTagValueExpression(@MeterTag(key = "value3", - expression = "'value3. overridden: ' + value1.toUpperCase + value2.toUpperCase") DataHolder param) { + expression = "'value3.overridden: ' + value1.toUpperCase + value2.toUpperCase") DataHolder param) { + } + + @Timed + @MeterTag(key = "value3", expression = "'value3. overridden: ' + value1.toUpperCase + value2.toUpperCase") + @Override + public DataHolder getMultipleAnnotationsWithContainerForTagValueExpression() { + return new DataHolder("zxe", "qwe"); } } @@ -714,6 +965,12 @@ static class MeterTagSuper { public void superMethod(@MeterTag("superTag") String foo) { } + @Timed + @MeterTag("superTag") + public String superMethod() { + return "someValue"; + } + } static class MeterTagSub extends MeterTagSuper { @@ -722,6 +979,12 @@ static class MeterTagSub extends MeterTagSuper { public void subMethod(@MeterTag("subTag") String foo) { } + @Timed + @MeterTag("subTag") + public String subMethod() { + return "someValue"; + } + } private static final class FailingMeterRegistry extends SimpleMeterRegistry { @@ -784,11 +1047,21 @@ CompletableFuture call(GuardedResult guardedResult) { return supplyAsync(guardedResult::get); } + @Timed(value = "callNull", extraTags = { "extra", "tag" }) + CompletableFuture callNull() { + return null; + } + @Timed(value = "longCall", extraTags = { "extra", "tag" }, longTask = true) CompletableFuture longCall(GuardedResult guardedResult) { return supplyAsync(guardedResult::get); } + @Timed(value = "longCallNull", extraTags = { "extra", "tag" }, longTask = true) + CompletableFuture longCallNull() { + return null; + } + } static class GuardedResult { @@ -853,7 +1126,7 @@ public void call() { } - public static final class DataHolder { + static class DataHolder { private final String value1; diff --git a/settings.gradle b/settings.gradle index 5edcb1c779..a54209e01a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,9 +5,9 @@ pluginManagement { } plugins { - id 'com.gradle.develocity' version '3.18.1' + id 'com.gradle.develocity' version '3.19' id 'io.spring.develocity.conventions' version '0.0.22' - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' } rootProject.name = 'micrometer'