55
66package io .opentelemetry .exporter .prometheus ;
77
8- import static io .prometheus .metrics .model .snapshots .PrometheusNaming .prometheusName ;
9- import static io .prometheus .metrics .model .snapshots .PrometheusNaming .sanitizeLabelName ;
10- import static io .prometheus .metrics .model .snapshots .PrometheusNaming .sanitizeMetricName ;
118import static java .util .Objects .requireNonNull ;
129
1310import io .opentelemetry .api .common .AttributeKey ;
@@ -87,6 +84,7 @@ final class Otel2PrometheusConverter {
8784
8885 private final boolean otelScopeLabelsEnabled ;
8986 private final boolean targetInfoMetricEnabled ;
87+ private final TranslationStrategy translationStrategy ;
9088 @ Nullable private final Predicate <String > allowedResourceAttributesFilter ;
9189
9290 /**
@@ -107,9 +105,11 @@ final class Otel2PrometheusConverter {
107105 Otel2PrometheusConverter (
108106 boolean otelScopeLabelsEnabled ,
109107 boolean targetInfoMetricEnabled ,
108+ TranslationStrategy translationStrategy ,
110109 @ Nullable Predicate <String > allowedResourceAttributesFilter ) {
111110 this .otelScopeLabelsEnabled = otelScopeLabelsEnabled ;
112111 this .targetInfoMetricEnabled = targetInfoMetricEnabled ;
112+ this .translationStrategy = translationStrategy ;
113113 this .allowedResourceAttributesFilter = allowedResourceAttributesFilter ;
114114 this .resourceAttributesToAllowedKeysCache =
115115 allowedResourceAttributesFilter != null
@@ -125,6 +125,10 @@ boolean isTargetInfoMetricEnabled() {
125125 return targetInfoMetricEnabled ;
126126 }
127127
128+ TranslationStrategy getTranslationStrategy () {
129+ return translationStrategy ;
130+ }
131+
128132 @ Nullable
129133 Predicate <String > getAllowedResourceAttributesFilter () {
130134 return allowedResourceAttributesFilter ;
@@ -154,11 +158,24 @@ MetricSnapshots convert(@Nullable Collection<MetricData> metricDataCollection) {
154158
155159 @ Nullable
156160 private MetricSnapshot convert (MetricData metricData ) {
161+ try {
162+ return doConvert (metricData );
163+ } catch (IllegalArgumentException e ) {
164+ THROTTLING_LOGGER .log (
165+ Level .WARNING ,
166+ "Failed to convert metric " + metricData .getName () + ". Dropping metric." ,
167+ e );
168+ return null ;
169+ }
170+ }
157171
172+ @ Nullable
173+ private MetricSnapshot doConvert (MetricData metricData ) {
158174 // Note that AggregationTemporality.DELTA should never happen
159175 // because PrometheusMetricReader#getAggregationTemporality returns CUMULATIVE.
160176
161- MetricMetadata metadata = convertMetadata (metricData );
177+ boolean isCounter = isMonotonicSum (metricData );
178+ MetricMetadata metadata = convertMetadata (metricData , isCounter );
162179 InstrumentationScopeInfo scope = metricData .getInstrumentationScopeInfo ();
163180 switch (metricData .getType ()) {
164181 case LONG_GAUGE :
@@ -213,6 +230,17 @@ private MetricSnapshot convert(MetricData metricData) {
213230 return null ;
214231 }
215232
233+ private static boolean isMonotonicSum (MetricData metricData ) {
234+ switch (metricData .getType ()) {
235+ case LONG_SUM :
236+ return metricData .getLongSumData ().isMonotonic ();
237+ case DOUBLE_SUM :
238+ return metricData .getDoubleSumData ().isMonotonic ();
239+ default :
240+ return false ;
241+ }
242+ }
243+
216244 private GaugeSnapshot convertLongGauge (
217245 MetricMetadata metadata ,
218246 InstrumentationScopeInfo scope ,
@@ -479,7 +507,7 @@ private static int labelSetLength(Labels labels) {
479507
480508 private InfoSnapshot makeTargetInfo (Resource resource ) {
481509 return new InfoSnapshot (
482- new MetricMetadata ( "target" ),
510+ MetricMetadata . builder (). name ( "target" ). build ( ),
483511 Collections .singletonList (
484512 new InfoDataPointSnapshot (
485513 convertAttributes (
@@ -582,34 +610,181 @@ private List<AttributeKey<?>> filterAllowedResourceAttributeKeys(@Nullable Resou
582610 return allowedAttributeKeys ;
583611 }
584612
613+ private String convertLabelName (String key ) {
614+ if (shouldEscape (translationStrategy )) {
615+ return convertLegacyLabelName (key );
616+ }
617+ return key ;
618+ }
619+
585620 /**
586- * Convert an attribute key to a legacy Prometheus label name. {@code prometheusName} converts
587- * non-standard characters (dots, dashes, etc.) to underscores, and {@code sanitizeLabelName}
588- * strips invalid leading prefixes.
621+ * Normalize an attribute name to the legacy Prometheus label scheme.
622+ *
623+ * <p>This mirrors {@code prometheus/otlptranslator}'s invalid-character collapsing rules, but we
624+ * intentionally do not preserve {@code __...__} names because Prometheus Java rejects user label
625+ * names that begin with {@code __}.
589626 */
590- private static String convertLabelName (String key ) {
591- return sanitizeLabelName (prometheusName (key ));
627+ private static String convertLegacyLabelName (String key ) {
628+ if (key .isEmpty ()) {
629+ throw new IllegalArgumentException ("label name is empty" );
630+ }
631+
632+ // OTel owns OTLP-to-Prometheus translation. Prometheus Java validates and serializes names,
633+ // but no longer owns this naming mangling. The OTel compatibility spec requires invalid
634+ // attribute-name characters and repeated underscores to collapse to a single "_". Prometheus
635+ // label names beginning with "__" are reserved for internal labels like "__name__" and
636+ // scrape/relabel labels, and Prometheus Java rejects them as user labels, so do not preserve
637+ // "__...__" reserved-looking names here.
638+ StringBuilder result = new StringBuilder (key .length ());
639+ boolean previousWasUnderscore = false ;
640+ for (int i = 0 ; i < key .length (); ) {
641+ int codePoint = key .codePointAt (i );
642+ if (isValidLegacyLabelChar (codePoint )) {
643+ result .appendCodePoint (codePoint );
644+ previousWasUnderscore = false ;
645+ } else if (!previousWasUnderscore ) {
646+ result .append ('_' );
647+ previousWasUnderscore = true ;
648+ }
649+ i += Character .charCount (codePoint );
650+ }
651+
652+ String normalized = result .toString ();
653+ if (Character .isDigit (normalized .charAt (0 ))) {
654+ normalized = "key_" + normalized ;
655+ }
656+ if (containsOnlyUnderscores (normalized )) {
657+ throw new IllegalArgumentException (
658+ "normalization for label name \" "
659+ + key
660+ + "\" resulted in invalid name \" "
661+ + normalized
662+ + "\" " );
663+ }
664+ return normalized ;
665+ }
666+
667+ private static boolean isValidLegacyLabelChar (int codePoint ) {
668+ return (codePoint >= 'a' && codePoint <= 'z' )
669+ || (codePoint >= 'A' && codePoint <= 'Z' )
670+ || (codePoint >= '0' && codePoint <= '9' );
671+ }
672+
673+ private static boolean containsOnlyUnderscores (String value ) {
674+ for (int i = 0 ; i < value .length (); i ++) {
675+ if (value .charAt (i ) != '_' ) {
676+ return false ;
677+ }
678+ }
679+ return true ;
592680 }
593681
594- private static MetricMetadata convertMetadata (MetricData metricData ) {
595- String name = sanitizeMetricName (prometheusName (metricData .getName ()));
682+ private static String convertLegacyMetricName (String name ) {
683+ if (name .isEmpty ()) {
684+ throw new IllegalArgumentException ("metric name is empty" );
685+ }
686+
687+ StringBuilder result = new StringBuilder (name .length ());
688+ boolean previousWasUnderscore = false ;
689+ for (int i = 0 ; i < name .length (); ) {
690+ int codePoint = name .codePointAt (i );
691+ if (isValidLegacyMetricChar (codePoint , i ) && codePoint != '_' ) {
692+ result .appendCodePoint (codePoint );
693+ previousWasUnderscore = false ;
694+ } else if (!previousWasUnderscore ) {
695+ result .append ('_' );
696+ previousWasUnderscore = true ;
697+ }
698+ i += Character .charCount (codePoint );
699+ }
700+ return result .toString ();
701+ }
702+
703+ private static boolean isValidLegacyMetricChar (int codePoint , int index ) {
704+ return (codePoint >= 'a' && codePoint <= 'z' )
705+ || (codePoint >= 'A' && codePoint <= 'Z' )
706+ || codePoint == '_'
707+ || codePoint == ':'
708+ || (codePoint >= '0' && codePoint <= '9' && index > 0 );
709+ }
710+
711+ private MetricMetadata convertMetadata (MetricData metricData , boolean isCounter ) {
712+ switch (translationStrategy ) {
713+ case UNDERSCORE_ESCAPING_WITH_SUFFIXES :
714+ return convertMetadataEscapedWithSuffixes (metricData );
715+ case UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES :
716+ return convertMetadataEscapedWithoutSuffixes (metricData );
717+ case NO_UTF8_ESCAPING_WITH_SUFFIXES :
718+ return convertMetadataUtf8WithSuffixes (metricData , isCounter );
719+ case NO_TRANSLATION :
720+ return convertMetadataNoTranslation (metricData );
721+ }
722+ throw new IllegalStateException ("Unknown strategy: " + translationStrategy );
723+ }
724+
725+ private static MetricMetadata convertMetadataEscapedWithSuffixes (MetricData metricData ) {
726+ String originalName = metricData .getName ();
727+ String name = stripReservedMetricSuffixes (convertLegacyMetricName (originalName ));
596728 String help = metricData .getDescription ();
597729 Unit unit = PrometheusUnitsHelper .convertUnit (metricData .getUnit ());
598730 if (unit != null && !name .endsWith (unit .toString ())) {
599731 name = name + "_" + unit ;
600732 }
601- // Repeated __ are discouraged according to spec, although this is allowed in prometheus, see
602- // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1
603- while (name .contains ("__" )) {
604- name = name .replace ("__" , "_" );
733+ validateNormalizedMetricName (originalName , name );
734+ return MetricMetadata .builder ().name (name ).help (help ).unit (unit ).build ();
735+ }
736+
737+ private static MetricMetadata convertMetadataEscapedWithoutSuffixes (MetricData metricData ) {
738+ String originalName = metricData .getName ();
739+ String name = stripReservedMetricSuffixes (convertLegacyMetricName (originalName ));
740+ String help = metricData .getDescription ();
741+ validateNormalizedMetricName (originalName , name );
742+ return MetricMetadata .builder ().name (name ).help (help ).build ();
743+ }
744+
745+ private static MetricMetadata convertMetadataUtf8WithSuffixes (
746+ MetricData metricData , boolean isCounter ) {
747+ String name = metricData .getName ();
748+ String help = metricData .getDescription ();
749+ Unit unit = PrometheusUnitsHelper .convertUnit (metricData .getUnit ());
750+ if (unit != null && !name .endsWith (unit .toString ())) {
751+ name = name + "_" + unit ;
605752 }
753+ return MetricMetadata .builder ()
754+ .name (stripReservedMetricSuffixes (name ))
755+ .help (help )
756+ .unit (unit )
757+ .counterSuffix (isCounter )
758+ .build ();
759+ }
606760
607- return new MetricMetadata (name , help , unit );
761+ private static MetricMetadata convertMetadataNoTranslation (MetricData metricData ) {
762+ String name = stripReservedMetricSuffixes (metricData .getName ());
763+ String help = metricData .getDescription ();
764+ return MetricMetadata .builder ().name (name ).help (help ).build ();
608765 }
609766
610- private static void putOrMerge (
611- Map <String , MetricSnapshot > snapshotsByName , MetricSnapshot snapshot ) {
612- String name = snapshot .getMetadata ().getPrometheusName ();
767+ private static String stripReservedMetricSuffixes (String name ) {
768+ boolean modified = true ;
769+ while (modified ) {
770+ modified = false ;
771+ for (String suffix : PrometheusUnitsHelper .RESERVED_SUFFIXES ) {
772+ if (name .equals (suffix )) {
773+ return name .substring (1 );
774+ }
775+ if (name .endsWith (suffix )) {
776+ name = name .substring (0 , name .length () - suffix .length ());
777+ modified = true ;
778+ }
779+ }
780+ }
781+ return name ;
782+ }
783+
784+ private void putOrMerge (Map <String , MetricSnapshot > snapshotsByName , MetricSnapshot snapshot ) {
785+ MetricMetadata metadata = snapshot .getMetadata ();
786+ String name =
787+ shouldEscape (translationStrategy ) ? metadata .getPrometheusName () : metadata .getName ();
613788 if (snapshotsByName .containsKey (name )) {
614789 MetricSnapshot merged = merge (snapshotsByName .get (name ), snapshot );
615790 if (merged != null ) {
@@ -620,6 +795,26 @@ private static void putOrMerge(
620795 }
621796 }
622797
798+ private static boolean shouldEscape (TranslationStrategy translationStrategy ) {
799+ return translationStrategy == TranslationStrategy .UNDERSCORE_ESCAPING_WITH_SUFFIXES
800+ || translationStrategy == TranslationStrategy .UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES ;
801+ }
802+
803+ private static void validateNormalizedMetricName (String originalName , String normalizedName ) {
804+ if (normalizedName .isEmpty ()) {
805+ throw new IllegalArgumentException (
806+ "normalization for metric \" " + originalName + "\" resulted in empty name" );
807+ }
808+ if (containsOnlyUnderscores (normalizedName )) {
809+ throw new IllegalArgumentException (
810+ "normalization for metric \" "
811+ + originalName
812+ + "\" resulted in invalid name \" "
813+ + normalizedName
814+ + "\" " );
815+ }
816+ }
817+
623818 /**
624819 * OpenTelemetry may use the same metric name multiple times but in different instrumentation
625820 * scopes. In that case, we try to merge the metrics. They will have different {@code
@@ -695,7 +890,7 @@ private static MetricMetadata mergeMetadata(MetricMetadata a, MetricMetadata b)
695890 + "." );
696891 return null ;
697892 }
698- return new MetricMetadata ( name , help , unit );
893+ return MetricMetadata . builder (). name ( name ). help ( help ). unit ( unit ). build ( );
699894 }
700895
701896 private static String typeString (MetricSnapshot snapshot ) {
0 commit comments