From 5550b3689b833dbf4a9a9e64abaa0b5e9f04b790 Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Fri, 29 May 2026 13:39:36 +0200 Subject: [PATCH] discovery/aws: keep AWS SDK clients out of interfaces to shrink the binary The AWS service-discovery code boxed each concrete SDK client (*ec2.Client, *rds.Client, *lightsail.Client, *elasticache.Client, *ecs.Client and *kafka.Client) into an interface, either directly or as a field of a struct that is itself boxed into the Discoverer interface. Once reflection is reachable in the program -- it always is, via the YAML/config machinery -- the Go linker conservatively retains every exported method of any concrete type reachable through an interface, including a type held as a field of an interface-boxed struct. Each SDK client exposes the service's full API (e.g. *ec2.Client has ~470 operation methods), so all of their operation serializers and the corresponding types (de)serializer graphs were kept, even though discovery only calls a handful of operations. EC2 alone accounted for ~21 MB. Wrap each client in a small adapter that captures only the operations discovery uses as method-value closures. The concrete client then lives only inside closure contexts, which reflection cannot traverse, so dead-code elimination can drop the unused operations. This reduces the binary sizes substantially: prometheus 228.6 MB -> 162.6 MB (-66 MB, -29%) promtool 205.1 MB -> 139.0 MB (-66 MB, -32%) There is no functional or API change; the mocking interfaces used by the tests are unchanged. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- discovery/aws/ec2.go | 45 ++++++++++++++++++++++++-- discovery/aws/ecs.go | 62 +++++++++++++++++++++++++++++++++--- discovery/aws/elasticache.go | 35 ++++++++++++++++++-- discovery/aws/lightsail.go | 25 ++++++++++++--- discovery/aws/msk.go | 34 ++++++++++++++++++-- discovery/aws/rds.go | 28 ++++++++++++++-- 6 files changed, 213 insertions(+), 16 deletions(-) diff --git a/discovery/aws/ec2.go b/discovery/aws/ec2.go index 364ec4f102..2bc7c29187 100644 --- a/discovery/aws/ec2.go +++ b/discovery/aws/ec2.go @@ -150,6 +150,47 @@ type ec2Client interface { DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) } +// ec2ClientAdapter holds the EC2 API calls that AWS discovery actually uses as +// method-value closures over the concrete *ec2.Client. +// +// It exists purely to keep the binary small. The Go linker, once reflection +// (reflect.Value.Method/Call plus struct-field traversal, both reachable via +// the YAML/config machinery) is live, conservatively retains every exported +// method of any concrete type that is reachable through an interface — and a +// type stored as a field of an interface-boxed struct counts. *ec2.Client has +// ~470 operation methods; retaining all of them pulls in ~1,500 serializers and +// roughly 21 MB. By capturing only the needed methods as func values, the +// concrete *ec2.Client is hidden inside closure contexts (which reflection +// cannot traverse) and never appears as a field of a boxed type, so dead-code +// elimination drops the unused operations. +type ec2ClientAdapter struct { + describeAvailabilityZones func(ctx context.Context, params *ec2.DescribeAvailabilityZonesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeAvailabilityZonesOutput, error) + describeInstances func(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) + describeNetworkInterfaces func(ctx context.Context, params *ec2.DescribeNetworkInterfacesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) +} + +// newEC2ClientAdapter wraps a concrete *ec2.Client, capturing only the API +// calls AWS discovery needs. See the ec2ClientAdapter doc comment for why. +func newEC2ClientAdapter(c *ec2.Client) ec2ClientAdapter { + return ec2ClientAdapter{ + describeAvailabilityZones: c.DescribeAvailabilityZones, + describeInstances: c.DescribeInstances, + describeNetworkInterfaces: c.DescribeNetworkInterfaces, + } +} + +func (a ec2ClientAdapter) DescribeAvailabilityZones(ctx context.Context, params *ec2.DescribeAvailabilityZonesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeAvailabilityZonesOutput, error) { + return a.describeAvailabilityZones(ctx, params, optFns...) +} + +func (a ec2ClientAdapter) DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { + return a.describeInstances(ctx, params, optFns...) +} + +func (a ec2ClientAdapter) DescribeNetworkInterfaces(ctx context.Context, params *ec2.DescribeNetworkInterfacesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) { + return a.describeNetworkInterfaces(ctx, params, optFns...) +} + // EC2Discovery periodically performs EC2-SD requests. It implements // the Discoverer interface. type EC2Discovery struct { @@ -235,12 +276,12 @@ func (d *EC2Discovery) ec2Client(ctx context.Context) (ec2Client, error) { cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } - d.ec2 = ec2.NewFromConfig(cfg, func(options *ec2.Options) { + d.ec2 = newEC2ClientAdapter(ec2.NewFromConfig(cfg, func(options *ec2.Options) { if d.cfg.Endpoint != "" { options.BaseEndpoint = &d.cfg.Endpoint } options.HTTPClient = httpClient - }) + })) return d.ec2, nil } diff --git a/discovery/aws/ecs.go b/discovery/aws/ecs.go index ff751ff414..def9c88e6e 100644 --- a/discovery/aws/ecs.go +++ b/discovery/aws/ecs.go @@ -162,6 +162,60 @@ type ecsClient interface { DescribeContainerInstances(context.Context, *ecs.DescribeContainerInstancesInput, ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) } +// ecsClientAdapter captures only the ECS API calls AWS discovery uses as +// method-value closures, keeping the concrete *ecs.Client out of any +// interface-boxed struct field. See ec2ClientAdapter for the full rationale: +// this stops the linker from retaining the entire ECS API surface (~2 MB). +type ecsClientAdapter struct { + listClusters func(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) + describeClusters func(context.Context, *ecs.DescribeClustersInput, ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) + listServices func(context.Context, *ecs.ListServicesInput, ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) + describeServices func(context.Context, *ecs.DescribeServicesInput, ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) + listTasks func(context.Context, *ecs.ListTasksInput, ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) + describeTasks func(context.Context, *ecs.DescribeTasksInput, ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) + describeContainerInstances func(context.Context, *ecs.DescribeContainerInstancesInput, ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) +} + +func newECSClientAdapter(c *ecs.Client) ecsClientAdapter { + return ecsClientAdapter{ + listClusters: c.ListClusters, + describeClusters: c.DescribeClusters, + listServices: c.ListServices, + describeServices: c.DescribeServices, + listTasks: c.ListTasks, + describeTasks: c.DescribeTasks, + describeContainerInstances: c.DescribeContainerInstances, + } +} + +func (a ecsClientAdapter) ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) { + return a.listClusters(ctx, params, optFns...) +} + +func (a ecsClientAdapter) DescribeClusters(ctx context.Context, params *ecs.DescribeClustersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) { + return a.describeClusters(ctx, params, optFns...) +} + +func (a ecsClientAdapter) ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) { + return a.listServices(ctx, params, optFns...) +} + +func (a ecsClientAdapter) DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) { + return a.describeServices(ctx, params, optFns...) +} + +func (a ecsClientAdapter) ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) { + return a.listTasks(ctx, params, optFns...) +} + +func (a ecsClientAdapter) DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + return a.describeTasks(ctx, params, optFns...) +} + +func (a ecsClientAdapter) DescribeContainerInstances(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) { + return a.describeContainerInstances(ctx, params, optFns...) +} + type ecsEC2Client interface { DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) DescribeNetworkInterfaces(context.Context, *ec2.DescribeNetworkInterfacesInput, ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) @@ -250,16 +304,16 @@ func (d *ECSDiscovery) initEcsClient(ctx context.Context) error { cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } - d.ecs = ecs.NewFromConfig(cfg, func(options *ecs.Options) { + d.ecs = newECSClientAdapter(ecs.NewFromConfig(cfg, func(options *ecs.Options) { if d.cfg.Endpoint != "" { options.BaseEndpoint = &d.cfg.Endpoint } options.HTTPClient = client - }) + })) - d.ec2 = ec2.NewFromConfig(cfg, func(options *ec2.Options) { + d.ec2 = newEC2ClientAdapter(ec2.NewFromConfig(cfg, func(options *ec2.Options) { options.HTTPClient = client - }) + })) // Test credentials by making a simple API call testCtx, cancel := context.WithTimeout(ctx, 10*time.Second) diff --git a/discovery/aws/elasticache.go b/discovery/aws/elasticache.go index f65834a079..722a46085a 100644 --- a/discovery/aws/elasticache.go +++ b/discovery/aws/elasticache.go @@ -246,6 +246,37 @@ type elasticacheClient interface { ListTagsForResource(ctx context.Context, params *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) } +// elasticacheClientAdapter captures only the ElastiCache API calls AWS +// discovery uses as method-value closures, keeping the concrete +// *elasticache.Client out of any interface-boxed struct field. See +// ec2ClientAdapter for the full rationale: this stops the linker from retaining +// the entire ElastiCache API surface (~2.5 MB). +type elasticacheClientAdapter struct { + describeServerlessCaches func(ctx context.Context, params *elasticache.DescribeServerlessCachesInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error) + describeCacheClusters func(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) + listTagsForResource func(ctx context.Context, params *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) +} + +func newElastiCacheClientAdapter(c *elasticache.Client) elasticacheClientAdapter { + return elasticacheClientAdapter{ + describeServerlessCaches: c.DescribeServerlessCaches, + describeCacheClusters: c.DescribeCacheClusters, + listTagsForResource: c.ListTagsForResource, + } +} + +func (a elasticacheClientAdapter) DescribeServerlessCaches(ctx context.Context, params *elasticache.DescribeServerlessCachesInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error) { + return a.describeServerlessCaches(ctx, params, optFns...) +} + +func (a elasticacheClientAdapter) DescribeCacheClusters(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) { + return a.describeCacheClusters(ctx, params, optFns...) +} + +func (a elasticacheClientAdapter) ListTagsForResource(ctx context.Context, params *elasticache.ListTagsForResourceInput, optFns ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) { + return a.listTagsForResource(ctx, params, optFns...) +} + // ElasticacheDiscovery periodically performs Elasticache-SD requests. // It implements the Discoverer interface. type ElasticacheDiscovery struct { @@ -328,12 +359,12 @@ func (d *ElasticacheDiscovery) initElasticacheClient(ctx context.Context) error cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } - d.elasticacheClient = elasticache.NewFromConfig(cfg, func(options *elasticache.Options) { + d.elasticacheClient = newElastiCacheClientAdapter(elasticache.NewFromConfig(cfg, func(options *elasticache.Options) { if d.cfg.Endpoint != "" { options.BaseEndpoint = &d.cfg.Endpoint } options.HTTPClient = client - }) + })) // Test credentials by making a simple API call testCtx, cancel := context.WithTimeout(ctx, 10*time.Second) diff --git a/discovery/aws/lightsail.go b/discovery/aws/lightsail.go index 77087b0821..c56fe75948 100644 --- a/discovery/aws/lightsail.go +++ b/discovery/aws/lightsail.go @@ -119,12 +119,29 @@ func (c *LightsailSDConfig) UnmarshalYAML(unmarshal func(any) error) error { return c.HTTPClientConfig.Validate() } +// lightsailClientAdapter captures only the Lightsail API calls AWS discovery +// uses as method-value closures, keeping the concrete *lightsail.Client out of +// any interface-boxed struct field. See ec2ClientAdapter for the full +// rationale: this stops the linker from retaining the entire Lightsail API +// surface (~3.4 MB). +type lightsailClientAdapter struct { + getInstances func(ctx context.Context, params *lightsail.GetInstancesInput, optFns ...func(*lightsail.Options)) (*lightsail.GetInstancesOutput, error) +} + +func newLightsailClientAdapter(c *lightsail.Client) *lightsailClientAdapter { + return &lightsailClientAdapter{getInstances: c.GetInstances} +} + +func (a *lightsailClientAdapter) GetInstances(ctx context.Context, params *lightsail.GetInstancesInput, optFns ...func(*lightsail.Options)) (*lightsail.GetInstancesOutput, error) { + return a.getInstances(ctx, params, optFns...) +} + // LightsailDiscovery periodically performs Lightsail-SD requests. It implements // the Discoverer interface. type LightsailDiscovery struct { *refresh.Discovery cfg *LightsailSDConfig - lightsail *lightsail.Client + lightsail *lightsailClientAdapter } // NewLightsailDiscovery returns a new LightsailDiscovery which periodically refreshes its targets. @@ -154,7 +171,7 @@ func NewLightsailDiscovery(conf *LightsailSDConfig, opts discovery.DiscovererOpt return d, nil } -func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsail.Client, error) { +func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsailClientAdapter, error) { if d.lightsail != nil { return d.lightsail, nil } @@ -198,12 +215,12 @@ func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsail.Cl cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } - d.lightsail = lightsail.NewFromConfig(cfg, func(options *lightsail.Options) { + d.lightsail = newLightsailClientAdapter(lightsail.NewFromConfig(cfg, func(options *lightsail.Options) { if d.cfg.Endpoint != "" { options.BaseEndpoint = &d.cfg.Endpoint } options.HTTPClient = httpClient - }) + })) return d.lightsail, nil } diff --git a/discovery/aws/msk.go b/discovery/aws/msk.go index bff8337576..475d2db145 100644 --- a/discovery/aws/msk.go +++ b/discovery/aws/msk.go @@ -158,6 +158,36 @@ type mskClient interface { ListNodes(context.Context, *kafka.ListNodesInput, ...func(*kafka.Options)) (*kafka.ListNodesOutput, error) } +// mskClientAdapter captures only the MSK (Kafka) API calls AWS discovery uses +// as method-value closures, keeping the concrete *kafka.Client out of any +// interface-boxed struct field. See ec2ClientAdapter for the full rationale: +// this stops the linker from retaining the entire MSK API surface (~1.4 MB). +type mskClientAdapter struct { + describeClusterV2 func(context.Context, *kafka.DescribeClusterV2Input, ...func(*kafka.Options)) (*kafka.DescribeClusterV2Output, error) + listClustersV2 func(context.Context, *kafka.ListClustersV2Input, ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error) + listNodes func(context.Context, *kafka.ListNodesInput, ...func(*kafka.Options)) (*kafka.ListNodesOutput, error) +} + +func newMSKClientAdapter(c *kafka.Client) mskClientAdapter { + return mskClientAdapter{ + describeClusterV2: c.DescribeClusterV2, + listClustersV2: c.ListClustersV2, + listNodes: c.ListNodes, + } +} + +func (a mskClientAdapter) DescribeClusterV2(ctx context.Context, params *kafka.DescribeClusterV2Input, optFns ...func(*kafka.Options)) (*kafka.DescribeClusterV2Output, error) { + return a.describeClusterV2(ctx, params, optFns...) +} + +func (a mskClientAdapter) ListClustersV2(ctx context.Context, params *kafka.ListClustersV2Input, optFns ...func(*kafka.Options)) (*kafka.ListClustersV2Output, error) { + return a.listClustersV2(ctx, params, optFns...) +} + +func (a mskClientAdapter) ListNodes(ctx context.Context, params *kafka.ListNodesInput, optFns ...func(*kafka.Options)) (*kafka.ListNodesOutput, error) { + return a.listNodes(ctx, params, optFns...) +} + // MSKDiscovery periodically performs MSK-SD requests. It implements // the Discoverer interface. type MSKDiscovery struct { @@ -240,12 +270,12 @@ func (d *MSKDiscovery) initMskClient(ctx context.Context) error { cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } - d.msk = kafka.NewFromConfig(cfg, func(options *kafka.Options) { + d.msk = newMSKClientAdapter(kafka.NewFromConfig(cfg, func(options *kafka.Options) { if d.cfg.Endpoint != "" { options.BaseEndpoint = &d.cfg.Endpoint } options.HTTPClient = client - }) + })) // Test credentials by making a simple API call testCtx, cancel := context.WithTimeout(ctx, 10*time.Second) diff --git a/discovery/aws/rds.go b/discovery/aws/rds.go index 438338b060..b2d5bae2d9 100644 --- a/discovery/aws/rds.go +++ b/discovery/aws/rds.go @@ -276,6 +276,30 @@ type rdsClient interface { DescribeDBInstances(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) } +// rdsClientAdapter captures only the RDS API calls AWS discovery uses as +// method-value closures, keeping the concrete *rds.Client out of any +// interface-boxed struct field. See ec2ClientAdapter for the full rationale: +// this stops the linker from retaining the entire RDS API surface (~5 MB). +type rdsClientAdapter struct { + describeDBClusters func(context.Context, *rds.DescribeDBClustersInput, ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error) + describeDBInstances func(context.Context, *rds.DescribeDBInstancesInput, ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) +} + +func newRDSClientAdapter(c *rds.Client) rdsClientAdapter { + return rdsClientAdapter{ + describeDBClusters: c.DescribeDBClusters, + describeDBInstances: c.DescribeDBInstances, + } +} + +func (a rdsClientAdapter) DescribeDBClusters(ctx context.Context, params *rds.DescribeDBClustersInput, optFns ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error) { + return a.describeDBClusters(ctx, params, optFns...) +} + +func (a rdsClientAdapter) DescribeDBInstances(ctx context.Context, params *rds.DescribeDBInstancesInput, optFns ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) { + return a.describeDBInstances(ctx, params, optFns...) +} + // RDSDiscovery periodically performs RDS-SD requests. It implements // the Discoverer interface. type RDSDiscovery struct { @@ -358,12 +382,12 @@ func (d *RDSDiscovery) initRdsClient(ctx context.Context) error { cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } - d.rds = rds.NewFromConfig(cfg, func(options *rds.Options) { + d.rds = newRDSClientAdapter(rds.NewFromConfig(cfg, func(options *rds.Options) { if d.cfg.Endpoint != "" { options.BaseEndpoint = &d.cfg.Endpoint } options.HTTPClient = client - }) + })) // Test credentials by making a simple API call testCtx, cancel := context.WithTimeout(ctx, 10*time.Second)