@rawdash/connector-aws-cost 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # @rawdash/connector-aws-cost
2
+
3
+ Rawdash connector for [AWS Cost Explorer](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/Welcome.html) — syncs daily/monthly spend and cost forecasts into the six-shape storage model, optionally broken down by service, linked account, or tag.
4
+
5
+ > **Cost note:** every Cost Explorer query is billed at **$0.01** by AWS. This connector's defaults are deliberately conservative — a single daily sync issues two queries (`GetCostAndUsage` + `GetCostForecast`). Keep the sync interval at a day (the minimum sensible cadence is 1 hour) and prefer `MONTHLY` granularity for long windows.
6
+
7
+ ## Auth setup
8
+
9
+ Cost Explorer must be queried from the **management (payer) account**, or from a member account that has been granted explicit Cost Explorer access. The connector uses the same auth shape as `@rawdash/connector-aws-cloudwatch`: a static access key/secret, optionally combined with a cross-account role to assume.
10
+
11
+ ### Option A — Access key + secret
12
+
13
+ 1. In the AWS console open **IAM → Users** and create (or pick) a programmatic user.
14
+ 2. Attach a policy granting `ce:GetCostAndUsage` and `ce:GetCostForecast` (the managed `AWSBillingReadOnlyAccess` policy is sufficient).
15
+ 3. Create an access key for the user and store the two halves as the secrets `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
16
+
17
+ ### Option B — Cross-account role assumption
18
+
19
+ 1. In the **management account**, create a role (e.g. `rawdash-cost-explorer`) with the Cost Explorer permissions above and a trust policy allowing your base principal to assume it. Require an **external ID** to guard against the confused-deputy problem.
20
+ 2. Provide `roleArn` and the matching `externalId`, plus a base access key/secret that is allowed to call `sts:AssumeRole` on that role.
21
+
22
+ The connector calls `sts:AssumeRole`, then signs Cost Explorer requests with the returned temporary credentials.
23
+
24
+ ## Configuration
25
+
26
+ ```ts
27
+ import { secret } from '@rawdash/core';
28
+
29
+ const awsCost = {
30
+ name: 'aws-cost',
31
+ connectorId: 'aws-cost',
32
+ config: {
33
+ accessKeyId: secret('AWS_ACCESS_KEY_ID'),
34
+ secretAccessKey: secret('AWS_SECRET_ACCESS_KEY'),
35
+ granularity: 'DAILY', // or 'MONTHLY'
36
+ groupBy: ['SERVICE'], // optional — up to two dimensions
37
+ lookbackDays: 90, // optional backfill window, defaults to 90
38
+ },
39
+ };
40
+ ```
41
+
42
+ Cross-account variant:
43
+
44
+ ```ts
45
+ const awsCost = {
46
+ name: 'aws-cost',
47
+ connectorId: 'aws-cost',
48
+ config: {
49
+ accessKeyId: secret('AWS_ACCESS_KEY_ID'),
50
+ secretAccessKey: secret('AWS_SECRET_ACCESS_KEY'),
51
+ roleArn: 'arn:aws:iam::123456789012:role/rawdash-cost-explorer',
52
+ externalId: 'rawdash-cost-explorer',
53
+ groupBy: ['LINKED_ACCOUNT', 'TAG:Environment'],
54
+ },
55
+ };
56
+ ```
57
+
58
+ `groupBy` accepts Cost Explorer dimension keys (`SERVICE`, `LINKED_ACCOUNT`, `REGION`, …), tag keys prefixed with `TAG:` (e.g. `TAG:Environment`), or cost categories prefixed with `COST_CATEGORY:`. Cost Explorer allows at most two group-by keys; extras are ignored.
59
+
60
+ Register the connector class when mounting the engine:
61
+
62
+ ```ts
63
+ import { AwsCostConnector } from '@rawdash/connector-aws-cost';
64
+ import { mountEngine } from '@rawdash/hono';
65
+
66
+ mountEngine(config, {
67
+ connectorRegistry: { 'aws-cost': AwsCostConnector },
68
+ });
69
+ ```
70
+
71
+ Then wire it into `defineConfig`:
72
+
73
+ ```ts
74
+ import { defineConfig, defineDashboard, defineMetric } from '@rawdash/core';
75
+
76
+ export default defineConfig({
77
+ connectors: [awsCost],
78
+ dashboards: {
79
+ spend: defineDashboard({
80
+ widgets: {
81
+ cost_today: {
82
+ kind: 'stat',
83
+ title: 'Spend today',
84
+ metric: defineMetric({
85
+ connector: awsCost,
86
+ shape: 'metric',
87
+ name: 'aws_cost_daily',
88
+ fn: 'sum',
89
+ window: '1d',
90
+ }),
91
+ },
92
+ cost_over_time: {
93
+ kind: 'timeseries',
94
+ title: 'Daily spend',
95
+ window: '30d',
96
+ metric: defineMetric({
97
+ connector: awsCost,
98
+ shape: 'metric',
99
+ name: 'aws_cost_daily',
100
+ fn: 'sum',
101
+ window: '30d',
102
+ groupBy: { field: 'ts', granularity: 'day' },
103
+ }),
104
+ },
105
+ forecast: {
106
+ kind: 'stat',
107
+ title: 'Forecast (this month)',
108
+ metric: defineMetric({
109
+ connector: awsCost,
110
+ shape: 'metric',
111
+ name: 'aws_cost_forecast',
112
+ fn: 'latest',
113
+ }),
114
+ },
115
+ },
116
+ }),
117
+ },
118
+ });
119
+ ```
120
+
121
+ ## Data model
122
+
123
+ All resources are stored as **metric samples** (`shape: 'metric'`). The `ts` field is the start of each cost period in Unix milliseconds and `value` is the unblended cost amount.
124
+
125
+ | Metric name | Source resource | `value` | Attributes |
126
+ | ------------------- | --------------- | -------------------- | -------------------------------------------------------------- |
127
+ | `aws_cost_daily` | `daily_cost` | unblended cost | `granularity`, `unit`, `estimated`, plus one per `groupBy` key |
128
+ | `aws_cost_forecast` | `forecast` | forecasted mean cost | `granularity`, `unit`, `lowerBound`, `upperBound` |
129
+
130
+ When `groupBy` is set, each group becomes its own sample with the dimension value stored under a normalized attribute name (`SERVICE` → `service`, `LINKED_ACCOUNT` → `linked_account`, `TAG:Environment` → `tag_Environment`).
131
+
132
+ ## Schemas
133
+
134
+ `AwsCostConnector.schemas` declares the Zod schema for each `request()` resource. Used by the cloud shape-drift pipeline to populate `connector_baselines`, and by the package's property tests.
135
+
136
+ | Resource | Represents |
137
+ | ------------ | ------------------------------------------------------------------------ |
138
+ | `daily_cost` | `GetCostAndUsage` — per-period unblended cost, optionally grouped |
139
+ | `forecast` | `GetCostForecast` — forecasted unblended cost for the upcoming period(s) |
140
+
141
+ ## Sync behaviour
142
+
143
+ - **Backfill** (`mode: 'full'`): fetches a rolling window (default 90 days, configurable via `lookbackDays`) of `daily_cost`, plus the upcoming forecast.
144
+ - **Incremental** (`mode: 'latest'`): fetches only the trailing 3 days, since Cost Explorer data can be revised for a couple of days after the fact.
145
+ - Both modes **clear existing metric data** for each resource before re-inserting, preventing duplicate rows across sync runs.
146
+ - **Pagination**: `GetCostAndUsage` is drained via `NextPageToken`. Interrupted syncs return a cursor and resume from the same phase and window.
147
+ - **Endpoint/region**: Cost Explorer is global and is always reached through `ce.us-east-1.amazonaws.com`, signed against `us-east-1` regardless of where your resources run.
148
+ - **Errors**: `ThrottlingException` → `RateLimitError` (host backs off), `AccessDenied`/auth failures → `AuthError`, 5xx → `TransientError`. A `DataUnavailableException` from the forecast API (typical for brand-new accounts) is treated as "no forecast" rather than a hard failure.
149
+
150
+ ## Aggregates
151
+
152
+ No `aggregate()` hook — the data is already aggregated upstream, so `count` / `latest` widgets resolve cheaply against the local metric table.
@@ -0,0 +1,119 @@
1
+ import { BaseAWSConnector, BaseAWSSettings } from '@rawdash/connector-aws-shared';
2
+ import { ConnectorContext, SyncOptions, StorageHandle, SyncResult, MetricSample } from '@rawdash/core';
3
+ import { z } from 'zod';
4
+
5
+ declare const configFields: z.ZodObject<{
6
+ granularity: z.ZodOptional<z.ZodEnum<{
7
+ DAILY: "DAILY";
8
+ MONTHLY: "MONTHLY";
9
+ }>>;
10
+ groupBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ lookbackDays: z.ZodOptional<z.ZodNumber>;
12
+ accessKeyId: z.ZodOptional<z.ZodObject<{
13
+ $secret: z.ZodString;
14
+ }, z.core.$strip>>;
15
+ secretAccessKey: z.ZodOptional<z.ZodObject<{
16
+ $secret: z.ZodString;
17
+ }, z.core.$strip>>;
18
+ roleArn: z.ZodOptional<z.ZodString>;
19
+ externalId: z.ZodOptional<z.ZodString>;
20
+ }, z.core.$strip>;
21
+ interface AwsCostSettings extends BaseAWSSettings {
22
+ granularity?: 'DAILY' | 'MONTHLY';
23
+ groupBy?: readonly string[];
24
+ lookbackDays?: number;
25
+ }
26
+ interface CostMetricAmount {
27
+ Amount?: string;
28
+ Unit?: string;
29
+ }
30
+ interface ResultByTime {
31
+ TimePeriod?: {
32
+ Start?: string;
33
+ End?: string;
34
+ };
35
+ Total?: Record<string, CostMetricAmount | undefined>;
36
+ Groups?: Array<{
37
+ Keys?: string[];
38
+ Metrics?: Record<string, CostMetricAmount | undefined>;
39
+ }>;
40
+ Estimated?: boolean;
41
+ }
42
+ interface GetCostAndUsageBody {
43
+ ResultsByTime?: ResultByTime[];
44
+ NextPageToken?: string;
45
+ }
46
+ interface ForecastResult {
47
+ TimePeriod?: {
48
+ Start?: string;
49
+ End?: string;
50
+ };
51
+ MeanValue?: string;
52
+ PredictionIntervalLowerBound?: string;
53
+ PredictionIntervalUpperBound?: string;
54
+ }
55
+ interface GetCostForecastBody {
56
+ Total?: CostMetricAmount;
57
+ ForecastResultsByTime?: ForecastResult[];
58
+ }
59
+ declare function buildDailyCostSamples(body: GetCostAndUsageBody, granularity: 'DAILY' | 'MONTHLY', groupBy: readonly string[] | undefined): MetricSample[];
60
+ declare function buildForecastSamples(body: GetCostForecastBody, granularity: 'DAILY' | 'MONTHLY'): MetricSample[];
61
+ interface CostWindow {
62
+ start: string;
63
+ end: string;
64
+ }
65
+ declare function getCostWindow(options: SyncOptions, granularity: 'DAILY' | 'MONTHLY', lookbackDays: number, now?: number): CostWindow;
66
+ declare class AwsCostConnector extends BaseAWSConnector<AwsCostSettings> {
67
+ static readonly id = "aws-cost";
68
+ static readonly schemas: {
69
+ readonly daily_cost: z.ZodObject<{
70
+ ResultsByTime: z.ZodArray<z.ZodObject<{
71
+ TimePeriod: z.ZodObject<{
72
+ Start: z.ZodString;
73
+ End: z.ZodString;
74
+ }, z.core.$strip>;
75
+ Total: z.ZodOptional<z.ZodObject<{
76
+ UnblendedCost: z.ZodOptional<z.ZodObject<{
77
+ Amount: z.ZodString;
78
+ Unit: z.ZodString;
79
+ }, z.core.$strip>>;
80
+ }, z.core.$strip>>;
81
+ Groups: z.ZodOptional<z.ZodArray<z.ZodObject<{
82
+ Keys: z.ZodArray<z.ZodString>;
83
+ Metrics: z.ZodObject<{
84
+ UnblendedCost: z.ZodObject<{
85
+ Amount: z.ZodString;
86
+ Unit: z.ZodString;
87
+ }, z.core.$strip>;
88
+ }, z.core.$strip>;
89
+ }, z.core.$strip>>>;
90
+ Estimated: z.ZodOptional<z.ZodBoolean>;
91
+ }, z.core.$strip>>;
92
+ NextPageToken: z.ZodOptional<z.ZodString>;
93
+ }, z.core.$strip>;
94
+ readonly forecast: z.ZodObject<{
95
+ Total: z.ZodOptional<z.ZodObject<{
96
+ Amount: z.ZodString;
97
+ Unit: z.ZodString;
98
+ }, z.core.$strip>>;
99
+ ForecastResultsByTime: z.ZodOptional<z.ZodArray<z.ZodObject<{
100
+ TimePeriod: z.ZodObject<{
101
+ Start: z.ZodString;
102
+ End: z.ZodString;
103
+ }, z.core.$strip>;
104
+ MeanValue: z.ZodString;
105
+ PredictionIntervalLowerBound: z.ZodOptional<z.ZodString>;
106
+ PredictionIntervalUpperBound: z.ZodOptional<z.ZodString>;
107
+ }, z.core.$strip>>>;
108
+ }, z.core.$strip>;
109
+ };
110
+ static create(input: unknown, ctx?: ConnectorContext): AwsCostConnector;
111
+ readonly id = "aws-cost";
112
+ private callCostExplorer;
113
+ private buildCeHeaders;
114
+ private syncDailyCost;
115
+ private syncForecast;
116
+ sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
117
+ }
118
+
119
+ export { AwsCostConnector, type AwsCostSettings, buildDailyCostSamples, buildForecastSamples, configFields, AwsCostConnector as default, getCostWindow };