diff --git a/pkg/config/scrapeconf.go b/pkg/config/scrapeconf.go index db4c155bd..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,6 +467,9 @@ func (c *ScrapeConf) toModelConfig() model.JobsConfig { job.IncludeContextOnInfoMetrics = discoveryJob.IncludeContextOnInfoMetrics job.DimensionsRegexps = svc.ToModelDimensionsRegexp() job.EnhancedMetrics = svc.toModelEnhancedMetricsConfig(discoveryJob.EnhancedMetrics) + if discoveryJob.EnableArnFallback { + 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 aa35af18d..43708f6db 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,105 @@ 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(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(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", 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). 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(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", 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(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", partitionForRegion(region), 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(region, accountID string, dimensions map[string]string) (string, bool) { + for _, fn := range fns { + if arn, ok := fn(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 +138,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 { @@ -74,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 { @@ -96,10 +216,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", }, @@ -112,6 +235,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: identityArn("CertificateArn"), }, { Namespace: "AWS/ACMPrivateCA", @@ -122,6 +246,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: identityArn("PrivateCAArn"), }, { Namespace: "AmazonMWAA", @@ -145,6 +270,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 +284,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":fleet/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("appstream", "fleet/%s", "FleetName"), }, { Namespace: "AWS/Backup", @@ -165,6 +295,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":backup-vault:(?P[^:]+)"), }, + ArnFromDimensions: arnFromDimensions("backup", "backup-vault:%s", "BackupVaultName"), }, { Namespace: "AWS/ApiGateway", @@ -181,6 +312,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,10 +325,19 @@ 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", 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", @@ -205,6 +348,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("apis/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("appsync", "apis/%s", "GraphQLAPIId"), }, { Namespace: "AWS/Athena", @@ -215,6 +359,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("workgroup/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("athena", "workgroup/%s", "WorkGroup"), }, { Namespace: "AWS/AutoScaling", @@ -222,6 +367,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,8 +381,11 @@ 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. }, { + // Account-level billing/cost metrics, not tied to a discrete resource. Namespace: "AWS/Billing", Alias: "billing", }, @@ -248,6 +399,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 +413,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 +425,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("userpool/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("cognito-idp", "userpool/%s", "UserPool"), }, { Namespace: "AWS/DataSync", @@ -280,6 +438,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 +452,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":directory/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ds", "directory/%s", "Directory_ID"), }, { Namespace: "AWS/DMS", @@ -301,6 +464,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 +476,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.+)"), }, + ArnFromDimensions: identityArn("ResourceArn"), }, { Namespace: "AWS/DocDB", @@ -323,6 +489,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 +505,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 +520,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":table/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("dynamodb", "table/%s", "TableName"), }, { Namespace: "AWS/EBS", @@ -355,6 +531,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("volume/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "volume/%s", "VolumeId"), }, { Namespace: "AWS/ElastiCache", @@ -367,6 +544,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 +558,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("cluster/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("memorydb", "cluster/%s", "ClusterName"), }, { Namespace: "AWS/EC2", @@ -387,6 +569,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("instance/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "instance/%s", "InstanceId"), }, { Namespace: "AWS/EC2Spot", @@ -394,6 +577,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "spot-fleet-request/%s", "FleetRequestId"), }, { Namespace: "AWS/EC2CapacityReservations", @@ -401,6 +585,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":capacity-reservation/(?P)$"), }, + ArnFromDimensions: arnFromDimensions("ec2", "capacity-reservation/%s", "CapacityReservationId"), }, { Namespace: "AWS/ECS", @@ -413,6 +598,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 +616,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 +630,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":cluster/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("eks", "cluster/%s", "ClusterName"), }, { Namespace: "AWS/EFS", @@ -447,6 +641,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("file-system/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("elasticfilesystem", "file-system/%s", "FileSystemId"), }, { Namespace: "AWS/EKS", @@ -457,6 +652,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":cluster/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("eks", "cluster/%s", "ClusterName"), }, { Namespace: "AWS/ELB", @@ -467,6 +663,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":loadbalancer/(?P.+)$"), }, + ArnFromDimensions: arnFromDimensions("elasticloadbalancing", "loadbalancer/%s", "LoadBalancerName"), }, { Namespace: "AWS/ElasticMapReduce", @@ -477,6 +674,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("cluster/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("elasticmapreduce", "cluster/%s", "JobFlowId"), }, { Namespace: "AWS/EMRServerless", @@ -487,6 +685,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 +698,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":domain/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("es", "domain/%s", "DomainName"), }, { Namespace: "AWS/Firehose", @@ -507,6 +709,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":deliverystream/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("firehose", "deliverystream/%s", "DeliveryStreamName"), }, { Namespace: "AWS/FSx", @@ -517,6 +720,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("file-system/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("fsx", "file-system/%s", "FileSystemId"), }, { Namespace: "AWS/GameLift", @@ -527,6 +731,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":fleet/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("gamelift", "fleet/%s", "FleetId"), }, { Namespace: "AWS/GatewayELB", @@ -538,6 +743,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 +759,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 +772,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":job/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("glue", "job/%s", "JobName"), }, { Namespace: "AWS/IoT", @@ -572,6 +785,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 +799,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 +811,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 +822,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":stream/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("kinesis", "stream/%s", "StreamName"), }, { Namespace: "AWS/KinesisAnalytics", @@ -612,6 +833,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":application/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("kinesisanalytics", "application/%s", "Application"), }, { Namespace: "AWS/KMS", @@ -622,6 +844,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":key/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("kms", "key/%s", "KeyId"), }, { Namespace: "AWS/Lambda", @@ -632,6 +855,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":function:(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("lambda", "function:%s", "FunctionName"), }, { Namespace: "AWS/Logs", @@ -642,6 +866,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":log-group:(?P.+)"), }, + ArnFromDimensions: arnFromDimensions("logs", "log-group:%s", "LogGroupName"), }, { Namespace: "AWS/MediaConnect", @@ -656,6 +881,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 +896,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*:.*:mediaconvert:.*:queues/.*)$"), }, + ArnFromDimensions: identityArn("Queue"), }, { Namespace: "AWS/MediaPackage", @@ -679,6 +910,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 +922,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":channel:(?P.+)$"), }, + ArnFromDimensions: arnFromDimensions("medialive", "channel:%s", "ChannelId"), }, { Namespace: "AWS/MediaTailor", @@ -699,6 +933,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("playbackConfiguration/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("mediatailor", "playbackConfiguration/%s", "ConfigurationName"), }, { Namespace: "AWS/Neptune", @@ -711,6 +946,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 +960,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("firewall/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("network-firewall", "firewall/%s", "FirewallName"), }, { Namespace: "AWS/NATGateway", @@ -731,6 +971,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("natgateway/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "natgateway/%s", "NatGatewayId"), }, { Namespace: "AWS/NetworkELB", @@ -743,6 +984,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 +998,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,10 +1009,20 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":vpc-endpoint-service/(?P.+)"), }, + 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", @@ -777,6 +1033,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":ledger/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("qldb", "ledger/%s", "LedgerName"), }, { Namespace: "AWS/QuickSight", @@ -795,6 +1052,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,8 +1067,14 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":cluster:(?P[^/]+)"), }, + 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{ @@ -823,6 +1091,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":resolver-endpoint/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("route53resolver", "resolver-endpoint/%s", "EndpointId"), }, { Namespace: "AWS/Route53", @@ -833,10 +1102,19 @@ 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", 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", @@ -847,18 +1125,49 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P[^:]+)$"), }, + // S3 bucket ARNs omit both the region and account segments. + 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", 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", @@ -877,6 +1186,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.*)"), }, + ArnFromDimensions: identityArn("StateMachineArn"), }, { Namespace: "AWS/SNS", @@ -887,6 +1197,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P[^:]+)$"), }, + ArnFromDimensions: arnFromDimensions("sns", "%s", "TopicName"), }, { Namespace: "AWS/SQS", @@ -897,6 +1208,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P[^:]+)$"), }, + ArnFromDimensions: arnFromDimensions("sqs", "%s", "QueueName"), }, { Namespace: "AWS/StorageGateway", @@ -909,6 +1221,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,8 +1239,10 @@ var SupportedServices = serviceConfigs{ regexp.MustCompile(":transit-gateway/(?P[^/]+)"), regexp.MustCompile("(?P[^/]+)/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "transit-gateway/%s", "TransitGateway"), }, { + // Trusted Advisor check-result metrics, not resource metrics -- no ARN to reconstruct. Namespace: "AWS/TrustedAdvisor", Alias: "trustedadvisor", }, @@ -938,6 +1255,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":vpn-connection/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("ec2", "vpn-connection/%s", "VpnId"), }, { Namespace: "AWS/ClientVPN", @@ -948,6 +1266,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 +1277,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 +1291,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 +1305,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":collection/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("aoss", "collection/%s", "CollectionId"), }, { Namespace: "AWS/SageMaker", @@ -992,6 +1318,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 +1332,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":endpoint/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("sagemaker", "endpoint/%s", "EndpointName"), }, { Namespace: "/aws/sagemaker/InferenceComponents", @@ -1012,6 +1343,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 +1375,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 +1386,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":pipeline/(?P[^/]+)"), }, + ArnFromDimensions: arnFromDimensions("sagemaker", "pipeline/%s", "PipelineName"), }, { Namespace: "AWS/IPAM", @@ -1063,8 +1397,12 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":ipam-pool/(?P[^/]+)$"), }, + 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", }, @@ -1077,6 +1415,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.+)"), }, + ArnFromDimensions: identityArn("AgentAliasArn"), }, { Namespace: "AWS/Bedrock/Guardrails", @@ -1087,6 +1426,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile("(?P.+)"), }, + ArnFromDimensions: identityArn("GuardrailArn"), }, { Namespace: "AWS/Events", @@ -1098,6 +1438,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 +1451,7 @@ var SupportedServices = serviceConfigs{ DimensionRegexps: []*regexp.Regexp{ regexp.MustCompile(":service/(?P[^/]+)$"), }, + ArnFromDimensions: arnFromDimensions("vpc-lattice", "service/%s", "Service"), }, { Namespace: "AWS/Network Manager", @@ -1118,5 +1462,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"), }, } diff --git a/pkg/job/discovery.go b/pkg/job/discovery.go index 09245fb67..9ac74f269 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,8 +77,9 @@ 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) + // Attach actual metrics to metric data. if len(metricData) > 0 && svc != nil { metricData, err = gmdProcessor.Run(ctx, svc.Namespace, metricData) if err != nil { @@ -127,6 +129,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 +154,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 +179,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 +205,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 {