@prism-d1/cli 1.0.66 → 1.0.68
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/constructs/otel-collector-construct.ts +7 -2
- package/dist/assets/infra/lib/dashboard-stack.ts +45 -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/assets/infra/package.json +1 -0
- 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/dist/src/commands/bootstrapper/setup-otel-sync.d.ts +15 -0
- package/dist/src/commands/bootstrapper/setup-otel-sync.d.ts.map +1 -0
- package/dist/src/commands/bootstrapper/setup-otel-sync.js +332 -0
- package/dist/src/commands/bootstrapper/setup-otel-sync.js.map +1 -0
- package/package.json +1 -1
|
@@ -287,16 +287,21 @@ export class OtelCollectorConstruct extends Construct {
|
|
|
287
287
|
|
|
288
288
|
// -------------------------------------------------------
|
|
289
289
|
// Outputs — everything needed to run `codeburn sync setup <url>`
|
|
290
|
+
// Logical IDs are overridden so the deploy output keys match the
|
|
291
|
+
// README verbatim (construct nesting would otherwise mangle them to
|
|
292
|
+
// OtelCollectorOtelCollectorUrl<hash>).
|
|
290
293
|
// -------------------------------------------------------
|
|
291
|
-
new cdk.CfnOutput(this, 'OtelCollectorUrl', {
|
|
294
|
+
const urlOutput = new cdk.CfnOutput(this, 'OtelCollectorUrl', {
|
|
292
295
|
value: this.httpApi.apiEndpoint,
|
|
293
296
|
description: 'Run: codeburn sync setup <this URL>',
|
|
294
297
|
});
|
|
298
|
+
urlOutput.overrideLogicalId('OtelCollectorUrl');
|
|
295
299
|
if (this.userPool) {
|
|
296
|
-
new cdk.CfnOutput(this, 'OtelUserPoolId', {
|
|
300
|
+
const poolOutput = new cdk.CfnOutput(this, 'OtelUserPoolId', {
|
|
297
301
|
value: this.userPool.userPoolId,
|
|
298
302
|
description: 'Create users: aws cognito-idp admin-create-user --user-pool-id <this> --username <email>',
|
|
299
303
|
});
|
|
304
|
+
poolOutput.overrideLogicalId('OtelUserPoolId');
|
|
300
305
|
}
|
|
301
306
|
}
|
|
302
307
|
}
|
|
@@ -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,48 @@ 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
|
+
// Dynamic label: each discovered series is legend-labeled with
|
|
249
|
+
// its Tool dimension value (e.g. "claude-code").
|
|
250
|
+
label: "${PROP('Dim.Tool')}",
|
|
251
|
+
period: DEFAULT_PERIOD,
|
|
252
|
+
}),
|
|
253
|
+
],
|
|
254
|
+
width: 12,
|
|
255
|
+
height: 6,
|
|
256
|
+
leftYAxis: { min: 0, label: 'USD' },
|
|
257
|
+
legendPosition: cloudwatch.LegendPosition.RIGHT,
|
|
258
|
+
}),
|
|
259
|
+
new cloudwatch.GraphWidget({
|
|
260
|
+
title: 'AI Cost by Model (USD)',
|
|
261
|
+
left: [
|
|
262
|
+
new cloudwatch.MathExpression({
|
|
263
|
+
expression: `SEARCH('{${METRIC_NAMESPACE},Model} MetricName="AICostUSD"', 'Sum')`,
|
|
264
|
+
usingMetrics: {},
|
|
265
|
+
// Dynamic label: each discovered series is legend-labeled with
|
|
266
|
+
// its Model dimension value (e.g. "claude-sonnet-4-5-20250929").
|
|
267
|
+
label: "${PROP('Dim.Model')}",
|
|
268
|
+
period: DEFAULT_PERIOD,
|
|
269
|
+
}),
|
|
270
|
+
],
|
|
271
|
+
width: 12,
|
|
272
|
+
height: 6,
|
|
273
|
+
leftYAxis: { min: 0, label: 'USD' },
|
|
274
|
+
legendPosition: cloudwatch.LegendPosition.RIGHT,
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
236
278
|
teamDashboard.addWidgets(
|
|
237
279
|
new cloudwatch.TextWidget({
|
|
238
280
|
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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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
description: string;
|
|
3
|
+
options: {
|
|
4
|
+
flags: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}[];
|
|
7
|
+
action(opts: {
|
|
8
|
+
url?: string;
|
|
9
|
+
interval?: string;
|
|
10
|
+
status?: boolean;
|
|
11
|
+
remove?: boolean;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export default _default;
|
|
15
|
+
//# sourceMappingURL=setup-otel-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-otel-sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/setup-otel-sync.ts"],"names":[],"mappings":";;;;;;iBAgPqB;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE;;AAR5F,wBAiIE"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { platform, homedir } from 'node:os';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
function prompt(question, defaultValue) {
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim() || defaultValue || '');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function run(cmd) {
|
|
17
|
+
try {
|
|
18
|
+
const stdout = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
19
|
+
return { ok: true, stdout, stderr: '' };
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
return { ok: false, stdout: '', stderr: (err.stderr || err.message || '').trim() };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// ---- Constants ----
|
|
26
|
+
const CRON_MARKER = '# prism-otel-sync';
|
|
27
|
+
const PLIST_LABEL = 'com.prism.otel-sync';
|
|
28
|
+
const SCHTASK_NAME = 'PrismOtelSync';
|
|
29
|
+
const LOG_DIR = join(homedir(), '.prism', 'logs');
|
|
30
|
+
// ---- Scheduler helpers per OS ----
|
|
31
|
+
function getCodeburnPath() {
|
|
32
|
+
const which = platform() === 'win32' ? 'where codeburn' : 'which codeburn';
|
|
33
|
+
const result = run(which);
|
|
34
|
+
return result.ok ? result.stdout.split('\n')[0] : null;
|
|
35
|
+
}
|
|
36
|
+
function buildSyncCommand(codeburnPath, since) {
|
|
37
|
+
return `${codeburnPath} sync push --since ${since}`;
|
|
38
|
+
}
|
|
39
|
+
// ---- Linux: user crontab ----
|
|
40
|
+
function linuxScheduleExists() {
|
|
41
|
+
const result = run('crontab -l');
|
|
42
|
+
return result.ok && result.stdout.includes(CRON_MARKER);
|
|
43
|
+
}
|
|
44
|
+
function linuxInstallSchedule(codeburnPath, intervalHours) {
|
|
45
|
+
const existing = run('crontab -l');
|
|
46
|
+
const lines = existing.ok ? existing.stdout.split('\n').filter(l => !l.includes(CRON_MARKER)) : [];
|
|
47
|
+
const logFile = join(LOG_DIR, 'otel-sync.log');
|
|
48
|
+
const cronExpr = `0 */${intervalHours} * * *`;
|
|
49
|
+
lines.push(`${CRON_MARKER}`);
|
|
50
|
+
lines.push(`${cronExpr} ${buildSyncCommand(codeburnPath, '7d')} >> ${logFile} 2>&1`);
|
|
51
|
+
const tmpFile = '/tmp/prism-crontab.tmp';
|
|
52
|
+
writeFileSync(tmpFile, lines.join('\n') + '\n');
|
|
53
|
+
const result = run(`crontab ${tmpFile}`);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
throw new Error(`Failed to install crontab: ${result.stderr}`);
|
|
56
|
+
}
|
|
57
|
+
function linuxRemoveSchedule() {
|
|
58
|
+
const existing = run('crontab -l');
|
|
59
|
+
if (!existing.ok)
|
|
60
|
+
return;
|
|
61
|
+
const lines = existing.stdout.split('\n').filter(l => !l.includes(CRON_MARKER) && !l.includes('codeburn sync push'));
|
|
62
|
+
const tmpFile = '/tmp/prism-crontab.tmp';
|
|
63
|
+
writeFileSync(tmpFile, lines.join('\n') + '\n');
|
|
64
|
+
run(`crontab ${tmpFile}`);
|
|
65
|
+
}
|
|
66
|
+
// ---- macOS: LaunchAgent ----
|
|
67
|
+
function darwinPlistPath() {
|
|
68
|
+
return join(homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
69
|
+
}
|
|
70
|
+
function darwinScheduleExists() {
|
|
71
|
+
return existsSync(darwinPlistPath());
|
|
72
|
+
}
|
|
73
|
+
function darwinInstallSchedule(codeburnPath, intervalHours) {
|
|
74
|
+
const logFile = join(LOG_DIR, 'otel-sync.log');
|
|
75
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
76
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
77
|
+
<plist version="1.0">
|
|
78
|
+
<dict>
|
|
79
|
+
<key>Label</key>
|
|
80
|
+
<string>${PLIST_LABEL}</string>
|
|
81
|
+
<key>ProgramArguments</key>
|
|
82
|
+
<array>
|
|
83
|
+
<string>${codeburnPath}</string>
|
|
84
|
+
<string>sync</string>
|
|
85
|
+
<string>push</string>
|
|
86
|
+
<string>--since</string>
|
|
87
|
+
<string>7d</string>
|
|
88
|
+
</array>
|
|
89
|
+
<key>StartInterval</key>
|
|
90
|
+
<integer>${intervalHours * 3600}</integer>
|
|
91
|
+
<key>StandardOutPath</key>
|
|
92
|
+
<string>${logFile}</string>
|
|
93
|
+
<key>StandardErrorPath</key>
|
|
94
|
+
<string>${logFile}</string>
|
|
95
|
+
<key>RunAtLoad</key>
|
|
96
|
+
<true/>
|
|
97
|
+
</dict>
|
|
98
|
+
</plist>`;
|
|
99
|
+
const plistPath = darwinPlistPath();
|
|
100
|
+
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
101
|
+
writeFileSync(plistPath, plist);
|
|
102
|
+
run(`launchctl unload ${plistPath} 2>/dev/null`);
|
|
103
|
+
const result = run(`launchctl load ${plistPath}`);
|
|
104
|
+
if (!result.ok)
|
|
105
|
+
throw new Error(`Failed to load LaunchAgent: ${result.stderr}`);
|
|
106
|
+
}
|
|
107
|
+
function darwinRemoveSchedule() {
|
|
108
|
+
const plistPath = darwinPlistPath();
|
|
109
|
+
if (existsSync(plistPath)) {
|
|
110
|
+
run(`launchctl unload ${plistPath}`);
|
|
111
|
+
run(`rm ${plistPath}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ---- Windows: schtasks ----
|
|
115
|
+
function windowsScheduleExists() {
|
|
116
|
+
const result = run(`schtasks /query /tn ${SCHTASK_NAME} /fo CSV`);
|
|
117
|
+
return result.ok;
|
|
118
|
+
}
|
|
119
|
+
function windowsInstallSchedule(codeburnPath, intervalHours) {
|
|
120
|
+
const logFile = join(LOG_DIR, 'otel-sync.log');
|
|
121
|
+
const cmd = `${codeburnPath} sync push --since 7d >> "${logFile}" 2>&1`;
|
|
122
|
+
const result = run(`schtasks /create /tn ${SCHTASK_NAME} /sc hourly /mo ${intervalHours} ` +
|
|
123
|
+
`/tr "cmd /c ${cmd.replace(/"/g, '\\"')}" /f`);
|
|
124
|
+
if (!result.ok)
|
|
125
|
+
throw new Error(`Failed to create scheduled task: ${result.stderr}`);
|
|
126
|
+
}
|
|
127
|
+
function windowsRemoveSchedule() {
|
|
128
|
+
run(`schtasks /delete /tn ${SCHTASK_NAME} /f`);
|
|
129
|
+
}
|
|
130
|
+
// ---- Unified scheduler interface ----
|
|
131
|
+
function scheduleExists() {
|
|
132
|
+
switch (platform()) {
|
|
133
|
+
case 'darwin': return darwinScheduleExists();
|
|
134
|
+
case 'win32': return windowsScheduleExists();
|
|
135
|
+
default: return linuxScheduleExists();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function installSchedule(codeburnPath, intervalHours) {
|
|
139
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
140
|
+
switch (platform()) {
|
|
141
|
+
case 'darwin': return darwinInstallSchedule(codeburnPath, intervalHours);
|
|
142
|
+
case 'win32': return windowsInstallSchedule(codeburnPath, intervalHours);
|
|
143
|
+
default: return linuxInstallSchedule(codeburnPath, intervalHours);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function removeSchedule() {
|
|
147
|
+
switch (platform()) {
|
|
148
|
+
case 'darwin': return darwinRemoveSchedule();
|
|
149
|
+
case 'win32': return windowsRemoveSchedule();
|
|
150
|
+
default: return linuxRemoveSchedule();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function schedulerName() {
|
|
154
|
+
switch (platform()) {
|
|
155
|
+
case 'darwin': return 'LaunchAgent';
|
|
156
|
+
case 'win32': return 'Scheduled Task';
|
|
157
|
+
default: return 'crontab';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ---- Status ----
|
|
161
|
+
function showStatus(codeburnPath) {
|
|
162
|
+
console.log('\n📊 OTEL Sync Status\n');
|
|
163
|
+
// Codeburn installed?
|
|
164
|
+
if (codeburnPath) {
|
|
165
|
+
console.log(` ✓ codeburn found: ${codeburnPath}`);
|
|
166
|
+
const ver = run(`${codeburnPath} --version`);
|
|
167
|
+
if (ver.ok)
|
|
168
|
+
console.log(` version: ${ver.stdout}`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log(' ✗ codeburn not found in PATH');
|
|
172
|
+
}
|
|
173
|
+
// Sync configured?
|
|
174
|
+
const configPath = join(homedir(), '.config', 'codeburn', 'sync.json');
|
|
175
|
+
if (existsSync(configPath)) {
|
|
176
|
+
try {
|
|
177
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
178
|
+
console.log(` ✓ sync configured: ${cfg.baseUrl || cfg.endpoint || cfg.url || '(configured)'}`);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
console.log(` ✓ sync config exists: ${configPath}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.log(' ✗ sync not configured (run setup first)');
|
|
186
|
+
}
|
|
187
|
+
// Schedule exists?
|
|
188
|
+
if (scheduleExists()) {
|
|
189
|
+
console.log(` ✓ ${schedulerName()} schedule active`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(` ✗ no ${schedulerName()} schedule found`);
|
|
193
|
+
}
|
|
194
|
+
// Last log?
|
|
195
|
+
const logFile = join(LOG_DIR, 'otel-sync.log');
|
|
196
|
+
if (existsSync(logFile)) {
|
|
197
|
+
const lastLines = run(`tail -5 "${logFile}"`);
|
|
198
|
+
if (lastLines.ok && lastLines.stdout) {
|
|
199
|
+
console.log(` Last sync output (${logFile}):`);
|
|
200
|
+
for (const line of lastLines.stdout.split('\n')) {
|
|
201
|
+
console.log(` ${line}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
console.log('');
|
|
206
|
+
}
|
|
207
|
+
// ---- Main ----
|
|
208
|
+
export default {
|
|
209
|
+
description: 'Set up automated codeburn OTEL sync (pushes AI usage telemetry every 12h)',
|
|
210
|
+
options: [
|
|
211
|
+
{ flags: '--url <url>', description: 'OTEL collector URL (from CDK deploy output OtelCollectorUrl)' },
|
|
212
|
+
{ flags: '--interval <hours>', description: 'Sync interval in hours (default: 12)' },
|
|
213
|
+
{ flags: '--status', description: 'Show current sync schedule status' },
|
|
214
|
+
{ flags: '--remove', description: 'Remove the sync schedule' },
|
|
215
|
+
],
|
|
216
|
+
async action(opts) {
|
|
217
|
+
const codeburnPath = getCodeburnPath();
|
|
218
|
+
// --status
|
|
219
|
+
if (opts.status) {
|
|
220
|
+
showStatus(codeburnPath);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// --remove
|
|
224
|
+
if (opts.remove) {
|
|
225
|
+
if (scheduleExists()) {
|
|
226
|
+
removeSchedule();
|
|
227
|
+
console.log(`\n ✓ ${schedulerName()} schedule removed.\n`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(`\n No ${schedulerName()} schedule found — nothing to remove.\n`);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// ---- Setup flow ----
|
|
235
|
+
console.log('\n🔄 OTEL Sync Setup\n');
|
|
236
|
+
console.log('This will configure automated codeburn telemetry sync to your PRISM');
|
|
237
|
+
console.log('OTEL collector. Runs every 12 hours to keep dashboards fresh.\n');
|
|
238
|
+
// 1. Check codeburn version
|
|
239
|
+
if (!codeburnPath) {
|
|
240
|
+
console.error('Error: codeburn not found. Install with: npm install -g codeburn');
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const verResult = run(`${codeburnPath} --version`);
|
|
244
|
+
if (verResult.ok) {
|
|
245
|
+
const ver = verResult.stdout.replace(/[^0-9.]/g, '');
|
|
246
|
+
const parts = ver.split('.').map(Number);
|
|
247
|
+
if (parts[0] === 0 && parts[1] < 9 || (parts[1] === 9 && parts[2] < 16)) {
|
|
248
|
+
console.error(`Error: codeburn >= 0.9.16 required (found ${ver}). Run: npm update -g codeburn`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
console.log(` ✓ codeburn ${ver}`);
|
|
252
|
+
}
|
|
253
|
+
// 2. Get the OTEL collector URL
|
|
254
|
+
const otelUrl = opts.url || await prompt('OTEL collector URL (from CDK deploy output OtelCollectorUrl)');
|
|
255
|
+
if (!otelUrl) {
|
|
256
|
+
console.error('Error: OTEL collector URL is required.');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
// 3. Check if codeburn sync is already configured for this URL
|
|
260
|
+
const configPath = join(homedir(), '.config', 'codeburn', 'sync.json');
|
|
261
|
+
let needsSetup = true;
|
|
262
|
+
if (existsSync(configPath)) {
|
|
263
|
+
try {
|
|
264
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
265
|
+
const existingUrl = cfg.baseUrl || cfg.endpoint || cfg.url || '';
|
|
266
|
+
if (existingUrl === otelUrl || existingUrl.startsWith(otelUrl)) {
|
|
267
|
+
console.log(` ✓ codeburn sync already configured for this endpoint.`);
|
|
268
|
+
needsSetup = false;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
console.log(` ⚠ codeburn sync configured for different endpoint: ${existingUrl}`);
|
|
272
|
+
const override = await prompt('Reconfigure for the new URL? (y/N)', 'N');
|
|
273
|
+
if (override.toLowerCase() !== 'y') {
|
|
274
|
+
console.log(' Keeping existing configuration.');
|
|
275
|
+
needsSetup = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Corrupt config — re-setup
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 4. Run codeburn sync setup (opens browser for OIDC)
|
|
284
|
+
if (needsSetup) {
|
|
285
|
+
console.log('\n Running: codeburn sync setup ' + otelUrl);
|
|
286
|
+
console.log(' (This will open a browser for OIDC authentication)\n');
|
|
287
|
+
try {
|
|
288
|
+
execSync(`${codeburnPath} sync setup ${otelUrl}`, { stdio: 'inherit' });
|
|
289
|
+
console.log('\n ✓ codeburn sync configured.');
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
console.error(`\n ✗ codeburn sync setup failed. Ensure Cognito user exists and try again.`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// 5. Initial backfill push (30d — server drops >14d from CloudWatch, keeps in DDB/S3)
|
|
297
|
+
console.log('\n Pushing initial backfill (last 30 days of telemetry)...');
|
|
298
|
+
const backfill = run(`${codeburnPath} sync push --since 30d`);
|
|
299
|
+
if (backfill.ok) {
|
|
300
|
+
console.log(' ✓ Backfill push complete.');
|
|
301
|
+
if (backfill.stdout)
|
|
302
|
+
console.log(` ${backfill.stdout.split('\n').slice(-1)[0]}`);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.log(` ⚠ Backfill push failed (non-fatal): ${backfill.stderr.split('\n')[0]}`);
|
|
306
|
+
console.log(' The schedule will retry. Check ~/.prism/logs/otel-sync.log');
|
|
307
|
+
}
|
|
308
|
+
// 6. Install OS schedule
|
|
309
|
+
const intervalHours = opts.interval ? parseInt(opts.interval, 10) : 12;
|
|
310
|
+
if (isNaN(intervalHours) || intervalHours < 1) {
|
|
311
|
+
console.error('Error: --interval must be a positive integer (hours).');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
if (scheduleExists()) {
|
|
315
|
+
console.log(`\n ✓ ${schedulerName()} schedule already exists — updating.`);
|
|
316
|
+
removeSchedule();
|
|
317
|
+
}
|
|
318
|
+
installSchedule(codeburnPath, intervalHours);
|
|
319
|
+
console.log(` ✓ ${schedulerName()} schedule installed (every ${intervalHours}h).`);
|
|
320
|
+
// Summary
|
|
321
|
+
console.log('\n════════════════════════════════════════════════');
|
|
322
|
+
console.log(' ✅ OTEL sync setup complete!');
|
|
323
|
+
console.log('════════════════════════════════════════════════');
|
|
324
|
+
console.log(`\n Endpoint: ${otelUrl}`);
|
|
325
|
+
console.log(` Schedule: every ${intervalHours}h via ${schedulerName()}`);
|
|
326
|
+
console.log(` Backfill: last 30d pushed (dashboard shows last 14d)`);
|
|
327
|
+
console.log(` Logs: ${join(LOG_DIR, 'otel-sync.log')}`);
|
|
328
|
+
console.log(`\n Run \`prism-cli bootstrapper setup-otel-sync --status\` to check health.`);
|
|
329
|
+
console.log(` Run \`prism-cli bootstrapper setup-otel-sync --remove\` to uninstall.\n`);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
//# sourceMappingURL=setup-otel-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-otel-sync.js","sourceRoot":"","sources":["../../../../src/commands/bootstrapper/setup-otel-sync.ts"],"names":[],"mappings":"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"}
|