From 674691523089727f8ec45d117e45068ebeb6ca01 Mon Sep 17 00:00:00 2001 From: Liam Clarke-Hutchinson Date: Wed, 25 Mar 2026 14:57:12 +1300 Subject: [PATCH] Relax metric name validation in Dropwizard5 CustomMappingSampleBuilder The MapperConfig and GraphiteNamePattern classes enforced Graphite-style dot-separated naming conventions on input metrics, preventing the CustomMappingSampleBuilder from remapping metrics with underscores, colons, or single-level names. Relaxes the validation regex to accept a broader range of metric name patterns while still rejecting clearly invalid inputs. Fixes #461, #518, #645 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Liam Clarke-Hutchinson --- .../labels/GraphiteNamePattern.java | 9 +++- .../dropwizard5/labels/MapperConfig.java | 15 ++++-- .../labels/GraphiteNamePatternTest.java | 23 +++++++--- .../dropwizard5/labels/MapperConfigTest.java | 46 +++++++++++++++++++ 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePattern.java b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePattern.java index c13ffd91c..7ec0445d0 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePattern.java +++ b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePattern.java @@ -9,13 +9,16 @@ /** * GraphiteNamePattern is initialised with a simplified glob pattern that only allows '*' as special - * character. Examples of valid patterns: + * character. Accepts a broad range of metric name patterns including single-level names, names with + * underscores, hyphens, and colons. Examples of valid patterns: * *
    *
  • org.test.controller.gather.status.400 *
  • org.test.controller.gather.status.* *
  • org.test.controller.*.status.* *
  • *.test.controller.*.status.* + *
  • app_metric_some_count + *
  • io.dropwizard.jetty.MutableServletContextHandler.*-requests *
* *

It contains logic to match a metric name and to extract named parameters from it. @@ -32,6 +35,10 @@ class GraphiteNamePattern { * @param pattern The glob style pattern to be used. */ GraphiteNamePattern(String pattern) throws IllegalArgumentException { + if (pattern.contains("**")) { + throw new IllegalArgumentException( + String.format("Provided pattern [%s] must not contain '**' (double-star glob)", pattern)); + } if (!VALIDATION_PATTERN.matcher(pattern).matches()) { throw new IllegalArgumentException( String.format("Provided pattern [%s] does not matches [%s]", pattern, METRIC_GLOB_REGEX)); diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfig.java b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfig.java index 13d890861..935286cf1 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfig.java +++ b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfig.java @@ -16,11 +16,14 @@ * and new labels based on this config. */ public final class MapperConfig { - // each part of the metric name between dots - private static final String METRIC_PART_REGEX = "[a-zA-Z_0-9](-?[a-zA-Z0-9_])+"; - // Simplified GLOB: we can have "*." at the beginning and "*" only at the end + // Each part of the metric name between dots. Accepts letters, digits, underscores, hyphens, + // colons, and glob wildcards (*) to support a broad range of metric naming conventions. + private static final String METRIC_PART_REGEX = "[a-zA-Z_0-9*][a-zA-Z0-9_:\\-*]*"; + // Simplified GLOB: accepts single-level names, dot-separated names, and glob patterns with '*'. + // The pattern requires at least one non-empty segment and does not allow empty segments (double + // dots) or empty/whitespace-only strings. The '**' glob is rejected separately in validateMatch. static final String METRIC_GLOB_REGEX = - "^(\\*\\.|" + METRIC_PART_REGEX + "\\.)+(\\*|" + METRIC_PART_REGEX + ")$"; + "^(" + METRIC_PART_REGEX + ")(\\." + METRIC_PART_REGEX + ")*$"; // Labels validation. private static final String LABEL_REGEX = "^[a-zA-Z_][a-zA-Z0-9_]+$"; private static final Pattern MATCH_EXPRESSION_PATTERN = Pattern.compile(METRIC_GLOB_REGEX); @@ -109,6 +112,10 @@ public void setLabels(Map labels) { } private void validateMatch(String match) { + if (match.contains("**")) { + throw new IllegalArgumentException( + String.format("Match expression [%s] must not contain '**' (double-star glob)", match)); + } if (!MATCH_EXPRESSION_PATTERN.matcher(match).matches()) { throw new IllegalArgumentException( String.format( diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePatternTest.java b/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePatternTest.java index 100703d94..aa3e20e6b 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePatternTest.java +++ b/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/GraphiteNamePatternTest.java @@ -17,18 +17,12 @@ void createNew_WHEN_InvalidPattern_THEN_ShouldThrowException() { List invalidPatterns = Arrays.asList( "", - "a", - "1org", "1org.", "org.", "org.**", - "org.**", - "org.company-", "org.company-.", - "org.company-*", "org.company.**", "org.company.**-", - "org.com*pany.*", "org.test.contr.oller.gather.status..400", "org.test.controller.gather.status..400"); for (String pattern : invalidPatterns) { @@ -47,7 +41,22 @@ void createNew_WHEN_ValidPattern_THEN_ShouldCreateThePatternSuccessfully() { "org.test.controller.*.status.*", "*.test.controller.*.status.*", "*.test.controller-1.*.status.*", - "*.amazing-test.controller-1.*.status.*"); + "*.amazing-test.controller-1.*.status.*", + // Single-level names (previously rejected, fixes #461) + "a", + "1org", + "app_metric_some_count", + // Names with colons (fixes #645) + "my:metric:name", + "org.company:metric.*", + // Names with hyphens at boundaries + "org.company-", + "org.company-*", + // Embedded glob in segment (fixes #518) + "org.com*pany.*", + "io.dropwizard.jetty.MutableServletContextHandler.*-requests", + // Standalone glob + "*"); for (String pattern : validPatterns) { new GraphiteNamePattern(pattern); } diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfigTest.java b/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfigTest.java index 982e8f24e..dd36ae7db 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfigTest.java +++ b/prometheus-metrics-instrumentation-dropwizard5/src/test/java/io/prometheus/metrics/instrumentation/dropwizard5/labels/MapperConfigTest.java @@ -41,6 +41,52 @@ void setLabels_WHEN_ExpressionDoesnNotMatchPattern_ThrowException() { .isThrownBy(() -> mapperConfig.setLabels(labels)); } + @Test + void setMatch_WHEN_SingleLevelName_AllGood() { + final MapperConfig mapperConfig = new MapperConfig(); + mapperConfig.setMatch("app_metric_some_count"); + assertThat(mapperConfig.getMatch()).isEqualTo("app_metric_some_count"); + } + + @Test + void setMatch_WHEN_NameWithColons_AllGood() { + final MapperConfig mapperConfig = new MapperConfig(); + mapperConfig.setMatch("my:metric:name"); + assertThat(mapperConfig.getMatch()).isEqualTo("my:metric:name"); + } + + @Test + void setMatch_WHEN_EmbeddedGlobInSegment_AllGood() { + final MapperConfig mapperConfig = new MapperConfig(); + mapperConfig.setMatch("io.dropwizard.jetty.MutableServletContextHandler.*-requests"); + assertThat(mapperConfig.getMatch()) + .isEqualTo("io.dropwizard.jetty.MutableServletContextHandler.*-requests"); + } + + @Test + void setMatch_WHEN_DoubleStarGlob_ThrowException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new MapperConfig().setMatch("org.**")); + } + + @Test + void setMatch_WHEN_EmptyString_ThrowException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new MapperConfig().setMatch("")); + } + + @Test + void setMatch_WHEN_TrailingDot_ThrowException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new MapperConfig().setMatch("org.company.")); + } + + @Test + void setMatch_WHEN_DoubleDot_ThrowException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new MapperConfig().setMatch("org..company")); + } + @Test void toString_WHEN_EmptyConfig_AllGood() { final MapperConfig mapperConfig = new MapperConfig();