From 1c37a05cda732e509a6a757d60094e74c9d8bf9d Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Thu, 27 Aug 2026 11:07:15 +0200 Subject: [PATCH 1/7] config: add ArnFromDimensions to SupportedServices Adds a per-service ArnFromDimensionsFunc field that builds a resource ARN directly from a metric's dimensions, without requiring the resource to be discovered via the Tagging API first. Left nil wherever the canonical ARN can't be trivially derived from dimensions alone -- e.g. it embeds an internal ID CloudWatch doesn't expose (AutoScaling, Kafka), the format has undocumented edge cases (DMS, WAFV2, EventBridge default bus, StorageGateway, API Gateway), or the namespace has no fixed resource type at all. Best-effort, derived from documented AWS ARN formats; not yet validated against live AWS API responses. Signed-off-by: Karsten Jeschkies --- pkg/config/services.go | 251 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/pkg/config/services.go b/pkg/config/services.go index aa35af18d..c267a84b4 100644 --- a/pkg/config/services.go +++ b/pkg/config/services.go @@ -13,6 +13,7 @@ package config import ( + "fmt" "strings" "github.com/aws/aws-sdk-go-v2/aws" @@ -21,6 +22,89 @@ import ( "github.com/prometheus-community/yet-another-cloudwatch-exporter/pkg/model" ) +// ArnFromDimensionsFunc builds a resource ARN directly from a ListMetrics result's dimensions, +// without requiring the resource to have first been discovered through the Tagging API (GetResources). +// dimensions is keyed by the same names used in this file's DimensionRegexps named capture groups +// (e.g. "FunctionName", "TableName", "Directory_ID"), which matches the real AWS CloudWatch dimension +// name for that namespace with spaces replaced by underscores. +// +// Returns false when the supplied dimensions don't contain enough information to build a valid ARN. +// +// These are best-effort, statically derived from documented AWS ARN formats and have not been +// validated against live AWS API responses; verify against real output before relying on them. +type ArnFromDimensionsFunc func(partition, region, accountID string, dimensions map[string]string) (string, bool) + +// arnFromDimensions returns an ArnFromDimensionsFunc that builds a standard +// "arn:{partition}:{service}:{region}:{account}:{resource}" ARN, substituting the named dimensions (in +// order) into resourceFormat's %s verbs. Returns false if any of dimensionNames is missing or empty. +func arnFromDimensions(service, resourceFormat string, dimensionNames ...string) ArnFromDimensionsFunc { + return func(partition, region, accountID string, dimensions map[string]string) (string, bool) { + resource, ok := formatResource(resourceFormat, dimensions, dimensionNames) + if !ok { + return "", false + } + return fmt.Sprintf("arn:%s:%s:%s:%s:%s", partition, service, region, accountID, resource), true + } +} + +// arnFromDimensionsNoRegion is like arnFromDimensions, but for global services whose ARNs omit the +// region segment (e.g. CloudFront, Global Accelerator, Network Manager). +func arnFromDimensionsNoRegion(service, resourceFormat string, dimensionNames ...string) ArnFromDimensionsFunc { + return func(partition, _, accountID string, dimensions map[string]string) (string, bool) { + resource, ok := formatResource(resourceFormat, dimensions, dimensionNames) + if !ok { + return "", false + } + return fmt.Sprintf("arn:%s:%s::%s:%s", partition, service, accountID, resource), true + } +} + +// arnFromDimensionsNoAccount is like arnFromDimensions, but for services whose ARNs omit both the +// region and account segments (e.g. S3, Route 53). +func arnFromDimensionsNoAccount(service, resourceFormat string, dimensionNames ...string) ArnFromDimensionsFunc { + return func(partition, _, _ string, dimensions map[string]string) (string, bool) { + resource, ok := formatResource(resourceFormat, dimensions, dimensionNames) + if !ok { + return "", false + } + return fmt.Sprintf("arn:%s:%s:::%s", partition, service, resource), true + } +} + +func formatResource(resourceFormat string, dimensions map[string]string, dimensionNames []string) (string, bool) { + args := make([]any, 0, len(dimensionNames)) + for _, name := range dimensionNames { + v, ok := dimensions[name] + if !ok || v == "" { + return "", false + } + args = append(args, v) + } + return fmt.Sprintf(resourceFormat, args...), true +} + +// identityArn returns an ArnFromDimensionsFunc for namespaces where CloudWatch already publishes the +// full resource ARN as the dimension's value (e.g. CertificateArn, StateMachineArn). +func identityArn(dimensionName string) ArnFromDimensionsFunc { + return func(_, _, _ string, dimensions map[string]string) (string, bool) { + v, ok := dimensions[dimensionName] + return v, ok && v != "" + } +} + +// firstOf tries each ArnFromDimensionsFunc in order and returns the first one that succeeds. Used for +// namespaces where a metric carries one of several possible resource-identifying dimension sets. +func firstOf(fns ...ArnFromDimensionsFunc) ArnFromDimensionsFunc { + return func(partition, region, accountID string, dimensions map[string]string) (string, bool) { + for _, fn := range fns { + if arn, ok := fn(partition, region, accountID, dimensions); ok { + return arn, true + } + } + return "", false + } +} + // ServiceConfig defines a namespace supported by discovery jobs. type ServiceConfig struct { // Namespace is the formal AWS namespace identification string @@ -38,6 +122,12 @@ type ServiceConfig struct { // In cases where the dimension name has a space, it should be // replaced with an underscore (`_`). DimensionRegexps []*regexp.Regexp + // ArnFromDimensions builds this namespace's resource ARN directly from a metric's dimensions, + // without needing the resource to be discovered via the Tagging API first. It is nil when there's + // no trivial way to derive the ARN from dimensions alone -- e.g. the canonical ARN embeds an + // internal identifier (a GUID/hash) that CloudWatch doesn't expose as a dimension, the ARN + // structure has ambiguous or undocumented edge cases, or the namespace has no fixed resource type. + ArnFromDimensions ArnFromDimensionsFunc } func (sc ServiceConfig) ToModelDimensionsRegexp() []model.DimensionsRegexp { @@ -112,6 +202,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: identityArn("CertificateArn"), }, { Namespace: "AWS/ACMPrivateCA", @@ -122,6 +213,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: identityArn("PrivateCAArn"), }, { Namespace: "AmazonMWAA", @@ -145,6 +237,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":(?Ptargetgroup/.+)"), regexp.MustCompile(":loadbalancer/(?P.+)$"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("elasticloadbalancing", "%s", "TargetGroup"), + arnFromDimensions("elasticloadbalancing", "loadbalancer/%s", "LoadBalancer"), + ), }, { Namespace: "AWS/AppStream", @@ -155,6 +251,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":fleet/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("appstream", "fleet/%s", "FleetName"), }, { Namespace: "AWS/Backup", @@ -165,6 +262,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":backup-vault:(?P[^:]+)"), }, + ArnFromDimensions: arnFromDimensions("backup", "backup-vault:%s", "BackupVaultName"), }, { Namespace: "AWS/ApiGateway", @@ -181,6 +279,9 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile("/apis/(?P[^/]+)/stages/(?P[^/]+)$"), regexp.MustCompile("/apis/(?P[^/]+)/routes/(?P[^/]+)$"), }, + // No trivial reconstruction: API Gateway management ARNs omit the account segment entirely + // (e.g. "arn:aws:apigateway:region::/restapis/id") and the shape varies across REST/HTTP/ + // Websocket/stage/route variants. }, { Namespace: "AWS/AmazonMQ", @@ -191,6 +292,8 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("broker:(?P[^:]+)"), }, + // No trivial reconstruction: unclear whether the "Broker" dimension value alone (name or id) + // is sufficient to build the canonical broker ARN. }, { Namespace: "AWS/AppRunner", @@ -205,6 +308,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("apis/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("appsync", "apis/%s", "GraphQLAPIId"), }, { Namespace: "AWS/Athena", @@ -215,6 +319,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("workgroup/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("athena", "workgroup/%s", "WorkGroup"), }, { Namespace: "AWS/AutoScaling", @@ -222,6 +327,9 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("autoScalingGroupName/(?P[^/]+)"), }, + // No trivial reconstruction: the canonical ASG ARN embeds an internal group ID + // ("autoScalingGroup:{group-id}:autoScalingGroupName/{name}") that CloudWatch doesn't expose + // as a dimension. }, { Namespace: "AWS/ElasticBeanstalk", @@ -233,6 +341,8 @@ var SupportedServices = serviceConfigs{ // arn uses /${ApplicationName}/${EnvironmentName}, but only EnvironmentName is a Metric Dimension regexp.MustCompile("environment/[^/]+/(?P[^/]+)"), }, + // No trivial reconstruction: the ARN also requires ApplicationName, which isn't a CloudWatch + // dimension for this namespace. }, { Namespace: "AWS/Billing", @@ -248,6 +358,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile("keyspace/(?P[^/]+)/table/(?P[^/]+)"), regexp.MustCompile("keyspace/(?P[^/]+)/"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("cassandra", "keyspace/%s/table/%s", "Keyspace", "TableName"), + arnFromDimensions("cassandra", "keyspace/%s", "Keyspace"), + ), }, { Namespace: "AWS/CloudFront", @@ -258,6 +372,8 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("distribution/(?P[^/]+)"), }, + // CloudFront is a global service; its ARNs omit the region segment. + ArnFromDimensions: arnFromDimensionsNoRegion("cloudfront", "distribution/%s", "DistributionId"), }, { Namespace: "AWS/Cognito", @@ -268,6 +384,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("userpool/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("cognito-idp", "userpool/%s", "UserPool"), }, { Namespace: "AWS/DataSync", @@ -280,6 +397,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":task/(?P[^/]+)"), regexp.MustCompile(":agent/(?P[^/]+)"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("datasync", "task/%s", "TaskId"), + arnFromDimensions("datasync", "agent/%s", "AgentId"), + ), }, { Namespace: "AWS/DirectoryService", @@ -290,6 +411,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":directory/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ds", "directory/%s", "Directory_ID"), }, { Namespace: "AWS/DMS", @@ -301,6 +423,8 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile("rep:[^/]+/(?P[^/]+)"), regexp.MustCompile("task:(?P[^/]+)/(?P[^/]+)"), }, + // No trivial reconstruction: the ARN contains an additional segment (before the identifier + // captured above) that isn't exposed as a CloudWatch dimension. }, { Namespace: "AWS/DDoSProtection", @@ -311,6 +435,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.+)"), }, + ArnFromDimensions: identityArn("ResourceArn"), }, { Namespace: "AWS/DocDB", @@ -323,6 +448,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile("cluster:(?P[^/]+)"), regexp.MustCompile("db:(?P[^/]+)"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("rds", "cluster:%s", "DBClusterIdentifier"), + arnFromDimensions("rds", "db:%s", "DBInstanceIdentifier"), + ), }, { Namespace: "AWS/DX", @@ -335,6 +464,11 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":dxlag/(?P[^/]+)"), regexp.MustCompile(":dxvif/(?P[^/]+)"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("directconnect", "dxcon/%s", "ConnectionId"), + arnFromDimensions("directconnect", "dxlag/%s", "LagId"), + arnFromDimensions("directconnect", "dxvif/%s", "VirtualInterfaceId"), + ), }, { Namespace: "AWS/DynamoDB", @@ -345,6 +479,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":table/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("dynamodb", "table/%s", "TableName"), }, { Namespace: "AWS/EBS", @@ -355,6 +490,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("volume/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "volume/%s", "VolumeId"), }, { Namespace: "AWS/ElastiCache", @@ -367,6 +503,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile("cluster:(?P[^/]+)"), regexp.MustCompile("serverlesscache:(?P[^/]+)"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("elasticache", "cluster:%s", "CacheClusterId"), + arnFromDimensions("elasticache", "serverlesscache:%s", "clusterId"), + ), }, { Namespace: "AWS/MemoryDB", @@ -377,6 +517,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("cluster/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("memorydb", "cluster/%s", "ClusterName"), }, { Namespace: "AWS/EC2", @@ -387,6 +528,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("instance/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "instance/%s", "InstanceId"), }, { Namespace: "AWS/EC2Spot", @@ -394,6 +536,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "spot-fleet-request/%s", "FleetRequestId"), }, { Namespace: "AWS/EC2CapacityReservations", @@ -401,6 +544,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":capacity-reservation/(?P)$"), }, + ArnFromDimensions: arnFromDimensions("ec2", "capacity-reservation/%s", "CapacityReservationId"), }, { Namespace: "AWS/ECS", @@ -413,6 +557,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":cluster/(?P[^/]+)$"), regexp.MustCompile(":service/(?P[^/]+)/(?P[^/]+)$"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("ecs", "service/%s/%s", "ClusterName", "ServiceName"), + arnFromDimensions("ecs", "cluster/%s", "ClusterName"), + ), }, { Namespace: "ECS/ContainerInsights", @@ -427,6 +575,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":cluster/(?P[^/]+)$"), regexp.MustCompile(":service/(?P[^/]+)/(?P[^/]+)$"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("ecs", "service/%s/%s", "ClusterName", "ServiceName"), + arnFromDimensions("ecs", "cluster/%s", "ClusterName"), + ), }, { Namespace: "ContainerInsights", @@ -437,6 +589,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":cluster/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("eks", "cluster/%s", "ClusterName"), }, { Namespace: "AWS/EFS", @@ -447,6 +600,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("file-system/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("elasticfilesystem", "file-system/%s", "FileSystemId"), }, { Namespace: "AWS/EKS", @@ -457,6 +611,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":cluster/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("eks", "cluster/%s", "ClusterName"), }, { Namespace: "AWS/ELB", @@ -467,6 +622,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":loadbalancer/(?P.+)$"), }, + ArnFromDimensions: arnFromDimensions("elasticloadbalancing", "loadbalancer/%s", "LoadBalancerName"), }, { Namespace: "AWS/ElasticMapReduce", @@ -477,6 +633,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("cluster/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("elasticmapreduce", "cluster/%s", "JobFlowId"), }, { Namespace: "AWS/EMRServerless", @@ -487,6 +644,9 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("applications/(?P[^/]+)"), }, + // No trivial reconstruction: EMR Serverless ARNs are documented with a leading slash after the + // account segment ("arn:aws:emr-serverless:region:account:/applications/id"), which the + // standard template doesn't produce and hasn't been verified here. }, { Namespace: "AWS/ES", @@ -497,6 +657,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":domain/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("es", "domain/%s", "DomainName"), }, { Namespace: "AWS/Firehose", @@ -507,6 +668,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":deliverystream/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("firehose", "deliverystream/%s", "DeliveryStreamName"), }, { Namespace: "AWS/FSx", @@ -517,6 +679,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("file-system/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("fsx", "file-system/%s", "FileSystemId"), }, { Namespace: "AWS/GameLift", @@ -527,6 +690,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":fleet/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("gamelift", "fleet/%s", "FleetId"), }, { Namespace: "AWS/GatewayELB", @@ -538,6 +702,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":(?Ptargetgroup/.+)"), regexp.MustCompile(":loadbalancer/(?P.+)$"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("elasticloadbalancing", "%s", "TargetGroup"), + arnFromDimensions("elasticloadbalancing", "loadbalancer/%s", "LoadBalancer"), + ), }, { Namespace: "AWS/GlobalAccelerator", @@ -550,6 +718,9 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile("accelerator/(?P[^/]+)/listener/(?P[^/]+)$"), regexp.MustCompile("accelerator/(?P[^/]+)/listener/(?P[^/]+)/endpoint-group/(?P[^/]+)$"), }, + // Global Accelerator is a global service; its ARNs omit the region segment. Only the top-level + // accelerator is covered here -- listener/endpoint-group sub-resource ARN nesting isn't. + ArnFromDimensions: arnFromDimensionsNoRegion("globalaccelerator", "accelerator/%s", "Accelerator"), }, { Namespace: "Glue", @@ -560,6 +731,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":job/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("glue", "job/%s", "JobName"), }, { Namespace: "AWS/IoT", @@ -572,6 +744,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":rule/(?P[^/]+)"), regexp.MustCompile(":provisioningtemplate/(?P[^/]+)"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("iot", "rule/%s", "RuleName"), + arnFromDimensions("iot", "provisioningtemplate/%s", "TemplateName"), + ), }, { Namespace: "AWS/Kafka", @@ -582,6 +758,8 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":cluster/(?P[^/]+)"), }, + // No trivial reconstruction: the canonical MSK cluster ARN embeds a UUID after the cluster + // name that CloudWatch doesn't expose as a dimension. }, { Namespace: "AWS/KafkaConnect", @@ -592,6 +770,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":connector/(?P[^/]+)"), }, + // No trivial reconstruction: same UUID-suffix issue as AWS/Kafka. }, { Namespace: "AWS/Kinesis", @@ -602,6 +781,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":stream/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("kinesis", "stream/%s", "StreamName"), }, { Namespace: "AWS/KinesisAnalytics", @@ -612,6 +792,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":application/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("kinesisanalytics", "application/%s", "Application"), }, { Namespace: "AWS/KMS", @@ -622,6 +803,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":key/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("kms", "key/%s", "KeyId"), }, { Namespace: "AWS/Lambda", @@ -632,6 +814,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":function:(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("lambda", "function:%s", "FunctionName"), }, { Namespace: "AWS/Logs", @@ -642,6 +825,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":log-group:(?P.+)"), }, + ArnFromDimensions: arnFromDimensions("logs", "log-group:%s", "LogGroupName"), }, { Namespace: "AWS/MediaConnect", @@ -656,6 +840,11 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile("^(?P.*:source:.*)$"), regexp.MustCompile("^(?P.*:output:.*)$"), }, + ArnFromDimensions: firstOf( + identityArn("FlowARN"), + identityArn("SourceARN"), + identityArn("OutputARN"), + ), }, { Namespace: "AWS/MediaConvert", @@ -666,6 +855,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*:.*:mediaconvert:.*:queues/.*)$"), }, + ArnFromDimensions: identityArn("Queue"), }, { Namespace: "AWS/MediaPackage", @@ -679,6 +869,8 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":channels/(?P.+)$"), regexp.MustCompile(":packaging-configurations/(?P.+)$"), }, + // No trivial reconstruction: unclear whether the "IngestEndpoint" dimension value maps + // directly to its parent channel's ARN identifier. }, { Namespace: "AWS/MediaLive", @@ -689,6 +881,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":channel:(?P.+)$"), }, + ArnFromDimensions: arnFromDimensions("medialive", "channel:%s", "ChannelId"), }, { Namespace: "AWS/MediaTailor", @@ -699,6 +892,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("playbackConfiguration/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("mediatailor", "playbackConfiguration/%s", "ConfigurationName"), }, { Namespace: "AWS/Neptune", @@ -711,6 +905,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":cluster:(?P[^/]+)"), regexp.MustCompile(":db:(?P[^/]+)"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("rds", "cluster:%s", "DBClusterIdentifier"), + arnFromDimensions("rds", "db:%s", "DBInstanceIdentifier"), + ), }, { Namespace: "AWS/NetworkFirewall", @@ -721,6 +919,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("firewall/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("network-firewall", "firewall/%s", "FirewallName"), }, { Namespace: "AWS/NATGateway", @@ -731,6 +930,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("natgateway/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "natgateway/%s", "NatGatewayId"), }, { Namespace: "AWS/NetworkELB", @@ -743,6 +943,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":(?Ptargetgroup/.+)"), regexp.MustCompile(":loadbalancer/(?P.+)$"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("elasticloadbalancing", "%s", "TargetGroup"), + arnFromDimensions("elasticloadbalancing", "loadbalancer/%s", "LoadBalancer"), + ), }, { Namespace: "AWS/PrivateLinkEndpoints", @@ -753,6 +957,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":vpc-endpoint/(?P.+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "vpc-endpoint/%s", "VPC_Endpoint_Id"), }, { Namespace: "AWS/PrivateLinkServices", @@ -763,6 +968,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":vpc-endpoint-service/(?P.+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "vpc-endpoint-service/%s", "Service_Id"), }, { Namespace: "AWS/Prometheus", @@ -777,6 +983,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":ledger/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("qldb", "ledger/%s", "LedgerName"), }, { Namespace: "AWS/QuickSight", @@ -795,6 +1002,11 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":db:(?P[^/]+)"), regexp.MustCompile(":db-proxy:(?P[^/]+)"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("rds", "cluster:%s", "DBClusterIdentifier"), + arnFromDimensions("rds", "db:%s", "DBInstanceIdentifier"), + arnFromDimensions("rds", "db-proxy:%s", "ProxyIdentifier"), + ), }, { Namespace: "AWS/Redshift", @@ -805,6 +1017,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":cluster:(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("redshift", "cluster:%s", "ClusterIdentifier"), }, { Namespace: "AWS/Redshift-Serverless", @@ -823,6 +1036,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":resolver-endpoint/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("route53resolver", "resolver-endpoint/%s", "EndpointId"), }, { Namespace: "AWS/Route53", @@ -833,6 +1047,8 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":healthcheck/(?P[^/]+)"), }, + // Route 53 is a global service; its ARNs omit both the region and account segments. + ArnFromDimensions: arnFromDimensionsNoAccount("route53", "healthcheck/%s", "HealthCheckId"), }, { Namespace: "AWS/RUM", @@ -847,6 +1063,8 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P[^:]+)$"), }, + // S3 bucket ARNs omit both the region and account segments. + ArnFromDimensions: arnFromDimensionsNoAccount("s3", "%s", "BucketName"), }, { Namespace: "AWS/Scheduler", @@ -877,6 +1095,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: identityArn("StateMachineArn"), }, { Namespace: "AWS/SNS", @@ -887,6 +1106,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P[^:]+)$"), }, + ArnFromDimensions: arnFromDimensions("sns", "%s", "TopicName"), }, { Namespace: "AWS/SQS", @@ -897,6 +1117,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P[^:]+)$"), }, + ArnFromDimensions: arnFromDimensions("sqs", "%s", "QueueName"), }, { Namespace: "AWS/StorageGateway", @@ -909,6 +1130,9 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":share/(?P[^:]+)$"), regexp.MustCompile("^(?P[^:/]+)/(?P[^:]+)$"), }, + // No trivial reconstruction: the three regex variants disagree on what "GatewayId" contains + // (a bare ID vs. a compound "id/name" string), so a single dimension-based template would be + // wrong for at least one of them. }, { Namespace: "AWS/Transfer", @@ -924,6 +1148,7 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":transit-gateway/(?P[^/]+)"), regexp.MustCompile("(?P[^/]+)/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "transit-gateway/%s", "TransitGateway"), }, { Namespace: "AWS/TrustedAdvisor", @@ -938,6 +1163,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":vpn-connection/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "vpn-connection/%s", "VpnId"), }, { Namespace: "AWS/ClientVPN", @@ -948,6 +1174,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":client-vpn-endpoint/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "client-vpn-endpoint/%s", "Endpoint"), }, { Namespace: "AWS/WAFV2", @@ -958,6 +1185,8 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("/webacl/(?P[^/]+)"), }, + // No trivial reconstruction: WAFV2 ARNs require a scope (REGIONAL/CLOUDFRONT) and an internal + // UUID that CloudWatch doesn't expose as a dimension. }, { Namespace: "AWS/WorkSpaces", @@ -970,6 +1199,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":workspace/(?P[^/]+)$"), regexp.MustCompile(":directory/(?P[^/]+)$"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("workspaces", "workspace/%s", "WorkspaceId"), + arnFromDimensions("workspaces", "directory/%s", "DirectoryId"), + ), }, { Namespace: "AWS/AOSS", @@ -980,6 +1213,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":collection/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("aoss", "collection/%s", "CollectionId"), }, { Namespace: "AWS/SageMaker", @@ -992,6 +1226,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":endpoint/(?P[^/]+)$"), regexp.MustCompile(":inference-component/(?P[^/]+)$"), }, + ArnFromDimensions: firstOf( + arnFromDimensions("sagemaker", "endpoint/%s", "EndpointName"), + arnFromDimensions("sagemaker", "inference-component/%s", "InferenceComponentName"), + ), }, { Namespace: "/aws/sagemaker/Endpoints", @@ -1002,6 +1240,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":endpoint/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("sagemaker", "endpoint/%s", "EndpointName"), }, { Namespace: "/aws/sagemaker/InferenceComponents", @@ -1012,6 +1251,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":inference-component/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("sagemaker", "inference-component/%s", "InferenceComponentName"), }, { Namespace: "/aws/sagemaker/TrainingJobs", @@ -1043,6 +1283,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":inference-recommendations-job/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("sagemaker", "inference-recommendations-job/%s", "JobName"), }, { Namespace: "AWS/Sagemaker/ModelBuildingPipeline", @@ -1053,6 +1294,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":pipeline/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("sagemaker", "pipeline/%s", "PipelineName"), }, { Namespace: "AWS/IPAM", @@ -1063,6 +1305,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":ipam-pool/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("ec2", "ipam-pool/%s", "IpamPoolId"), }, { Namespace: "AWS/Bedrock", @@ -1077,6 +1320,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.+)"), }, + ArnFromDimensions: identityArn("AgentAliasArn"), }, { Namespace: "AWS/Bedrock/Guardrails", @@ -1087,6 +1331,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.+)"), }, + ArnFromDimensions: identityArn("GuardrailArn"), }, { Namespace: "AWS/Events", @@ -1098,6 +1343,9 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":rule/(?P[^/]+)/(?P[^/]+)$"), regexp.MustCompile(":rule/aws.partner/(?P.+)/(?P[^/]+)$"), }, + // No trivial reconstruction: rules on the default event bus omit the event-bus segment + // entirely from the ARN, and it's not verified here whether the "EventBusName" dimension + // reports a sentinel value or is simply absent in that case. }, { Namespace: "AWS/VpcLattice", @@ -1108,6 +1356,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":service/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("vpc-lattice", "service/%s", "Service"), }, { Namespace: "AWS/Network Manager", @@ -1118,5 +1367,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":core-network/(?P[^/]+)$"), }, + // Network Manager is a global service; its ARNs omit the region segment. + ArnFromDimensions: arnFromDimensionsNoRegion("networkmanager", "core-network/%s", "CoreNetwork"), }, } From 35fd445e0de77604b5e304dd800d5a792d35bb87 Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Thu, 27 Aug 2026 13:29:11 +0200 Subject: [PATCH 2/7] config: add AWS/Prometheus, AWS/Scheduler, AWS/ECR DimensionRegexps and ArnFromDimensions Verified against live resources in a sandbox account: - AWS/Prometheus (amp): "Workspace" dimension is the workspace ID, maps directly onto the ARN (service segment is "aps", not "prometheus"). - AWS/Scheduler: metrics are only dimensioned by "ScheduleGroup", not per-schedule -- the ARN this reconstructs is the schedule group's. - AWS/ECR: "RepositoryName" dimension is the repository name. Also confirmed AWS/Redshift-Serverless can't be covered by either DimensionRegexps or ArnFromDimensions: its "Namespace"/"Workgroup" dimensions carry the resource name, but the canonical ARN embeds an internal UUID that isn't derivable from the name. Signed-off-by: Karsten Jeschkies --- pkg/config/services.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/config/services.go b/pkg/config/services.go index c267a84b4..44bf8d771 100644 --- a/pkg/config/services.go +++ b/pkg/config/services.go @@ -971,8 +971,17 @@ var SupportedServices = serviceConfigs{ ArnFromDimensions: arnFromDimensions("ec2", "vpc-endpoint-service/%s", "Service_Id"), }, { + // Verified against a live workspace: the "Workspace" dimension is the workspace ID and maps + // directly onto the ARN (service segment is "aps", not "prometheus"). Namespace: "AWS/Prometheus", Alias: "amp", + ResourceFilters: []*string{ + aws.String("aps:workspace"), + }, + DimensionRegexps: []*regexp.Regexp{ + regexp.MustCompile(":workspace/(?P[^/]+)"), + }, + ArnFromDimensions: arnFromDimensions("aps", "workspace/%s", "Workspace"), }, { Namespace: "AWS/QLDB", @@ -1067,12 +1076,29 @@ var SupportedServices = serviceConfigs{ ArnFromDimensions: arnFromDimensionsNoAccount("s3", "%s", "BucketName"), }, { + // Verified against a live schedule: CloudWatch only publishes AWS/Scheduler metrics dimensioned + // by "ScheduleGroup" -- there's no per-schedule dimension at all, so the resource this + // reconstructs is the schedule group, not an individual schedule. Namespace: "AWS/Scheduler", Alias: "scheduler", + ResourceFilters: []*string{ + aws.String("scheduler:schedule-group"), + }, + DimensionRegexps: []*regexp.Regexp{ + regexp.MustCompile(":schedule-group/(?P[^/]+)"), + }, + ArnFromDimensions: arnFromDimensions("scheduler", "schedule-group/%s", "ScheduleGroup"), }, { Namespace: "AWS/ECR", Alias: "ecr", + ResourceFilters: []*string{ + aws.String("ecr:repository"), + }, + DimensionRegexps: []*regexp.Regexp{ + regexp.MustCompile(":repository/(?P.+)$"), + }, + ArnFromDimensions: arnFromDimensions("ecr", "repository/%s", "RepositoryName"), }, { Namespace: "AWS/Timestream", From e28644134645d7271a55479fc1de5a10cae02e6d Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Thu, 27 Aug 2026 13:54:58 +0200 Subject: [PATCH 3/7] config: document why AWS/Redshift-Serverless has no ArnFromDimensions Signed-off-by: Karsten Jeschkies --- pkg/config/services.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/config/services.go b/pkg/config/services.go index 44bf8d771..ce532abe5 100644 --- a/pkg/config/services.go +++ b/pkg/config/services.go @@ -1029,6 +1029,11 @@ var SupportedServices = serviceConfigs{ ArnFromDimensions: arnFromDimensions("redshift", "cluster:%s", "ClusterIdentifier"), }, { + // Verified against a live namespace/workgroup: CloudWatch's "Namespace" and "Workgroup" + // dimensions carry the resource *name* (e.g. "my-workgroup"), but the canonical ARN embeds an + // internal UUID instead of the name (e.g. "workgroup/3c63945d-3bea-4863-8c5c-7d0b5b285288"). + // The UUID isn't derivable from the name without an API call, so neither DimensionRegexps nor + // ArnFromDimensions can bridge this -- same failure mode as AWS/AutoScaling and AWS/Kafka. Namespace: "AWS/Redshift-Serverless", Alias: "redshift", ResourceFilters: []*string{ From 4bad72409bdf858daa00835ed4a29a288f87a33f Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Thu, 27 Aug 2026 15:04:33 +0200 Subject: [PATCH 4/7] config: add AWS/Timestream, AWS/RUM, AWS/AppRunner ArnFromDimensions Verified against live resources in a sandbox account: - AWS/Timestream: "DatabaseName"/"TableName" dimensions map directly onto the ARN structure. - AWS/RUM: "application_name" dimension is the app monitor name (only appears once a CloudWatch metrics destination is explicitly configured via PutRumMetricsDestination -- it's not on by default). - AWS/AppRunner: uniquely among the UUID-suffixed ARN services checked so far, CloudWatch publishes both "ServiceName" and "ServiceID" as separate dimensions, so the full ARN (which needs both) is reconstructable. AWS/Transfer, and the three SageMaker job-type namespaces, remain unresolved: Transfer needs real SFTP transfer activity to emit any metrics at all, and two separate SageMaker training jobs run to completion produced zero metrics under any SageMaker namespace with a bare (non-framework) container. Signed-off-by: Karsten Jeschkies --- pkg/config/services.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/config/services.go b/pkg/config/services.go index ce532abe5..0536454a9 100644 --- a/pkg/config/services.go +++ b/pkg/config/services.go @@ -298,6 +298,13 @@ var SupportedServices = serviceConfigs{ { Namespace: "AWS/AppRunner", Alias: "apprunner", + ResourceFilters: []*string{ + aws.String("apprunner:service"), + }, + DimensionRegexps: []*regexp.Regexp{ + regexp.MustCompile(":service/(?P[^/]+)/(?P[^/]+)$"), + }, + ArnFromDimensions: arnFromDimensions("apprunner", "service/%s/%s", "ServiceName", "ServiceID"), }, { Namespace: "AWS/AppSync", @@ -1067,6 +1074,13 @@ var SupportedServices = serviceConfigs{ { Namespace: "AWS/RUM", Alias: "rum", + ResourceFilters: []*string{ + aws.String("rum:appmonitor"), + }, + // Real CloudWatch dimension name is "application_name" (lowercase with underscore), not a + // space-converted PascalCase name -- no DimensionRegexps entry to avoid this file's usual + // underscore-means-space convention mangling it into "application name". + ArnFromDimensions: arnFromDimensions("rum", "appmonitor/%s", "application_name"), }, { Namespace: "AWS/S3", @@ -1108,6 +1122,18 @@ var SupportedServices = serviceConfigs{ { Namespace: "AWS/Timestream", Alias: "timestream", + ResourceFilters: []*string{ + aws.String("timestream:database"), + aws.String("timestream:table"), + }, + DimensionRegexps: []*regexp.Regexp{ + regexp.MustCompile(":database/(?P[^/]+)/table/(?P[^/]+)"), + regexp.MustCompile(":database/(?P[^/]+)$"), + }, + ArnFromDimensions: firstOf( + arnFromDimensions("timestream", "database/%s/table/%s", "DatabaseName", "TableName"), + arnFromDimensions("timestream", "database/%s", "DatabaseName"), + ), }, { Namespace: "AWS/SecretsManager", From 936b0482f7f3a109caa54a1da2c8c19aa18ee665 Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Thu, 27 Aug 2026 15:08:59 +0200 Subject: [PATCH 5/7] config: document why the genuinely resource-less namespaces have no ArnFromDimensions Signed-off-by: Karsten Jeschkies --- pkg/config/services.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/config/services.go b/pkg/config/services.go index 0536454a9..2fa142b6f 100644 --- a/pkg/config/services.go +++ b/pkg/config/services.go @@ -186,10 +186,13 @@ func (sc serviceConfigs) getServiceByAlias(alias string) *ServiceConfig { var SupportedServices = serviceConfigs{ { + // Generic namespace for custom metrics pushed by the CloudWatch Agent -- no fixed resource + // type, so there's no ARN to reconstruct. Namespace: "CWAgent", Alias: "cwagent", }, { + // Account/service-level API usage and quota metrics, not tied to a discrete resource. Namespace: "AWS/Usage", Alias: "usage", }, @@ -352,6 +355,7 @@ var SupportedServices = serviceConfigs{ // dimension for this namespace. }, { + // Account-level billing/cost metrics, not tied to a discrete resource. Namespace: "AWS/Billing", Alias: "billing", }, @@ -1208,6 +1212,7 @@ var SupportedServices = serviceConfigs{ ArnFromDimensions: arnFromDimensions("ec2", "transit-gateway/%s", "TransitGateway"), }, { + // Trusted Advisor check-result metrics, not resource metrics -- no ARN to reconstruct. Namespace: "AWS/TrustedAdvisor", Alias: "trustedadvisor", }, @@ -1365,6 +1370,9 @@ var SupportedServices = serviceConfigs{ ArnFromDimensions: arnFromDimensions("ec2", "ipam-pool/%s", "IpamPoolId"), }, { + // On-demand foundation model invocation metrics; foundation models are AWS-owned, not a + // discrete customer resource with an ARN in this account (unlike Bedrock/Agents and + // Bedrock/Guardrails below, which are customer-owned resources). Namespace: "AWS/Bedrock", Alias: "bedrock", }, From 93b2fa8301c5b7eb91a9e4e9ceb9d14efea01049 Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Fri, 28 Aug 2026 10:15:37 +0200 Subject: [PATCH 6/7] feat: Use ARN fallback is available. Signed-off-by: Karsten Jeschkies --- pkg/config/scrapeconf.go | 1 + pkg/config/services.go | 52 ++++++++++++++++++++++++++++++--------- pkg/job/discovery.go | 29 ++++++++++++++-------- pkg/job/discovery_test.go | 7 +++++- pkg/job/scrape.go | 1 + pkg/model/model.go | 3 +++ 6 files changed, 71 insertions(+), 22 deletions(-) diff --git a/pkg/config/scrapeconf.go b/pkg/config/scrapeconf.go index db4c155bd..3724f5d35 100644 --- a/pkg/config/scrapeconf.go +++ b/pkg/config/scrapeconf.go @@ -466,6 +466,7 @@ func (c *ScrapeConf) toModelConfig() model.JobsConfig { job.IncludeContextOnInfoMetrics = discoveryJob.IncludeContextOnInfoMetrics job.DimensionsRegexps = svc.ToModelDimensionsRegexp() job.EnhancedMetrics = svc.toModelEnhancedMetricsConfig(discoveryJob.EnhancedMetrics) + job.ArnFallback = svc.toArnFallback() job.ExportedTagsOnMetrics = []string{} if len(c.Discovery.ExportedTagsOnMetrics) > 0 { diff --git a/pkg/config/services.go b/pkg/config/services.go index 2fa142b6f..43708f6db 100644 --- a/pkg/config/services.go +++ b/pkg/config/services.go @@ -32,42 +32,58 @@ import ( // // These are best-effort, statically derived from documented AWS ARN formats and have not been // validated against live AWS API responses; verify against real output before relying on them. -type ArnFromDimensionsFunc func(partition, region, accountID string, dimensions map[string]string) (string, bool) +type ArnFromDimensionsFunc func(region, accountID string, dimensions map[string]string) (string, bool) + +// partitionForRegion infers the AWS partition from a region name, the same way the AWS SDK's own +// partition resolver does: region prefix determines partition. Region is always known at the call +// site (it's what ListMetrics/GetResources were just queried against), so there's no need for a +// caller-supplied partition that could disagree with it. +func partitionForRegion(region string) string { + switch { + case strings.HasPrefix(region, "cn-"): + return "aws-cn" + case strings.HasPrefix(region, "us-gov-"): + return "aws-us-gov" + default: + return "aws" + } +} // arnFromDimensions returns an ArnFromDimensionsFunc that builds a standard // "arn:{partition}:{service}:{region}:{account}:{resource}" ARN, substituting the named dimensions (in // order) into resourceFormat's %s verbs. Returns false if any of dimensionNames is missing or empty. func arnFromDimensions(service, resourceFormat string, dimensionNames ...string) ArnFromDimensionsFunc { - return func(partition, region, accountID string, dimensions map[string]string) (string, bool) { + return func(region, accountID string, dimensions map[string]string) (string, bool) { resource, ok := formatResource(resourceFormat, dimensions, dimensionNames) if !ok { return "", false } - return fmt.Sprintf("arn:%s:%s:%s:%s:%s", partition, service, region, accountID, resource), true + return fmt.Sprintf("arn:%s:%s:%s:%s:%s", partitionForRegion(region), service, region, accountID, resource), true } } // arnFromDimensionsNoRegion is like arnFromDimensions, but for global services whose ARNs omit the -// region segment (e.g. CloudFront, Global Accelerator, Network Manager). +// region segment (e.g. CloudFront, Global Accelerator, Network Manager). Partition is still inferred +// from region, since the region the metric was fetched from still tells us which partition it's in. func arnFromDimensionsNoRegion(service, resourceFormat string, dimensionNames ...string) ArnFromDimensionsFunc { - return func(partition, _, accountID string, dimensions map[string]string) (string, bool) { + return func(region, accountID string, dimensions map[string]string) (string, bool) { resource, ok := formatResource(resourceFormat, dimensions, dimensionNames) if !ok { return "", false } - return fmt.Sprintf("arn:%s:%s::%s:%s", partition, service, accountID, resource), true + return fmt.Sprintf("arn:%s:%s::%s:%s", partitionForRegion(region), service, accountID, resource), true } } // arnFromDimensionsNoAccount is like arnFromDimensions, but for services whose ARNs omit both the // region and account segments (e.g. S3, Route 53). func arnFromDimensionsNoAccount(service, resourceFormat string, dimensionNames ...string) ArnFromDimensionsFunc { - return func(partition, _, _ string, dimensions map[string]string) (string, bool) { + return func(region, _ string, dimensions map[string]string) (string, bool) { resource, ok := formatResource(resourceFormat, dimensions, dimensionNames) if !ok { return "", false } - return fmt.Sprintf("arn:%s:%s:::%s", partition, service, resource), true + return fmt.Sprintf("arn:%s:%s:::%s", partitionForRegion(region), service, resource), true } } @@ -86,7 +102,7 @@ func formatResource(resourceFormat string, dimensions map[string]string, dimensi // identityArn returns an ArnFromDimensionsFunc for namespaces where CloudWatch already publishes the // full resource ARN as the dimension's value (e.g. CertificateArn, StateMachineArn). func identityArn(dimensionName string) ArnFromDimensionsFunc { - return func(_, _, _ string, dimensions map[string]string) (string, bool) { + return func(_, _ string, dimensions map[string]string) (string, bool) { v, ok := dimensions[dimensionName] return v, ok && v != "" } @@ -95,9 +111,9 @@ func identityArn(dimensionName string) ArnFromDimensionsFunc { // firstOf tries each ArnFromDimensionsFunc in order and returns the first one that succeeds. Used for // namespaces where a metric carries one of several possible resource-identifying dimension sets. func firstOf(fns ...ArnFromDimensionsFunc) ArnFromDimensionsFunc { - return func(partition, region, accountID string, dimensions map[string]string) (string, bool) { + return func(region, accountID string, dimensions map[string]string) (string, bool) { for _, fn := range fns { - if arn, ok := fn(partition, region, accountID, dimensions); ok { + if arn, ok := fn(region, accountID, dimensions); ok { return arn, true } } @@ -164,6 +180,20 @@ func (sc ServiceConfig) toModelEnhancedMetricsConfig(ems []*EnhancedMetric) []*m return emc } +func (sc ServiceConfig) toArnFallback() model.ArnFallbackFunc { + if sc.ArnFromDimensions == nil { + return nil + } + + return func(region, accountID string, dimensions []model.Dimension) (string, bool) { + dimensionsMap := make(map[string]string) + for _, d := range dimensions { + dimensionsMap[d.Name] = d.Value + } + return sc.ArnFromDimensions(region, accountID, dimensionsMap) + } +} + type serviceConfigs []ServiceConfig func (sc serviceConfigs) GetService(serviceType string) *ServiceConfig { diff --git a/pkg/job/discovery.go b/pkg/job/discovery.go index 09245fb67..2b16ba063 100644 --- a/pkg/job/discovery.go +++ b/pkg/job/discovery.go @@ -52,6 +52,7 @@ func runDiscoveryJob( ctx context.Context, logger *slog.Logger, job model.DiscoveryJob, + accountID string, region string, clientTag tagging.Client, clientCloudwatch cloudwatch.Client, @@ -76,7 +77,7 @@ func runDiscoveryJob( } svc := config.SupportedServices.GetService(job.Namespace) - metricData := getMetricDataForQueries(ctx, logger, job, svc, clientCloudwatch, resources) + metricData := getMetricDataForQueries(ctx, logger, job, svc, clientCloudwatch, resources, accountID, region) if len(metricData) > 0 && svc != nil { metricData, err = gmdProcessor.Run(ctx, svc.Namespace, metricData) @@ -127,6 +128,8 @@ func getMetricDataForQueries( svc *config.ServiceConfig, clientCloudwatch cloudwatch.Client, resources []*model.TaggedResource, + accountID, + region string, ) []*model.CloudwatchData { mux := &sync.Mutex{} var getMetricDatas []*model.CloudwatchData @@ -150,7 +153,7 @@ func getMetricDataForQueries( defer wg.Done() err := clientCloudwatch.ListMetrics(ctx, svc.Namespace, metric, discoveryJob.RecentlyActiveOnly, func(page []*model.Metric) { - data := getFilteredMetricDatas(logger, discoveryJob.Namespace, discoveryJob.ExportedTagsOnMetrics, page, discoveryJob.DimensionNameRequirements, metric, assoc) + data := getFilteredMetricDatas(logger, discoveryJob, page, metric, assoc, accountID, region) mux.Lock() getMetricDatas = append(getMetricDatas, data...) @@ -175,16 +178,16 @@ func (ns nopAssociator) AssociateMetricToResource(_ *model.Metric) (*model.Tagge func getFilteredMetricDatas( logger *slog.Logger, - namespace string, - tagsOnMetrics []string, + discoveryJob model.DiscoveryJob, metricsList []*model.Metric, - dimensionNameList []string, m *model.MetricConfig, assoc resourceAssociator, + accountID, + region string, ) []*model.CloudwatchData { getMetricsData := make([]*model.CloudwatchData, 0, len(metricsList)) for _, cwMetric := range metricsList { - if len(dimensionNameList) > 0 && !metricDimensionsMatchNames(cwMetric, dimensionNameList) { + if len(discoveryJob.DimensionNameRequirements) > 0 && !metricDimensionsMatchNames(cwMetric, discoveryJob.DimensionNameRequirements) { continue } @@ -201,18 +204,24 @@ func getFilteredMetricDatas( resource := matchedResource if resource == nil { + arn := "global" + if discoveryJob.ArnFallback != nil { + if fallback, ok := discoveryJob.ArnFallback(region, accountID, cwMetric.Dimensions); ok { + arn = fallback + } + } resource = &model.TaggedResource{ - ARN: "global", - Namespace: namespace, + ARN: arn, + Namespace: discoveryJob.Namespace, } } - metricTags := resource.MetricTags(tagsOnMetrics) + metricTags := resource.MetricTags(discoveryJob.ExportedTagsOnMetrics) for _, stat := range m.Statistics { getMetricsData = append(getMetricsData, &model.CloudwatchData{ MetricName: m.Name, ResourceName: resource.ARN, - Namespace: namespace, + Namespace: discoveryJob.Namespace, Dimensions: cwMetric.Dimensions, GetMetricDataProcessingParams: &model.GetMetricDataProcessingParams{ Period: m.Period, diff --git a/pkg/job/discovery_test.go b/pkg/job/discovery_test.go index 8e6a1069f..9421fe036 100644 --- a/pkg/job/discovery_test.go +++ b/pkg/job/discovery_test.go @@ -483,7 +483,12 @@ func Test_getFilteredMetricDatas(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assoc := maxdimassociator.NewAssociator(promslog.NewNopLogger(), tt.args.dimensionRegexps, tt.args.resources) - metricDatas := getFilteredMetricDatas(promslog.NewNopLogger(), tt.args.namespace, tt.args.tagsOnMetrics, tt.args.metricsList, tt.args.dimensionNameRequirements, tt.args.m, assoc) + discoveryJob := model.DiscoveryJob{ + Namespace: tt.args.namespace, + ExportedTagsOnMetrics: tt.args.tagsOnMetrics, + DimensionNameRequirements: tt.args.dimensionNameRequirements, + } + metricDatas := getFilteredMetricDatas(promslog.NewNopLogger(), discoveryJob, tt.args.metricsList, tt.args.m, assoc, tt.args.accountID, tt.args.region) if len(metricDatas) != len(tt.wantGetMetricsData) { t.Errorf("len(getFilteredMetricDatas()) = %v, want %v", len(metricDatas), len(tt.wantGetMetricsData)) } diff --git a/pkg/job/scrape.go b/pkg/job/scrape.go index a975af4cd..cf9e315d9 100644 --- a/pkg/job/scrape.go +++ b/pkg/job/scrape.go @@ -86,6 +86,7 @@ func ScrapeAwsData( ctx, jobLogger, discoveryJob, + accountID, region, factory.GetTaggingClient(region, role, taggingAPIConcurrency), cloudwatchClient, diff --git a/pkg/model/model.go b/pkg/model/model.go index fa3e7389b..8a7edd7ce 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -30,6 +30,8 @@ type JobsConfig struct { CustomNamespaceJobs []CustomNamespaceJob } +type ArnFallbackFunc func(region, accountID string, dimensions []Dimension) (string, bool) + type DiscoveryJob struct { Regions []string Namespace string @@ -46,6 +48,7 @@ type DiscoveryJob struct { // EnhancedMetrics holds configuration for enhanced metrics in discovery jobs. It contains a configuration for the non-CloudWatch metrics to collect. EnhancedMetrics []*EnhancedMetricConfig + ArnFallback ArnFallbackFunc } func (d *DiscoveryJob) HasEnhancedMetrics() bool { From cd7a08e6977f6f84f8ac1afab89188972a22b426 Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Fri, 28 Aug 2026 10:15:37 +0200 Subject: [PATCH 7/7] feat: Use ARN fallback is available. Signed-off-by: Karsten Jeschkies --- pkg/config/scrapeconf.go | 5 ++++- pkg/job/discovery.go | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/config/scrapeconf.go b/pkg/config/scrapeconf.go index 3724f5d35..95bf0ed65 100644 --- a/pkg/config/scrapeconf.go +++ b/pkg/config/scrapeconf.go @@ -69,6 +69,7 @@ type Job struct { RecentlyActiveOnly bool `yaml:"recentlyActiveOnly"` IncludeContextOnInfoMetrics bool `yaml:"includeContextOnInfoMetrics"` EnhancedMetrics []*EnhancedMetric `yaml:"enhancedMetrics"` + EnableArnFallback bool `yaml:"enabled_arn_fallback"` JobLevelMetricFields `yaml:",inline"` } @@ -466,7 +467,9 @@ func (c *ScrapeConf) toModelConfig() model.JobsConfig { job.IncludeContextOnInfoMetrics = discoveryJob.IncludeContextOnInfoMetrics job.DimensionsRegexps = svc.ToModelDimensionsRegexp() job.EnhancedMetrics = svc.toModelEnhancedMetricsConfig(discoveryJob.EnhancedMetrics) - job.ArnFallback = svc.toArnFallback() + if discoveryJob.EnableArnFallback { + job.ArnFallback = svc.toArnFallback() + } job.ExportedTagsOnMetrics = []string{} if len(c.Discovery.ExportedTagsOnMetrics) > 0 { diff --git a/pkg/job/discovery.go b/pkg/job/discovery.go index 2b16ba063..9ac74f269 100644 --- a/pkg/job/discovery.go +++ b/pkg/job/discovery.go @@ -79,6 +79,7 @@ func runDiscoveryJob( svc := config.SupportedServices.GetService(job.Namespace) metricData := getMetricDataForQueries(ctx, logger, job, svc, clientCloudwatch, resources, accountID, region) + // Attach actual metrics to metric data. if len(metricData) > 0 && svc != nil { metricData, err = gmdProcessor.Run(ctx, svc.Namespace, metricData) if err != nil {