| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "log" |
| 7 | "strings" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/aws/aws-sdk-go-v2/aws" |
| 11 | "github.com/aws/aws-sdk-go-v2/service/cloudwatch" |
| 12 | "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/rootaws" |
| 15 | ) |
| 16 | |
| 17 | // The AWS adapter of the eligible-capacity metric (region HA spec 2 §4, |
| 18 | // §25): CloudWatch, Carlos/Region EligibleBoxes, keyed by the placement |
| 19 | // code, in each region's own CloudWatch, where the region-host module's |
| 20 | // alarm reads it. Armed by CARLOS_REGION_METRICS=cloudwatch on the |
| 21 | // console. The platform box's role needs cloudwatch:PutMetricData on the |
| 22 | // Carlos/Region namespace: plan 2c grants it. |
| 23 | |
| 24 | const ( |
| 25 | capacityNamespace = "Carlos/Region" |
| 26 | capacityMetric = "EligibleBoxes" |
| 27 | ) |
| 28 | |
| 29 | // capacityData is the exact PutMetricData input for one region's count. |
| 30 | func capacityData(c regionCapacity, now time.Time) *cloudwatch.PutMetricDataInput { |
| 31 | return &cloudwatch.PutMetricDataInput{Namespace: aws.String(capacityNamespace), MetricData: []types.MetricDatum{{ |
| 32 | MetricName: aws.String(capacityMetric), |
| 33 | Dimensions: []types.Dimension{{Name: aws.String("Region"), Value: aws.String(c.Placement)}}, |
| 34 | Timestamp: aws.Time(now), |
| 35 | Unit: types.StandardUnitCount, |
| 36 | Value: aws.Float64(float64(c.Eligible)), |
| 37 | }}} |
| 38 | } |
| 39 | |
| 40 | // cloudWatchCapacity publishes each count to its region's CloudWatch. |
| 41 | type cloudWatchCapacity struct { |
| 42 | put func(ctx context.Context, awsRegion string, in *cloudwatch.PutMetricDataInput) error |
| 43 | now func() time.Time |
| 44 | } |
| 45 | |
| 46 | func (c cloudWatchCapacity) PublishCapacity(ctx context.Context, counts []regionCapacity) error { |
| 47 | var errs []error |
| 48 | for _, n := range counts { |
| 49 | errs = append(errs, c.put(ctx, n.AWSRegion, capacityData(n, c.now()))) |
| 50 | } |
| 51 | return errors.Join(errs...) |
| 52 | } |
| 53 | |
| 54 | // capacityPublisherFromEnv is the console's publisher: CloudWatch when |
| 55 | // CARLOS_REGION_METRICS is "cloudwatch", and nil otherwise, or when the |
| 56 | // AWS configuration cannot be loaded (logged). |
| 57 | func capacityPublisherFromEnv(ctx context.Context, getenv func(string) string) capacityPublisher { |
| 58 | if strings.TrimSpace(getenv("CARLOS_REGION_METRICS")) != "cloudwatch" { |
| 59 | return nil |
| 60 | } |
| 61 | cfg, err := rootaws.LoadDefaultConfig(ctx) |
| 62 | if err != nil { |
| 63 | log.Printf("console: eligible-capacity metric disabled: load aws config: %v", err) |
| 64 | return nil |
| 65 | } |
| 66 | return cloudWatchCapacity{now: time.Now, put: func(ctx context.Context, awsRegion string, in *cloudwatch.PutMetricDataInput) error { |
| 67 | _, err := cloudwatch.NewFromConfig(cfg, func(o *cloudwatch.Options) { o.Region = awsRegion }).PutMetricData(ctx, in) |
| 68 | return err |
| 69 | }} |
| 70 | } |
| 71 | |