Skip to content

Fix Prometheus $digit escape for OTel expandconverter#2187

Open
Paamicky wants to merge 8 commits into
mainfrom
prometheus-dollar-escape
Open

Fix Prometheus $digit escape for OTel expandconverter#2187
Paamicky wants to merge 8 commits into
mainfrom
prometheus-dollar-escape

Conversation

@Paamicky

@Paamicky Paamicky commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Description of the issue

On EKS environments, Prometheus relabel_configs that use regex capture group references (e.g., $1, $2 in replacement fields) are silently corrupted. The OTel expandconverter treats bare $digit sequences as environment variable references and replaces them with empty strings, breaking label relabeling rules.

Description of changes

Added escapeDollarDigit to the OTel Prometheus receiver translator. Before the Prometheus config file is unmarshaled, bare $digit sequences are doubled to $$digit. The expandconverter interprets $$ as a literal $, so the values are correctly preserved as $1, $2, etc. when the collector starts.

The same pattern already existed in the Container Insights translator, this change brings Prometheus to parity.

Tests

  • TestEscapeDollarDigit: 10 unit test cases covering bare $1–$9, $0, multi-digit $10, $ followed by non-digit, mixed text, empty string

    • TestPrometheusReceiverTranslatorDollarEscape: integration test using a prometheus config with relabel_configs containing $1 and $1:$2, asserting the
      translator correctly escapes them to $$$$1 and $$$$1:$$$$2 to survive the confmap resolver + expandconverter pipeline
    • Manually validated end-to-end on EC2: agent starts successfully with a prometheus config containing $1 relabel patterns
  • prometheus.yaml:

scrape_configs:
    - job_name: test_app
      static_configs:
        - targets: ['localhost:9100']
      relabel_configs:
        - source_labels: [__name__]
          regex: (.*)
          target_label: __name__
          replacement: $1
  • agent.config:
{
    "agent": { "region": "us-east-1" },
    "opentelemetry": {
      "collect": {
        "prometheus": {
          "config_path": "/home/mcommey/prometheus.yml"
        }
      }
    }
  }
  • agent logs:
(26-07-13 17:41:22) <0> [~]
dev-dsk-mcommey-1e-84492f53 % tail -10 /opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log
2026-07-13T17:41:17Z I! cloudwatch: publish with ForceFlushInterval: 1m0s, Publish Jitter: 53.820199145s
2026-07-13T17:41:17Z W! {"caller":"ec2/ec2.go:128","msg":"failed fetching ec2 instance tags","error":"operation error EC2: DescribeTags, https response error StatusCode: 403, RequestID: e19e8503-bc35-4f01-a753-166f746da32d, api error UnauthorizedOperation: You are not authorized to perform this operation. User: arn:aws:sts::936689364878:assumed-role/DevEnvSSMAgentRole-DO-NOT-DELETE/mi-0df2ee837dd085d78 is not authorized to perform: ec2:DescribeTags because no identity-based policy allows the ec2:DescribeTags action"}
2026-07-13T17:41:17Z I! {"caller":"internal/resourcedetection.go:202","msg":"detected resource information","resource":{"cloud.account.id":"123367104812","cloud.availability_zone":"us-east-1e","cloud.platform":"aws_ec2","cloud.provider":"aws","cloud.region":"us-east-1","host.id":"i-0e0fb09c64acba767","host.image.id":"ami-0cd141609c6b2fecc","host.name":"ip-172-19-148-134.amazon.com","host.type":"c7a.12xlarge"}}
2026-07-13T17:41:17Z I! {"caller":"prometheusreceiver@v0.124.1/metrics_receiver.go:154","msg":"Starting discovery manager"}
2026-07-13T17:41:17Z I! {"caller":"targetallocator/manager.go:262","msg":"Scrape job added","jobName":"test_app"}
2026-07-13T17:41:17Z I! {"caller":"ec2tagger/ec2tagger.go:482","msg":"ec2tagger: Check EC2 Metadata."}
2026-07-13T17:41:17Z I! {"caller":"prometheusreceiver@v0.124.1/metrics_receiver.go:224","msg":"Starting scrape manager"}
2026-07-13T17:41:17Z I! {"caller":"ec2tagger/ec2tagger.go:458","msg":"ec2tagger: EC2 tagger has started, finished initial retrieval of tags and Volumes"}
2026-07-13T17:41:17Z I! {"caller":"systemmetricsreceiver/scraper_linux.go:49","msg":"Starting system metrics scraper","scraper_count":5}
2026-07-13T17:41:17Z I! {"caller":"service@v0.124.0/service.go:289","msg":"Everything is ready. Begin running and processing data."}

License

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Requirements

Before commiting your code, please do the following steps.

  1. Run make fmt and make fmt-sh
  2. Run make lint

Integration Tests

To run integration tests against this PR, add the ready for testing label.

@Paamicky Paamicky self-assigned this Jul 8, 2026
@Paamicky Paamicky added the ready for testing Indicates this PR is ready for integration tests to run label Jul 8, 2026
@Paamicky Paamicky marked this pull request as ready for review July 8, 2026 16:10
@Paamicky Paamicky requested a review from a team as a code owner July 8, 2026 16:10

// escapeDollarDigit escapes bare $ followed by a digit so the expandconverter
// does not interpret regex backreferences (e.g., $1 in relabel_configs) as env vars.
func escapeDollarDigit(s string) string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this is a duplicate of the container insights function - https://github.com/aws/amazon-cloudwatch-agent/blob/main/translator/translate/otel/pipeline/opentelemetry/containerinsights/common.go#L68

this should be extracted in a common package and imported in both container insights and prometheus translations

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During testing I noticed prometheus needed it own funtion. Container insights uses Go templates with rawMapConfig that bypass the confmap resolver, so its single $$ escape is sufficient. Prometheus reads an external file that goes through the full resolver and the expandconverter pipeline, requiring $$$$. The same function won't work for both.

Also, I tested manually, see description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for testing Indicates this PR is ready for integration tests to run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants