@prism-d1/cli 1.0.66 → 1.0.67
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/dist/assets/infra/lib/dashboard-stack.ts +41 -3
- package/dist/assets/infra/lib/lambda/metrics-processor.ts +12 -3
- package/dist/assets/infra/lib/lambda/otel-metrics-publisher.ts +185 -0
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +7 -4
- package/dist/assets/infra/lib/metrics-pipeline-stack.ts +114 -2
- package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts +1 -0
- package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-github-oidc.js +92 -2
- package/dist/src/commands/bootstrapper/setup-github-oidc.js.map +1 -1
- package/package.json +1 -1
|
@@ -187,7 +187,7 @@ export class DashboardStack extends cdk.Stack {
|
|
|
187
187
|
|
|
188
188
|
teamDashboard.addWidgets(
|
|
189
189
|
new cloudwatch.GraphWidget({
|
|
190
|
-
title: 'AI Input Tokens
|
|
190
|
+
title: 'AI Input Tokens',
|
|
191
191
|
left: [
|
|
192
192
|
new cloudwatch.Metric({
|
|
193
193
|
namespace: METRIC_NAMESPACE,
|
|
@@ -202,7 +202,7 @@ export class DashboardStack extends cdk.Stack {
|
|
|
202
202
|
leftYAxis: { min: 0, label: 'Tokens' },
|
|
203
203
|
}),
|
|
204
204
|
new cloudwatch.GraphWidget({
|
|
205
|
-
title: 'AI Output Tokens
|
|
205
|
+
title: 'AI Output Tokens',
|
|
206
206
|
left: [
|
|
207
207
|
new cloudwatch.Metric({
|
|
208
208
|
namespace: METRIC_NAMESPACE,
|
|
@@ -217,7 +217,7 @@ export class DashboardStack extends cdk.Stack {
|
|
|
217
217
|
leftYAxis: { min: 0, label: 'Tokens' },
|
|
218
218
|
}),
|
|
219
219
|
new cloudwatch.GraphWidget({
|
|
220
|
-
title: 'AI Cost
|
|
220
|
+
title: 'AI Cost (USD)',
|
|
221
221
|
left: [
|
|
222
222
|
new cloudwatch.Metric({
|
|
223
223
|
namespace: METRIC_NAMESPACE,
|
|
@@ -233,6 +233,44 @@ export class DashboardStack extends cdk.Stack {
|
|
|
233
233
|
}),
|
|
234
234
|
);
|
|
235
235
|
|
|
236
|
+
// Cost breakdowns by Tool and Model. SEARCH expressions auto-discover
|
|
237
|
+
// every dimension value, so new tools/models appear without dashboard
|
|
238
|
+
// changes. These series are populated by the otel-metrics-publisher
|
|
239
|
+
// (requires -c enableOtelCollector=true); the trailer path has no
|
|
240
|
+
// per-tool/per-model breakdown and leaves these widgets empty.
|
|
241
|
+
teamDashboard.addWidgets(
|
|
242
|
+
new cloudwatch.GraphWidget({
|
|
243
|
+
title: 'AI Cost by Tool (USD)',
|
|
244
|
+
left: [
|
|
245
|
+
new cloudwatch.MathExpression({
|
|
246
|
+
expression: `SEARCH('{${METRIC_NAMESPACE},Tool} MetricName="AICostUSD"', 'Sum')`,
|
|
247
|
+
usingMetrics: {},
|
|
248
|
+
label: '',
|
|
249
|
+
period: DEFAULT_PERIOD,
|
|
250
|
+
}),
|
|
251
|
+
],
|
|
252
|
+
width: 12,
|
|
253
|
+
height: 6,
|
|
254
|
+
leftYAxis: { min: 0, label: 'USD' },
|
|
255
|
+
legendPosition: cloudwatch.LegendPosition.RIGHT,
|
|
256
|
+
}),
|
|
257
|
+
new cloudwatch.GraphWidget({
|
|
258
|
+
title: 'AI Cost by Model (USD)',
|
|
259
|
+
left: [
|
|
260
|
+
new cloudwatch.MathExpression({
|
|
261
|
+
expression: `SEARCH('{${METRIC_NAMESPACE},Model} MetricName="AICostUSD"', 'Sum')`,
|
|
262
|
+
usingMetrics: {},
|
|
263
|
+
label: '',
|
|
264
|
+
period: DEFAULT_PERIOD,
|
|
265
|
+
}),
|
|
266
|
+
],
|
|
267
|
+
width: 12,
|
|
268
|
+
height: 6,
|
|
269
|
+
leftYAxis: { min: 0, label: 'USD' },
|
|
270
|
+
legendPosition: cloudwatch.LegendPosition.RIGHT,
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
|
|
236
274
|
teamDashboard.addWidgets(
|
|
237
275
|
new cloudwatch.TextWidget({
|
|
238
276
|
markdown: '### Agent Operations',
|
|
@@ -441,11 +441,20 @@ async function publishCloudWatchMetrics(
|
|
|
441
441
|
['PostMergeDefectRate', detail.ai_dora.post_merge_defect_rate, StandardUnit.Percent, true],
|
|
442
442
|
['EvalGatePassRate', detail.ai_dora.eval_gate_pass_rate, StandardUnit.Percent, true],
|
|
443
443
|
['AITestCoverageDelta', detail.ai_dora.ai_test_coverage_delta, StandardUnit.Percent, true],
|
|
444
|
-
['AIInputTokens', detail.ai_dora.total_input_tokens, StandardUnit.Count, false],
|
|
445
|
-
['AIOutputTokens', detail.ai_dora.total_output_tokens, StandardUnit.Count, false],
|
|
446
|
-
['AICostUSD', detail.ai_dora.total_cost_usd, StandardUnit.None, false],
|
|
447
444
|
];
|
|
448
445
|
|
|
446
|
+
// Token & cost metrics come from git-trailer PR sums by default. When the
|
|
447
|
+
// OTEL collector is enabled (OTEL_ENABLED=true), the otel-metrics-publisher
|
|
448
|
+
// Lambda owns AIInputTokens / AIOutputTokens / AICostUSD (higher-fidelity
|
|
449
|
+
// per-span data via `codeburn sync`) — publishing both would double-count.
|
|
450
|
+
if (process.env.OTEL_ENABLED !== 'true') {
|
|
451
|
+
aiDoraMap.push(
|
|
452
|
+
['AIInputTokens', detail.ai_dora.total_input_tokens, StandardUnit.Count, false],
|
|
453
|
+
['AIOutputTokens', detail.ai_dora.total_output_tokens, StandardUnit.Count, false],
|
|
454
|
+
['AICostUSD', detail.ai_dora.total_cost_usd, StandardUnit.None, false],
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
449
458
|
for (const [name, value, unit, scaleToPercent] of aiDoraMap) {
|
|
450
459
|
if (value != null) {
|
|
451
460
|
const publishValue = scaleToPercent && value <= 1 ? value * 100 : value;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRISM D1 — OTEL metrics publisher.
|
|
3
|
+
*
|
|
4
|
+
* Consumes the prism-d1-ai-usage DynamoDB stream (filtered at the event
|
|
5
|
+
* source to sk begins_with "OTEL#DAY#") and publishes token/cost deltas to
|
|
6
|
+
* CloudWatch under the same metric names the dashboards already read:
|
|
7
|
+
* AIInputTokens, AIOutputTokens, AICostUSD.
|
|
8
|
+
*
|
|
9
|
+
* Only active when the OTEL collector is enabled — the metrics-processor
|
|
10
|
+
* stops publishing trailer-sourced token metrics (OTEL_ENABLED=true) so
|
|
11
|
+
* these series are exclusively OTEL-fed and never double-counted.
|
|
12
|
+
*
|
|
13
|
+
* Scale guards (by design):
|
|
14
|
+
* - Deltas are exact: OTEL#DAY# items are ADD counters, so delta = NEW − OLD.
|
|
15
|
+
* - Metrics are timestamped with the SPAN day, not arrival time. Backfilled
|
|
16
|
+
* days older than the CloudWatch ingest window (14 days) are dropped —
|
|
17
|
+
* the DynamoDB aggregates and S3 OTLP archive still hold full history.
|
|
18
|
+
* - Published dimensionless (matches dashboard widgets) AND with Tool and
|
|
19
|
+
* Model dimensions (small, bounded cardinality) for per-tool / per-model
|
|
20
|
+
* slicing.
|
|
21
|
+
* - Single PutMetricData call per batch, chunked at the API limit.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
CloudWatchClient,
|
|
26
|
+
PutMetricDataCommand,
|
|
27
|
+
MetricDatum,
|
|
28
|
+
StandardUnit,
|
|
29
|
+
} from '@aws-sdk/client-cloudwatch';
|
|
30
|
+
|
|
31
|
+
const cloudwatch = new CloudWatchClient({});
|
|
32
|
+
|
|
33
|
+
const METRIC_NAMESPACE = process.env.METRIC_NAMESPACE || 'PRISM/D1/Velocity';
|
|
34
|
+
/** CloudWatch rejects datapoints older than 14 days. */
|
|
35
|
+
const MAX_METRIC_AGE_DAYS = 14;
|
|
36
|
+
/** PutMetricData accepts up to 1000 datums per call. */
|
|
37
|
+
const PUT_METRIC_BATCH_SIZE = 1000;
|
|
38
|
+
|
|
39
|
+
// ---- Types (DynamoDB stream event, trimmed to what we read) ----
|
|
40
|
+
|
|
41
|
+
interface AttributeValue {
|
|
42
|
+
S?: string;
|
|
43
|
+
N?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface StreamImage {
|
|
47
|
+
sk?: AttributeValue;
|
|
48
|
+
day?: AttributeValue;
|
|
49
|
+
tool?: AttributeValue;
|
|
50
|
+
model?: AttributeValue;
|
|
51
|
+
input_tokens?: AttributeValue;
|
|
52
|
+
output_tokens?: AttributeValue;
|
|
53
|
+
cost_usd?: AttributeValue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface StreamRecord {
|
|
57
|
+
eventName?: 'INSERT' | 'MODIFY' | 'REMOVE';
|
|
58
|
+
dynamodb?: {
|
|
59
|
+
Keys?: { pk?: AttributeValue; sk?: AttributeValue };
|
|
60
|
+
NewImage?: StreamImage;
|
|
61
|
+
OldImage?: StreamImage;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface StreamEvent {
|
|
66
|
+
Records?: StreamRecord[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- Helpers ----
|
|
70
|
+
|
|
71
|
+
function num(v: AttributeValue | undefined): number {
|
|
72
|
+
const n = Number(v?.N ?? 0);
|
|
73
|
+
return Number.isFinite(n) ? n : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Resolve the metric timestamp from the aggregate's span day. Null = drop. */
|
|
77
|
+
export function resolveTimestamp(day: string, now: Date = new Date()): Date | null {
|
|
78
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return null;
|
|
79
|
+
// Noon UTC keeps the datum inside the calendar day for all dashboard periods.
|
|
80
|
+
const ts = new Date(`${day}T12:00:00Z`);
|
|
81
|
+
if (isNaN(ts.getTime())) return null;
|
|
82
|
+
const ageMs = now.getTime() - ts.getTime();
|
|
83
|
+
// Too old for CloudWatch ingest (backfilled history) — drop, don't error.
|
|
84
|
+
if (ageMs > MAX_METRIC_AGE_DAYS * 24 * 60 * 60 * 1000) return null;
|
|
85
|
+
// Guard against clock skew: never publish a future timestamp.
|
|
86
|
+
return ts.getTime() > now.getTime() ? now : ts;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Delta {
|
|
90
|
+
day: string;
|
|
91
|
+
tool: string;
|
|
92
|
+
model: string;
|
|
93
|
+
inputTokens: number;
|
|
94
|
+
outputTokens: number;
|
|
95
|
+
costUsd: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Compute the counter delta for one OTEL#DAY# stream record. Null = skip. */
|
|
99
|
+
export function computeDelta(record: StreamRecord): Delta | null {
|
|
100
|
+
if (record.eventName !== 'INSERT' && record.eventName !== 'MODIFY') return null;
|
|
101
|
+
|
|
102
|
+
const newImage = record.dynamodb?.NewImage;
|
|
103
|
+
const sk = newImage?.sk?.S ?? record.dynamodb?.Keys?.sk?.S ?? '';
|
|
104
|
+
// Defense in depth — the event source filter should already guarantee this.
|
|
105
|
+
if (!sk.startsWith('OTEL#DAY#')) return null;
|
|
106
|
+
|
|
107
|
+
const oldImage = record.dynamodb?.OldImage;
|
|
108
|
+
const delta: Delta = {
|
|
109
|
+
day: newImage?.day?.S ?? sk.split('#')[2] ?? '',
|
|
110
|
+
tool: newImage?.tool?.S ?? sk.split('#')[3] ?? 'unknown',
|
|
111
|
+
model: newImage?.model?.S ?? sk.split('#')[4] ?? 'unknown',
|
|
112
|
+
inputTokens: num(newImage?.input_tokens) - num(oldImage?.input_tokens),
|
|
113
|
+
outputTokens: num(newImage?.output_tokens) - num(oldImage?.output_tokens),
|
|
114
|
+
costUsd: num(newImage?.cost_usd) - num(oldImage?.cost_usd),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Nothing to publish (e.g. metadata-only update) or a counter went
|
|
118
|
+
// backwards (should not happen with ADD — treat defensively as no-op).
|
|
119
|
+
if (delta.inputTokens <= 0 && delta.outputTokens <= 0 && delta.costUsd <= 0) return null;
|
|
120
|
+
return delta;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build datums for a delta across three dimension sets:
|
|
125
|
+
* dimensionless (dashboards), Tool, and Model. CloudWatch cannot roll up
|
|
126
|
+
* across dimensions, so each slice needs its own series. Tool and model
|
|
127
|
+
* cardinality are both small and bounded.
|
|
128
|
+
*/
|
|
129
|
+
export function buildDatums(delta: Delta, timestamp: Date): MetricDatum[] {
|
|
130
|
+
const metrics: Array<[string, number, StandardUnit]> = [
|
|
131
|
+
['AIInputTokens', delta.inputTokens, StandardUnit.Count],
|
|
132
|
+
['AIOutputTokens', delta.outputTokens, StandardUnit.Count],
|
|
133
|
+
['AICostUSD', delta.costUsd, StandardUnit.None],
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const dimensionSets = [
|
|
137
|
+
[],
|
|
138
|
+
[{ Name: 'Tool', Value: delta.tool }],
|
|
139
|
+
[{ Name: 'Model', Value: delta.model }],
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const datums: MetricDatum[] = [];
|
|
143
|
+
for (const [name, value, unit] of metrics) {
|
|
144
|
+
if (value <= 0) continue;
|
|
145
|
+
for (const dimensions of dimensionSets) {
|
|
146
|
+
datums.push({ MetricName: name, Value: value, Unit: unit, Timestamp: timestamp, Dimensions: dimensions });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return datums;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- Handler ----
|
|
153
|
+
|
|
154
|
+
export async function handler(event: StreamEvent): Promise<void> {
|
|
155
|
+
const now = new Date();
|
|
156
|
+
const datums: MetricDatum[] = [];
|
|
157
|
+
let processed = 0;
|
|
158
|
+
let droppedOld = 0;
|
|
159
|
+
|
|
160
|
+
for (const record of event.Records ?? []) {
|
|
161
|
+
const delta = computeDelta(record);
|
|
162
|
+
if (!delta) continue;
|
|
163
|
+
|
|
164
|
+
const timestamp = resolveTimestamp(delta.day, now);
|
|
165
|
+
if (!timestamp) {
|
|
166
|
+
droppedOld++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
datums.push(...buildDatums(delta, timestamp));
|
|
171
|
+
processed++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < datums.length; i += PUT_METRIC_BATCH_SIZE) {
|
|
175
|
+
await cloudwatch.send(new PutMetricDataCommand({
|
|
176
|
+
Namespace: METRIC_NAMESPACE,
|
|
177
|
+
MetricData: datums.slice(i, i + PUT_METRIC_BATCH_SIZE),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(
|
|
182
|
+
`[otel-metrics-publisher] records=${event.Records?.length ?? 0} ` +
|
|
183
|
+
`published=${processed} droppedTooOld=${droppedOld} datums=${datums.length}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* newly-seen spans, bucketed by SPAN date, not arrival date).
|
|
12
12
|
*
|
|
13
13
|
* Item shapes in the AI-usage table:
|
|
14
|
-
* pk=USER#<identity> sk=SPAN#<timestamp>#<spanId>
|
|
15
|
-
* pk=USER#<identity> sk=OTEL#DAY#<yyyy-mm-dd>#<tool>
|
|
14
|
+
* pk=USER#<identity> sk=SPAN#<timestamp>#<spanId> raw span, TTL 90d
|
|
15
|
+
* pk=USER#<identity> sk=OTEL#DAY#<yyyy-mm-dd>#<tool>#<model> daily aggregate
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import {
|
|
@@ -272,15 +272,17 @@ async function writeSpanIfNew(user: string, s: ParsedSpan): Promise<boolean> {
|
|
|
272
272
|
/** ADD-increment the daily aggregate for a newly-seen span (bucketed by SPAN date). */
|
|
273
273
|
async function bumpDailyAggregate(user: string, s: ParsedSpan): Promise<void> {
|
|
274
274
|
const day = s.timestamp.slice(0, 10);
|
|
275
|
+
// '#' is the key delimiter — strip it from the model to keep the sk parseable.
|
|
276
|
+
const model = (s.model || 'unknown').replace(/#/g, '');
|
|
275
277
|
await dynamoClient.send(new UpdateItemCommand({
|
|
276
278
|
TableName: AI_USAGE_TABLE,
|
|
277
279
|
Key: {
|
|
278
280
|
pk: { S: `USER#${user}` },
|
|
279
|
-
sk: { S: `OTEL#DAY#${day}#${s.tool}` },
|
|
281
|
+
sk: { S: `OTEL#DAY#${day}#${s.tool}#${model}` },
|
|
280
282
|
},
|
|
281
283
|
UpdateExpression:
|
|
282
284
|
'ADD input_tokens :in, output_tokens :out, cost_usd :cost, call_count :one ' +
|
|
283
|
-
'SET record_type = :rt, tool = :tool, #day = :day, updated_at = :now',
|
|
285
|
+
'SET record_type = :rt, tool = :tool, model = :model, #day = :day, updated_at = :now',
|
|
284
286
|
ExpressionAttributeNames: { '#day': 'day' },
|
|
285
287
|
ExpressionAttributeValues: {
|
|
286
288
|
':in': { N: String(s.inputTokens) },
|
|
@@ -289,6 +291,7 @@ async function bumpDailyAggregate(user: string, s: ParsedSpan): Promise<void> {
|
|
|
289
291
|
':one': { N: '1' },
|
|
290
292
|
':rt': { S: 'OTEL_DAY' },
|
|
291
293
|
':tool': { S: s.tool },
|
|
294
|
+
':model': { S: model },
|
|
292
295
|
':day': { S: day },
|
|
293
296
|
':now': { S: new Date().toISOString() },
|
|
294
297
|
},
|
|
@@ -2,6 +2,8 @@ import * as cdk from 'aws-cdk-lib';
|
|
|
2
2
|
import * as events from 'aws-cdk-lib/aws-events';
|
|
3
3
|
import * as targets from 'aws-cdk-lib/aws-events-targets';
|
|
4
4
|
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
5
|
+
import * as eventsources from 'aws-cdk-lib/aws-lambda-event-sources';
|
|
6
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
5
7
|
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
6
8
|
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
7
9
|
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
@@ -133,10 +135,14 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
133
135
|
// -------------------------------------------------------
|
|
134
136
|
// DynamoDB AI-usage table — user-keyed, KMS encrypted
|
|
135
137
|
// pk = USER#<email>
|
|
136
|
-
// sk = COMMIT#<ts>#<sha>#<tool> | SPAN#<ts>#<spanId> | OTEL#DAY#<date>#<tool>
|
|
138
|
+
// sk = COMMIT#<ts>#<sha>#<tool> | SPAN#<ts>#<spanId> | OTEL#DAY#<date>#<tool>#<model>
|
|
137
139
|
// Sparse GSIs (only commit-level items set them) invert the user key so the
|
|
138
140
|
// same table answers by-repo / by-PR / by-commit / global-by-date.
|
|
139
141
|
// -------------------------------------------------------
|
|
142
|
+
// Read the OTEL collector flag early — it also controls the table stream
|
|
143
|
+
// and which pipeline owns the token/cost CloudWatch metrics.
|
|
144
|
+
const enableOtelCollector = this.node.tryGetContext('enableOtelCollector') === 'true';
|
|
145
|
+
|
|
140
146
|
this.aiUsageTable = new dynamodb.Table(this, 'AiUsageTable', {
|
|
141
147
|
tableName: 'prism-d1-ai-usage',
|
|
142
148
|
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
|
|
@@ -147,6 +153,9 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
147
153
|
timeToLiveAttribute: 'ttl',
|
|
148
154
|
encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED,
|
|
149
155
|
encryptionKey: prismKmsKey,
|
|
156
|
+
// NEW_AND_OLD_IMAGES so the otel-metrics-publisher can compute exact
|
|
157
|
+
// counter deltas (new − old) on OTEL#DAY# aggregate updates.
|
|
158
|
+
stream: enableOtelCollector ? dynamodb.StreamViewType.NEW_AND_OLD_IMAGES : undefined,
|
|
150
159
|
});
|
|
151
160
|
for (const [indexName, pkAttr, skAttr] of [
|
|
152
161
|
['by-repo', 'gsi_repo', 'gsi_repo_sk'],
|
|
@@ -222,6 +231,10 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
222
231
|
EVENTS_TABLE: this.eventsTable.tableName,
|
|
223
232
|
METADATA_TABLE: this.metadataTable.tableName,
|
|
224
233
|
METRIC_NAMESPACE: 'PRISM/D1/Velocity',
|
|
234
|
+
// When the OTEL collector is enabled, the otel-metrics-publisher owns
|
|
235
|
+
// AIInputTokens/AIOutputTokens/AICostUSD — the processor skips its
|
|
236
|
+
// trailer-sourced copies to avoid double-counting.
|
|
237
|
+
OTEL_ENABLED: enableOtelCollector ? 'true' : 'false',
|
|
225
238
|
},
|
|
226
239
|
logRetention: logs.RetentionDays.ONE_MONTH,
|
|
227
240
|
description: 'Processes PRISM D1 metric events from EventBridge into DynamoDB and CloudWatch',
|
|
@@ -295,7 +308,6 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
295
308
|
// Per-user AI usage flows directly from codeburn sync to the
|
|
296
309
|
// ai-usage table (spans + daily aggregates).
|
|
297
310
|
// -------------------------------------------------------
|
|
298
|
-
const enableOtelCollector = this.node.tryGetContext('enableOtelCollector') === 'true';
|
|
299
311
|
if (enableOtelCollector) {
|
|
300
312
|
this.otelCollector = new OtelCollectorConstruct(this, 'OtelCollector', {
|
|
301
313
|
aiUsageTable: this.aiUsageTable,
|
|
@@ -305,6 +317,97 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
305
317
|
externalClientId: this.node.tryGetContext('otelClientId') as string | undefined,
|
|
306
318
|
identityClaim: this.node.tryGetContext('otelIdentityClaim') as string | undefined,
|
|
307
319
|
});
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------
|
|
322
|
+
// OTEL metrics publisher — DDB stream → CloudWatch.
|
|
323
|
+
// Replaces the trailer-sourced AIInputTokens/AIOutputTokens/AICostUSD
|
|
324
|
+
// series with per-span OTEL data (same metric names — dashboards
|
|
325
|
+
// require no changes). The event-source filter limits invocations to
|
|
326
|
+
// OTEL#DAY# aggregate updates, so span-volume writes never invoke it.
|
|
327
|
+
// ---------------------------------------------------
|
|
328
|
+
const otelPublisherDlq = new sqs.Queue(this, 'OtelPublisherDLQ', {
|
|
329
|
+
queueName: 'prism-d1-otel-publisher-dlq',
|
|
330
|
+
encryptionMasterKey: prismKmsKey,
|
|
331
|
+
enforceSSL: true,
|
|
332
|
+
retentionPeriod: cdk.Duration.days(14),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const otelMetricsPublisher = new lambda.Function(this, 'OtelMetricsPublisher', {
|
|
336
|
+
functionName: 'prism-d1-otel-metrics-publisher',
|
|
337
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
338
|
+
handler: 'otel-metrics-publisher.handler',
|
|
339
|
+
...lambdaVpcProps,
|
|
340
|
+
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda'), {
|
|
341
|
+
bundling: {
|
|
342
|
+
image: lambda.Runtime.NODEJS_22_X.bundlingImage,
|
|
343
|
+
command: [
|
|
344
|
+
'bash', '-c',
|
|
345
|
+
[
|
|
346
|
+
'npm init -y > /dev/null 2>&1',
|
|
347
|
+
'npm install --save @aws-sdk/client-cloudwatch esbuild > /dev/null 2>&1',
|
|
348
|
+
'npx esbuild otel-metrics-publisher.ts --bundle --platform=node --target=node22 --outfile=/asset-output/otel-metrics-publisher.js --external:@aws-sdk/*',
|
|
349
|
+
].join(' && '),
|
|
350
|
+
],
|
|
351
|
+
local: {
|
|
352
|
+
tryBundle(outputDir: string): boolean {
|
|
353
|
+
try {
|
|
354
|
+
const { execSync } = require('child_process');
|
|
355
|
+
execSync(
|
|
356
|
+
`npx esbuild ${path.join(__dirname, 'lambda', 'otel-metrics-publisher.ts')} --bundle --platform=node --target=node22 --outfile=${path.join(outputDir, 'otel-metrics-publisher.js')} --external:@aws-sdk/*`,
|
|
357
|
+
{ stdio: 'pipe' },
|
|
358
|
+
);
|
|
359
|
+
return true;
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
}),
|
|
367
|
+
timeout: cdk.Duration.seconds(60),
|
|
368
|
+
memorySize: 256,
|
|
369
|
+
environment: {
|
|
370
|
+
METRIC_NAMESPACE: 'PRISM/D1/Velocity',
|
|
371
|
+
},
|
|
372
|
+
logRetention: logs.RetentionDays.ONE_MONTH,
|
|
373
|
+
description: 'Publishes OTEL#DAY aggregate deltas from the ai-usage stream as CloudWatch token/cost metrics',
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
otelMetricsPublisher.addToRolePolicy(
|
|
377
|
+
new iam.PolicyStatement({
|
|
378
|
+
effect: iam.Effect.ALLOW,
|
|
379
|
+
actions: ['cloudwatch:PutMetricData'],
|
|
380
|
+
resources: ['*'],
|
|
381
|
+
conditions: {
|
|
382
|
+
StringEquals: {
|
|
383
|
+
'cloudwatch:namespace': 'PRISM/D1/Velocity',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
otelMetricsPublisher.addEventSource(
|
|
390
|
+
new eventsources.DynamoEventSource(this.aiUsageTable, {
|
|
391
|
+
startingPosition: lambda.StartingPosition.LATEST,
|
|
392
|
+
batchSize: 100,
|
|
393
|
+
maxBatchingWindow: cdk.Duration.seconds(10),
|
|
394
|
+
bisectBatchOnError: true,
|
|
395
|
+
retryAttempts: 3,
|
|
396
|
+
onFailure: new eventsources.SqsDlq(otelPublisherDlq),
|
|
397
|
+
// Only OTEL#DAY# aggregate inserts/updates — SPAN# and COMMIT#
|
|
398
|
+
// writes (the overwhelming majority at scale) never invoke us.
|
|
399
|
+
filters: [
|
|
400
|
+
lambda.FilterCriteria.filter({
|
|
401
|
+
eventName: lambda.FilterRule.or('INSERT', 'MODIFY'),
|
|
402
|
+
dynamodb: {
|
|
403
|
+
Keys: {
|
|
404
|
+
sk: { S: lambda.FilterRule.beginsWith('OTEL#DAY#') },
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
}),
|
|
408
|
+
],
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
308
411
|
}
|
|
309
412
|
|
|
310
413
|
// -------------------------------------------------------
|
|
@@ -737,6 +840,15 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
737
840
|
reason: 'CDK grantReadData on DynamoDB generates wildcard for GSI index ARNs via token reference',
|
|
738
841
|
appliesTo: ['Resource::<EventsTableD24865E5.Arn>/index/*', 'Resource::<AiUsageTable79F37CE0.Arn>/index/*'],
|
|
739
842
|
},
|
|
843
|
+
{
|
|
844
|
+
id: 'AwsSolutions-IAM5',
|
|
845
|
+
reason: 'DynamoDB stream event source mapping requires ListStreams which does not support resource-level scoping beyond the table stream wildcard',
|
|
846
|
+
appliesTo: ['Resource::<AiUsageTable79F37CE0.Arn>/stream/*', 'Resource::*'],
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
id: 'AwsSolutions-SQS3',
|
|
850
|
+
reason: 'The OTEL publisher queue is itself a DLQ for the DynamoDB stream event source',
|
|
851
|
+
},
|
|
740
852
|
{ id: 'AwsSolutions-L1', reason: 'All Lambdas use nodejs22.x which is the latest Node.js runtime available' },
|
|
741
853
|
]);
|
|
742
854
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-github-oidc.d.ts","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/setup-github-oidc.ts"],"names":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"setup-github-oidc.d.ts","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/setup-github-oidc.ts"],"names":[],"mappings":";;;;;;iBAqDqB;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE;;AAN3D,wBAyNE"}
|
|
@@ -19,10 +19,38 @@ function run(cmd) {
|
|
|
19
19
|
return { ok: false, stdout: '', stderr: (err.stderr || err.message || '').trim() };
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch a GitHub API path, trying progressively more authenticated clients:
|
|
24
|
+
* unauthenticated (public repos/users) → GITHUB_TOKEN env → gh CLI token.
|
|
25
|
+
* Returns the parsed JSON body, or null if every attempt fails.
|
|
26
|
+
*/
|
|
27
|
+
async function githubApi(apiPath) {
|
|
28
|
+
const tokens = [undefined];
|
|
29
|
+
if (process.env.GITHUB_TOKEN)
|
|
30
|
+
tokens.push(process.env.GITHUB_TOKEN);
|
|
31
|
+
const ghToken = run('gh auth token');
|
|
32
|
+
if (ghToken.ok && ghToken.stdout)
|
|
33
|
+
tokens.push(ghToken.stdout);
|
|
34
|
+
for (const token of tokens) {
|
|
35
|
+
try {
|
|
36
|
+
const headers = { Accept: 'application/vnd.github+json' };
|
|
37
|
+
if (token)
|
|
38
|
+
headers.Authorization = `Bearer ${token}`;
|
|
39
|
+
const res = await fetch(`https://api.github.com${apiPath}`, { headers });
|
|
40
|
+
if (res.ok)
|
|
41
|
+
return await res.json();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Network error — try the next credential source.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
22
49
|
export default {
|
|
23
50
|
description: 'Set up GitHub OIDC identity provider and IAM role for GitHub Actions',
|
|
24
51
|
options: [
|
|
25
52
|
{ flags: '--global', description: 'Create a single role for all repos in the org/user (wildcard sub claim)' },
|
|
53
|
+
{ flags: '--pin-ids', description: 'Pin the trust policy to immutable GitHub owner/repo IDs (protects against namespace resurrection; may require gh CLI or GITHUB_TOKEN for private repos)' },
|
|
26
54
|
],
|
|
27
55
|
async action(opts) {
|
|
28
56
|
console.log('\n🔐 GitHub OIDC Setup for AWS\n');
|
|
@@ -34,6 +62,7 @@ export default {
|
|
|
34
62
|
process.exit(1);
|
|
35
63
|
}
|
|
36
64
|
let repoPath;
|
|
65
|
+
let repoName;
|
|
37
66
|
let roleName;
|
|
38
67
|
if (opts.global) {
|
|
39
68
|
repoPath = `${githubUsername}/*`;
|
|
@@ -41,11 +70,66 @@ export default {
|
|
|
41
70
|
console.log(`\nConfiguring OIDC for ALL repos: ${githubUsername}/*`);
|
|
42
71
|
}
|
|
43
72
|
else {
|
|
44
|
-
|
|
73
|
+
repoName = await prompt('Repository name', 'prism-d1-velocity');
|
|
45
74
|
repoPath = `${githubUsername}/${repoName}`;
|
|
46
75
|
roleName = `GitHubActions-${repoName}`;
|
|
47
76
|
console.log(`\nConfiguring OIDC for: ${repoPath}`);
|
|
48
77
|
}
|
|
78
|
+
// -----------------------------------------------------------------
|
|
79
|
+
// Build the OIDC sub patterns. GitHub repos created (or renamed /
|
|
80
|
+
// transferred) after mid-2026 present an immutable-ID sub format:
|
|
81
|
+
// repo:<owner>@<ownerId>/<repo>@<repoId>:<ref>
|
|
82
|
+
// Older repos keep the classic format:
|
|
83
|
+
// repo:<owner>/<repo>:<ref>
|
|
84
|
+
// We trust both. '@' is not a legal character in GitHub owner or repo
|
|
85
|
+
// names, so the wildcarded ID segments can only ever match numeric IDs —
|
|
86
|
+
// names cannot be crafted to collide. Wildcard IDs do forgo namespace-
|
|
87
|
+
// resurrection protection; use --pin-ids to lock to the exact IDs.
|
|
88
|
+
// -----------------------------------------------------------------
|
|
89
|
+
let ownerId;
|
|
90
|
+
let repoId;
|
|
91
|
+
if (opts.pinIds) {
|
|
92
|
+
console.log('\nResolving immutable GitHub IDs (--pin-ids)...');
|
|
93
|
+
if (opts.global) {
|
|
94
|
+
const user = await githubApi(`/users/${githubUsername}`);
|
|
95
|
+
if (user?.id)
|
|
96
|
+
ownerId = String(user.id);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const repo = await githubApi(`/repos/${githubUsername}/${repoName}`);
|
|
100
|
+
if (repo?.id && repo?.owner?.id) {
|
|
101
|
+
repoId = String(repo.id);
|
|
102
|
+
ownerId = String(repo.owner.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!ownerId) {
|
|
106
|
+
console.log(' Could not resolve IDs automatically (private repo without gh CLI or GITHUB_TOKEN?).');
|
|
107
|
+
console.log(` You can find them with: gh api repos/${repoPath.replace('/*', '/<repo>')} --jq '"\\(.owner.id) \\(.id)"'`);
|
|
108
|
+
const pasted = await prompt(opts.global
|
|
109
|
+
? 'GitHub owner ID (blank = fall back to wildcard IDs)'
|
|
110
|
+
: 'GitHub "<ownerId> <repoId>" (blank = fall back to wildcard IDs)');
|
|
111
|
+
const parts = pasted.split(/\s+/).filter(Boolean);
|
|
112
|
+
if (parts[0])
|
|
113
|
+
ownerId = parts[0];
|
|
114
|
+
if (parts[1])
|
|
115
|
+
repoId = parts[1];
|
|
116
|
+
}
|
|
117
|
+
if (ownerId) {
|
|
118
|
+
console.log(` ✓ Pinned: owner ID ${ownerId}${repoId ? `, repo ID ${repoId}` : ''}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.log(' Falling back to wildcard ID matching.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const subPatterns = opts.global
|
|
125
|
+
? [
|
|
126
|
+
`repo:${githubUsername}/*:*`,
|
|
127
|
+
`repo:${githubUsername}@${ownerId ?? '*'}/*:*`,
|
|
128
|
+
]
|
|
129
|
+
: [
|
|
130
|
+
`repo:${githubUsername}/${repoName}:*`,
|
|
131
|
+
`repo:${githubUsername}@${ownerId ?? '*'}/${repoName}@${repoId ?? '*'}:*`,
|
|
132
|
+
];
|
|
49
133
|
// Check AWS credentials
|
|
50
134
|
const sts = run('aws sts get-caller-identity --query Account --output text');
|
|
51
135
|
if (!sts.ok) {
|
|
@@ -91,7 +175,7 @@ export default {
|
|
|
91
175
|
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
|
|
92
176
|
},
|
|
93
177
|
StringLike: {
|
|
94
|
-
'token.actions.githubusercontent.com:sub':
|
|
178
|
+
'token.actions.githubusercontent.com:sub': subPatterns,
|
|
95
179
|
},
|
|
96
180
|
},
|
|
97
181
|
},
|
|
@@ -156,6 +240,12 @@ export default {
|
|
|
156
240
|
console.log('════════════════════════════════════════════════');
|
|
157
241
|
console.log(`\n Role ARN: ${roleArn}`);
|
|
158
242
|
console.log(` Repository: ${repoPath}`);
|
|
243
|
+
console.log(' Trusted sub patterns:');
|
|
244
|
+
for (const p of subPatterns)
|
|
245
|
+
console.log(` - ${p}`);
|
|
246
|
+
if (!opts.pinIds) {
|
|
247
|
+
console.log(' Tip: rerun with --pin-ids to lock the trust policy to immutable GitHub IDs.');
|
|
248
|
+
}
|
|
159
249
|
console.log('\n Next step: Add a repository secret in GitHub');
|
|
160
250
|
console.log('');
|
|
161
251
|
console.log(' In your GitHub repository, go to:');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-github-oidc.js","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/setup-github-oidc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,SAAS,MAAM,CAAC,QAAgB,EAAE,YAAqB;IACrD,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7E,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,KAAK,YAAY,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,EAAE,CAAC,QAAQ,CAAC,GAAG,QAAQ,GAAG,MAAM,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE;YAC/C,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,YAAY,IAAI,EAAE,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,GAAG,CAAC,GAAW;IACtB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3F,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;IACrF,CAAC;AACH,CAAC;AAED,eAAe;IACb,WAAW,EAAE,sEAAsE;IACnF,OAAO,EAAE;QACP,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,yEAAyE,EAAE;
|
|
1
|
+
{"version":3,"file":"setup-github-oidc.js","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/setup-github-oidc.ts"],"names":[],"mappings":"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"}
|