@prism-d1/cli 1.0.62 → 1.0.65
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 +302 -0
- package/dist/assets/infra/lib/constructs/prism-vpc-construct.ts +5 -0
- package/dist/assets/infra/lib/lambda/metrics-processor.ts +1 -0
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +369 -0
- package/dist/assets/infra/lib/metrics-pipeline-stack.ts +55 -1
- package/dist/src/commands/git/commit-trailers.d.ts.map +1 -1
- package/dist/src/commands/git/commit-trailers.js +21 -22
- package/dist/src/commands/git/commit-trailers.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRISM D1 — OTEL Collector construct (opt-in).
|
|
3
|
+
*
|
|
4
|
+
* Server side of `codeburn sync`: an HTTP API serving the codeburn discovery
|
|
5
|
+
* doc and an OTLP/HTTP traces endpoint, authorized by OIDC JWT.
|
|
6
|
+
*
|
|
7
|
+
* Enable with: cdk deploy -c enableOtelCollector=true
|
|
8
|
+
* BYO IdP with: -c otelIssuer=... -c otelClientId=... [-c otelIdentityClaim=email]
|
|
9
|
+
*
|
|
10
|
+
* Default mode provisions a Cognito User Pool (admin-create-user only,
|
|
11
|
+
* username = email, PKCE public client) with loopback callback URLs matching
|
|
12
|
+
* codeburn's fixed callback ports (19876-19878). BYO mode skips Cognito and
|
|
13
|
+
* wires the JWT authorizer to the external issuer.
|
|
14
|
+
*
|
|
15
|
+
* Data flow: codeburn sync push → API Gateway (JWT authorizer) → receiver
|
|
16
|
+
* Lambda → S3 raw OTLP archive (external contract) + DynamoDB span/aggregate
|
|
17
|
+
* rows (PRISM dashboards).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as cdk from 'aws-cdk-lib';
|
|
21
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
22
|
+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
23
|
+
import * as cognito from 'aws-cdk-lib/aws-cognito';
|
|
24
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
25
|
+
import * as kms from 'aws-cdk-lib/aws-kms';
|
|
26
|
+
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
27
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
28
|
+
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
29
|
+
import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
|
|
30
|
+
import * as path from 'path';
|
|
31
|
+
import { Construct } from 'constructs';
|
|
32
|
+
import { NagSuppressions } from 'cdk-nag';
|
|
33
|
+
|
|
34
|
+
/** codeburn sync fixed loopback callback ports (see codeburn src/sync/auth.ts). */
|
|
35
|
+
const CODEBURN_CALLBACK_PORTS = [19876, 19877, 19878];
|
|
36
|
+
|
|
37
|
+
export interface OtelCollectorConstructProps {
|
|
38
|
+
/** The prism-d1-ai-usage table — receiver writes SPAN# and OTEL#DAY# items. */
|
|
39
|
+
aiUsageTable: dynamodb.Table;
|
|
40
|
+
/** KMS key for the S3 archive bucket. */
|
|
41
|
+
kmsKey: kms.IKey;
|
|
42
|
+
/** VPC props spread applied to the receiver Lambda (matches stack pattern). */
|
|
43
|
+
lambdaVpcProps?: { vpc?: cdk.aws_ec2.IVpc; securityGroups?: cdk.aws_ec2.ISecurityGroup[] };
|
|
44
|
+
/** BYO IdP: OIDC issuer URL. When set, Cognito is NOT provisioned. */
|
|
45
|
+
externalIssuer?: string;
|
|
46
|
+
/** BYO IdP: OAuth client ID registered for codeburn (public client + PKCE). */
|
|
47
|
+
externalClientId?: string;
|
|
48
|
+
/** JWT claim used as the user identity key (default: username for Cognito, sub for BYO). */
|
|
49
|
+
identityClaim?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class OtelCollectorConstruct extends Construct {
|
|
53
|
+
public readonly httpApi: apigwv2.HttpApi;
|
|
54
|
+
public readonly archiveBucket: s3.Bucket;
|
|
55
|
+
public readonly userPool?: cognito.UserPool;
|
|
56
|
+
public readonly userPoolClient?: cognito.UserPoolClient;
|
|
57
|
+
|
|
58
|
+
constructor(scope: Construct, id: string, props: OtelCollectorConstructProps) {
|
|
59
|
+
super(scope, id);
|
|
60
|
+
|
|
61
|
+
const byoIdp = !!props.externalIssuer;
|
|
62
|
+
if (byoIdp && !props.externalClientId) {
|
|
63
|
+
throw new Error('otelClientId is required when otelIssuer is set');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// -------------------------------------------------------
|
|
67
|
+
// OIDC provider — Cognito (default) or BYO issuer
|
|
68
|
+
// -------------------------------------------------------
|
|
69
|
+
let issuer: string;
|
|
70
|
+
let clientId: string;
|
|
71
|
+
let identityClaim: string;
|
|
72
|
+
|
|
73
|
+
if (byoIdp) {
|
|
74
|
+
issuer = props.externalIssuer!.replace(/\/$/, '');
|
|
75
|
+
clientId = props.externalClientId!;
|
|
76
|
+
identityClaim = props.identityClaim ?? 'sub';
|
|
77
|
+
} else {
|
|
78
|
+
// Admin-create-user only (no self-signup); username = email so DDB keys
|
|
79
|
+
// line up with the USER#<email> key convention. We do NOT use
|
|
80
|
+
// signInAliases: { email: true } because that generates a random UUID
|
|
81
|
+
// as the internal username — the access token's `username` claim would
|
|
82
|
+
// carry the UUID, not the email. Instead, admins create users with
|
|
83
|
+
// --username dev@example.com and the `username` claim = the email.
|
|
84
|
+
this.userPool = new cognito.UserPool(this, 'UserPool', {
|
|
85
|
+
userPoolName: 'prism-d1-otel-users',
|
|
86
|
+
selfSignUpEnabled: false,
|
|
87
|
+
passwordPolicy: {
|
|
88
|
+
minLength: 12,
|
|
89
|
+
requireLowercase: true,
|
|
90
|
+
requireUppercase: true,
|
|
91
|
+
requireDigits: true,
|
|
92
|
+
requireSymbols: true,
|
|
93
|
+
},
|
|
94
|
+
advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
|
|
95
|
+
featurePlan: cognito.FeaturePlan.PLUS,
|
|
96
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Hosted UI domain — prefix must be globally unique per region.
|
|
100
|
+
this.userPool.addDomain('Domain', {
|
|
101
|
+
cognitoDomain: { domainPrefix: `prism-d1-otel-${cdk.Aws.ACCOUNT_ID}` },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Public client (no secret) — codeburn does Authorization Code + PKCE
|
|
105
|
+
// with loopback redirects. Cognito allows http:// for localhost, 127.0.0.1,
|
|
106
|
+
// and [::1] (per CreateUserPoolClient API docs). codeburn uses 127.0.0.1.
|
|
107
|
+
const callbackUrls = CODEBURN_CALLBACK_PORTS.map(
|
|
108
|
+
(port) => `http://127.0.0.1:${port}/callback`,
|
|
109
|
+
);
|
|
110
|
+
this.userPoolClient = this.userPool.addClient('CodeburnClient', {
|
|
111
|
+
userPoolClientName: 'codeburn-sync',
|
|
112
|
+
generateSecret: false,
|
|
113
|
+
authFlows: { userSrp: true },
|
|
114
|
+
oAuth: {
|
|
115
|
+
flows: { authorizationCodeGrant: true },
|
|
116
|
+
scopes: [cognito.OAuthScope.OPENID, cognito.OAuthScope.EMAIL, cognito.OAuthScope.PROFILE],
|
|
117
|
+
callbackUrls,
|
|
118
|
+
},
|
|
119
|
+
preventUserExistenceErrors: true,
|
|
120
|
+
accessTokenValidity: cdk.Duration.hours(1),
|
|
121
|
+
refreshTokenValidity: cdk.Duration.days(30),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
issuer = `https://cognito-idp.${cdk.Aws.REGION}.amazonaws.com/${this.userPool.userPoolId}`;
|
|
125
|
+
clientId = this.userPoolClient.userPoolClientId;
|
|
126
|
+
// Cognito ACCESS tokens carry `username` (no email claim) — with
|
|
127
|
+
// email-alias sign-in and admin-created users, username == email.
|
|
128
|
+
identityClaim = props.identityClaim ?? 'username';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// -------------------------------------------------------
|
|
132
|
+
// S3 archive — raw OTLP batches, the external consumption contract
|
|
133
|
+
// -------------------------------------------------------
|
|
134
|
+
this.archiveBucket = new s3.Bucket(this, 'ArchiveBucket', {
|
|
135
|
+
encryption: s3.BucketEncryption.KMS,
|
|
136
|
+
encryptionKey: props.kmsKey,
|
|
137
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
138
|
+
enforceSSL: true,
|
|
139
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
140
|
+
lifecycleRules: [{
|
|
141
|
+
id: 'expire-raw-otlp',
|
|
142
|
+
expiration: cdk.Duration.days(365),
|
|
143
|
+
}],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// -------------------------------------------------------
|
|
147
|
+
// Receiver Lambda — discovery doc + OTLP traces ingestion
|
|
148
|
+
// -------------------------------------------------------
|
|
149
|
+
const receiver = new lambda.Function(this, 'Receiver', {
|
|
150
|
+
functionName: 'prism-d1-otel-receiver',
|
|
151
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
152
|
+
handler: 'otel-receiver.handler',
|
|
153
|
+
...(props.lambdaVpcProps ?? {}),
|
|
154
|
+
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambda'), {
|
|
155
|
+
bundling: {
|
|
156
|
+
image: lambda.Runtime.NODEJS_22_X.bundlingImage,
|
|
157
|
+
command: [
|
|
158
|
+
'bash', '-c',
|
|
159
|
+
[
|
|
160
|
+
'npm init -y > /dev/null 2>&1',
|
|
161
|
+
'npm install --save @aws-sdk/client-dynamodb @aws-sdk/client-s3 esbuild > /dev/null 2>&1',
|
|
162
|
+
'npx esbuild otel-receiver.ts --bundle --platform=node --target=node22 --outfile=/asset-output/otel-receiver.js --external:@aws-sdk/*',
|
|
163
|
+
].join(' && '),
|
|
164
|
+
],
|
|
165
|
+
local: {
|
|
166
|
+
tryBundle(outputDir: string): boolean {
|
|
167
|
+
try {
|
|
168
|
+
const { execSync } = require('child_process');
|
|
169
|
+
execSync(
|
|
170
|
+
`npx esbuild ${path.join(__dirname, '..', 'lambda', 'otel-receiver.ts')} --bundle --platform=node --target=node22 --outfile=${path.join(outputDir, 'otel-receiver.js')} --external:@aws-sdk/*`,
|
|
171
|
+
{ stdio: 'pipe' },
|
|
172
|
+
);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
timeout: cdk.Duration.seconds(60),
|
|
182
|
+
memorySize: 512,
|
|
183
|
+
environment: {
|
|
184
|
+
AI_USAGE_TABLE: props.aiUsageTable.tableName,
|
|
185
|
+
ARCHIVE_BUCKET: this.archiveBucket.bucketName,
|
|
186
|
+
IDENTITY_CLAIM: identityClaim,
|
|
187
|
+
OIDC_ISSUER: issuer,
|
|
188
|
+
OIDC_CLIENT_ID: clientId,
|
|
189
|
+
MAX_BATCH_SIZE: '1000',
|
|
190
|
+
SPAN_TTL_DAYS: '90',
|
|
191
|
+
},
|
|
192
|
+
logRetention: logs.RetentionDays.ONE_MONTH,
|
|
193
|
+
description: 'Receives codeburn sync OTLP traces: S3 archive + per-user span/daily-aggregate rows',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
props.aiUsageTable.grantReadWriteData(receiver);
|
|
197
|
+
this.archiveBucket.grantPut(receiver);
|
|
198
|
+
props.kmsKey.grantEncryptDecrypt(receiver);
|
|
199
|
+
|
|
200
|
+
// -------------------------------------------------------
|
|
201
|
+
// HTTP API — discovery (public) + traces (JWT)
|
|
202
|
+
// -------------------------------------------------------
|
|
203
|
+
this.httpApi = new apigwv2.HttpApi(this, 'Api', {
|
|
204
|
+
apiName: 'prism-d1-otel-collector',
|
|
205
|
+
description: 'PRISM D1 OTEL collector for codeburn sync',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const integration = new HttpLambdaIntegration('ReceiverIntegration', receiver);
|
|
209
|
+
|
|
210
|
+
// HTTP API JWT authorizers validate the `aud` claim, or `client_id` for
|
|
211
|
+
// Cognito access tokens — both paths work with jwtAudience = [clientId].
|
|
212
|
+
const authorizer = new HttpJwtAuthorizer('OtelJwtAuthorizer', issuer, {
|
|
213
|
+
jwtAudience: [clientId],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Discovery doc — unauthenticated by design (it only exposes issuer +
|
|
217
|
+
// client_id, both public values in an OAuth public-client flow).
|
|
218
|
+
this.httpApi.addRoutes({
|
|
219
|
+
path: '/.well-known/codeburn-export.json',
|
|
220
|
+
methods: [apigwv2.HttpMethod.GET],
|
|
221
|
+
integration,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
this.httpApi.addRoutes({
|
|
225
|
+
path: '/v1/traces',
|
|
226
|
+
methods: [apigwv2.HttpMethod.POST],
|
|
227
|
+
integration,
|
|
228
|
+
authorizer,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Access logging on the default stage (AwsSolutions-APIG1).
|
|
232
|
+
const accessLogGroup = new logs.LogGroup(this, 'ApiAccessLogs', {
|
|
233
|
+
logGroupName: '/aws/apigateway/prism-d1-otel-collector',
|
|
234
|
+
retention: logs.RetentionDays.ONE_MONTH,
|
|
235
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
236
|
+
});
|
|
237
|
+
const defaultStage = this.httpApi.defaultStage!.node.defaultChild as apigwv2.CfnStage;
|
|
238
|
+
defaultStage.accessLogSettings = {
|
|
239
|
+
destinationArn: accessLogGroup.logGroupArn,
|
|
240
|
+
format: JSON.stringify({
|
|
241
|
+
requestId: '$context.requestId',
|
|
242
|
+
ip: '$context.identity.sourceIp',
|
|
243
|
+
requestTime: '$context.requestTime',
|
|
244
|
+
httpMethod: '$context.httpMethod',
|
|
245
|
+
path: '$context.path',
|
|
246
|
+
status: '$context.status',
|
|
247
|
+
responseLength: '$context.responseLength',
|
|
248
|
+
userAgent: '$context.identity.userAgent',
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// -------------------------------------------------------
|
|
253
|
+
// cdk-nag suppressions (with evidence)
|
|
254
|
+
// -------------------------------------------------------
|
|
255
|
+
NagSuppressions.addResourceSuppressions(receiver, [
|
|
256
|
+
{
|
|
257
|
+
id: 'AwsSolutions-IAM5',
|
|
258
|
+
reason: 'grantPut generates s3:Abort* + bucket/* wildcard scoped to the single OTLP archive bucket',
|
|
259
|
+
appliesTo: ['Action::s3:Abort*', { regex: '/^Resource::<OtelCollectorArchiveBucket.*\\.Arn>\\/\\*$/g' }],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'AwsSolutions-IAM5',
|
|
263
|
+
reason: 'grantReadWriteData generates a wildcard for the AI-usage table GSI index ARNs',
|
|
264
|
+
appliesTo: [{ regex: '/^Resource::<AiUsageTable.*\\.Arn>\\/index\\/\\*$/g' }],
|
|
265
|
+
},
|
|
266
|
+
], true);
|
|
267
|
+
NagSuppressions.addResourceSuppressions(this.httpApi, [
|
|
268
|
+
{
|
|
269
|
+
id: 'AwsSolutions-APIG4',
|
|
270
|
+
reason: 'The codeburn discovery doc route is unauthenticated by design — it only publishes the OIDC issuer and public client_id (both non-secret in an OAuth public-client PKCE flow). The traces route is JWT-authorized.',
|
|
271
|
+
},
|
|
272
|
+
], true);
|
|
273
|
+
NagSuppressions.addResourceSuppressions(this.archiveBucket, [
|
|
274
|
+
{
|
|
275
|
+
id: 'AwsSolutions-S1',
|
|
276
|
+
reason: 'Telemetry archive bucket for a sample project; access is limited to the receiver Lambda (put) and account principals. Enable S3 server access logging or CloudTrail data events for production hardening.',
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
if (this.userPool) {
|
|
280
|
+
NagSuppressions.addResourceSuppressions(this.userPool, [
|
|
281
|
+
{
|
|
282
|
+
id: 'AwsSolutions-COG2',
|
|
283
|
+
reason: 'MFA is not required for the codeburn telemetry-push user pool: users are admin-created, the client is scoped to telemetry writes only, and MFA on a CLI OIDC device flow adds friction disproportionate to the data sensitivity. Enable MFA for production hardening.',
|
|
284
|
+
},
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// -------------------------------------------------------
|
|
289
|
+
// Outputs — everything needed to run `codeburn sync setup <url>`
|
|
290
|
+
// -------------------------------------------------------
|
|
291
|
+
new cdk.CfnOutput(this, 'OtelCollectorUrl', {
|
|
292
|
+
value: this.httpApi.apiEndpoint,
|
|
293
|
+
description: 'Run: codeburn sync setup <this URL>',
|
|
294
|
+
});
|
|
295
|
+
if (this.userPool) {
|
|
296
|
+
new cdk.CfnOutput(this, 'OtelUserPoolId', {
|
|
297
|
+
value: this.userPool.userPoolId,
|
|
298
|
+
description: 'Create users: aws cognito-idp admin-create-user --user-pool-id <this> --username <email>',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -46,6 +46,11 @@ export class PrismVpcConstruct extends Construct {
|
|
|
46
46
|
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// --- Gateway endpoint: S3 (free; used by the OTEL receiver archive writes) ---
|
|
50
|
+
this.vpc.addGatewayEndpoint('S3Endpoint', {
|
|
51
|
+
service: ec2.GatewayVpcEndpointAwsService.S3,
|
|
52
|
+
});
|
|
53
|
+
|
|
49
54
|
// --- Interface endpoints ---
|
|
50
55
|
const interfaceEndpoints = [
|
|
51
56
|
{ id: 'EventBridgeEndpoint', service: ec2.InterfaceVpcEndpointAwsService.EVENTBRIDGE },
|
|
@@ -151,6 +151,7 @@ const EVENTS_TABLE = process.env.EVENTS_TABLE!;
|
|
|
151
151
|
const METADATA_TABLE = process.env.METADATA_TABLE!;
|
|
152
152
|
const METRIC_NAMESPACE = process.env.METRIC_NAMESPACE ?? 'PRISM/D1/Velocity';
|
|
153
153
|
|
|
154
|
+
|
|
154
155
|
// ---- Handler ----
|
|
155
156
|
|
|
156
157
|
export async function handler(event: EventBridgeEvent): Promise<void> {
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRISM D1 — OTLP receiver for codeburn sync.
|
|
3
|
+
*
|
|
4
|
+
* Serves two routes on the OTEL collector HTTP API:
|
|
5
|
+
* GET /.well-known/codeburn-export.json — discovery doc (no auth)
|
|
6
|
+
* POST /v1/traces — OTLP/HTTP JSON traces (JWT-authorized)
|
|
7
|
+
*
|
|
8
|
+
* Trace flow: validate (JWT done by API Gateway) → archive raw OTLP batch to S3
|
|
9
|
+
* (external contract — standard OTLP JSON, Athena/replay-friendly) → per-span
|
|
10
|
+
* conditional DynamoDB write (dedup gate) → daily aggregate ADD (only for
|
|
11
|
+
* newly-seen spans, bucketed by SPAN date, not arrival date).
|
|
12
|
+
*
|
|
13
|
+
* Item shapes in the AI-usage table:
|
|
14
|
+
* pk=USER#<identity> sk=SPAN#<timestamp>#<spanId> raw span, TTL 90d
|
|
15
|
+
* pk=USER#<identity> sk=OTEL#DAY#<yyyy-mm-dd>#<tool> daily aggregate
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DynamoDBClient,
|
|
20
|
+
PutItemCommand,
|
|
21
|
+
UpdateItemCommand,
|
|
22
|
+
ConditionalCheckFailedException,
|
|
23
|
+
} from '@aws-sdk/client-dynamodb';
|
|
24
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
25
|
+
import { randomUUID } from 'crypto';
|
|
26
|
+
|
|
27
|
+
const dynamoClient = new DynamoDBClient({});
|
|
28
|
+
const s3Client = new S3Client({});
|
|
29
|
+
|
|
30
|
+
const AI_USAGE_TABLE = process.env.AI_USAGE_TABLE || 'prism-d1-ai-usage';
|
|
31
|
+
const ARCHIVE_BUCKET = process.env.ARCHIVE_BUCKET || '';
|
|
32
|
+
const IDENTITY_CLAIM = process.env.IDENTITY_CLAIM || 'username';
|
|
33
|
+
const OIDC_ISSUER = process.env.OIDC_ISSUER || '';
|
|
34
|
+
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || '';
|
|
35
|
+
const MAX_BATCH_SIZE = Number(process.env.MAX_BATCH_SIZE || '1000');
|
|
36
|
+
const SPAN_TTL_DAYS = Number(process.env.SPAN_TTL_DAYS || '90');
|
|
37
|
+
|
|
38
|
+
/** codeburn provider names → PRISM tool names. */
|
|
39
|
+
const PROVIDER_TO_TOOL: Record<string, string> = {
|
|
40
|
+
claude: 'claude-code',
|
|
41
|
+
kiro: 'kiro',
|
|
42
|
+
cursor: 'cursor',
|
|
43
|
+
codex: 'codex',
|
|
44
|
+
copilot: 'copilot',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---- Types (API Gateway HTTP API v2 proxy + OTLP JSON) ----
|
|
48
|
+
|
|
49
|
+
interface HttpApiEvent {
|
|
50
|
+
rawPath: string;
|
|
51
|
+
requestContext: {
|
|
52
|
+
http: { method: string; path: string };
|
|
53
|
+
authorizer?: { jwt?: { claims?: Record<string, unknown> } };
|
|
54
|
+
};
|
|
55
|
+
body?: string;
|
|
56
|
+
isBase64Encoded?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface HttpApiResponse {
|
|
60
|
+
statusCode: number;
|
|
61
|
+
headers: Record<string, string>;
|
|
62
|
+
body: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type OtlpValue =
|
|
66
|
+
| { stringValue?: string; intValue?: string | number; doubleValue?: number; boolValue?: boolean };
|
|
67
|
+
|
|
68
|
+
interface OtlpAttribute {
|
|
69
|
+
key: string;
|
|
70
|
+
value: OtlpValue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface OtlpSpan {
|
|
74
|
+
traceId?: string;
|
|
75
|
+
spanId?: string;
|
|
76
|
+
name?: string;
|
|
77
|
+
startTimeUnixNano?: string;
|
|
78
|
+
attributes?: OtlpAttribute[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface OtlpPayload {
|
|
82
|
+
resourceSpans?: Array<{
|
|
83
|
+
resource?: { attributes?: OtlpAttribute[] };
|
|
84
|
+
scopeSpans?: Array<{ spans?: OtlpSpan[] }>;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ParsedSpan {
|
|
89
|
+
spanId: string;
|
|
90
|
+
timestamp: string; // ISO from span start time
|
|
91
|
+
tool: string;
|
|
92
|
+
model: string;
|
|
93
|
+
inputTokens: number;
|
|
94
|
+
outputTokens: number;
|
|
95
|
+
costUsd: number;
|
|
96
|
+
project: string;
|
|
97
|
+
costEstimated: boolean;
|
|
98
|
+
deviceId: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- Helpers ----
|
|
102
|
+
|
|
103
|
+
function jsonResponse(statusCode: number, body: unknown): HttpApiResponse {
|
|
104
|
+
return {
|
|
105
|
+
statusCode,
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify(body),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function attrMap(attrs: OtlpAttribute[] | undefined): Map<string, OtlpValue> {
|
|
112
|
+
const m = new Map<string, OtlpValue>();
|
|
113
|
+
for (const a of attrs ?? []) {
|
|
114
|
+
if (a && typeof a.key === 'string' && a.value) m.set(a.key, a.value);
|
|
115
|
+
}
|
|
116
|
+
return m;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function str(v: OtlpValue | undefined): string {
|
|
120
|
+
return typeof v?.stringValue === 'string' ? v.stringValue : '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function num(v: OtlpValue | undefined): number {
|
|
124
|
+
if (v?.intValue !== undefined) {
|
|
125
|
+
const n = Number(v.intValue);
|
|
126
|
+
return Number.isFinite(n) ? n : 0;
|
|
127
|
+
}
|
|
128
|
+
if (typeof v?.doubleValue === 'number' && Number.isFinite(v.doubleValue)) return v.doubleValue;
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function bool(v: OtlpValue | undefined): boolean {
|
|
133
|
+
return v?.boolValue === true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Convert OTLP unix-nano string to ISO timestamp; empty string if invalid. */
|
|
137
|
+
function nanoToIso(nano: string | undefined): string {
|
|
138
|
+
if (!nano || !/^\d+$/.test(nano)) return '';
|
|
139
|
+
try {
|
|
140
|
+
const ms = Number(BigInt(nano) / 1_000_000n);
|
|
141
|
+
if (!Number.isFinite(ms) || ms <= 0) return '';
|
|
142
|
+
const d = new Date(ms);
|
|
143
|
+
return isNaN(d.getTime()) ? '' : d.toISOString();
|
|
144
|
+
} catch {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Resolve caller identity from JWT claims (validated by the API Gateway authorizer). */
|
|
150
|
+
function resolveIdentity(event: HttpApiEvent): string | null {
|
|
151
|
+
const claims = event.requestContext.authorizer?.jwt?.claims;
|
|
152
|
+
if (!claims) return null;
|
|
153
|
+
for (const claim of [IDENTITY_CLAIM, 'username', 'email', 'sub']) {
|
|
154
|
+
const v = claims[claim];
|
|
155
|
+
if (typeof v === 'string' && v.trim()) return v.trim().toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Extract and sanity-check spans from an OTLP payload. Returns spans + rejected count. */
|
|
161
|
+
export function parseOtlpSpans(payload: OtlpPayload): { spans: ParsedSpan[]; rejected: number } {
|
|
162
|
+
const spans: ParsedSpan[] = [];
|
|
163
|
+
let rejected = 0;
|
|
164
|
+
|
|
165
|
+
for (const rs of payload.resourceSpans ?? []) {
|
|
166
|
+
const resourceAttrs = attrMap(rs.resource?.attributes);
|
|
167
|
+
const deviceId = str(resourceAttrs.get('codeburn.device_id')).slice(0, 64);
|
|
168
|
+
|
|
169
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
170
|
+
for (const span of ss.spans ?? []) {
|
|
171
|
+
const attrs = attrMap(span.attributes);
|
|
172
|
+
const spanId = typeof span.spanId === 'string' ? span.spanId : '';
|
|
173
|
+
const timestamp = nanoToIso(span.startTimeUnixNano);
|
|
174
|
+
const provider = str(attrs.get('ai.provider'));
|
|
175
|
+
|
|
176
|
+
// Minimum viable span: identity key parts must be present and sane.
|
|
177
|
+
if (!/^[0-9a-f]{16}$/i.test(spanId) || !timestamp || !provider) {
|
|
178
|
+
rejected++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const inputTokens = num(attrs.get('ai.input_tokens'));
|
|
183
|
+
const outputTokens = num(attrs.get('ai.output_tokens'));
|
|
184
|
+
const costUsd = num(attrs.get('ai.cost_usd'));
|
|
185
|
+
// Clamp: reject absurd values (mirrors the CI trailer clamps).
|
|
186
|
+
if (
|
|
187
|
+
inputTokens < 0 || outputTokens < 0 || costUsd < 0 ||
|
|
188
|
+
inputTokens > 100_000_000 || outputTokens > 100_000_000 || costUsd > 10_000
|
|
189
|
+
) {
|
|
190
|
+
rejected++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
spans.push({
|
|
195
|
+
spanId: spanId.toLowerCase(),
|
|
196
|
+
timestamp,
|
|
197
|
+
tool: PROVIDER_TO_TOOL[provider] ?? provider.slice(0, 32),
|
|
198
|
+
model: str(attrs.get('ai.model')).slice(0, 128),
|
|
199
|
+
inputTokens,
|
|
200
|
+
outputTokens,
|
|
201
|
+
costUsd,
|
|
202
|
+
project: str(attrs.get('ai.project')).slice(0, 256),
|
|
203
|
+
costEstimated: bool(attrs.get('ai.cost_estimated')),
|
|
204
|
+
deviceId,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { spans, rejected };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- Route: discovery doc ----
|
|
214
|
+
|
|
215
|
+
function handleDiscovery(): HttpApiResponse {
|
|
216
|
+
return jsonResponse(200, {
|
|
217
|
+
version: 1,
|
|
218
|
+
issuer: OIDC_ISSUER,
|
|
219
|
+
client_id: OIDC_CLIENT_ID,
|
|
220
|
+
scopes: ['openid', 'email', 'profile'],
|
|
221
|
+
traces_path: '/v1/traces',
|
|
222
|
+
max_batch_size: MAX_BATCH_SIZE,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- Route: traces ----
|
|
227
|
+
|
|
228
|
+
async function archiveToS3(rawBody: string): Promise<void> {
|
|
229
|
+
if (!ARCHIVE_BUCKET) return;
|
|
230
|
+
const now = new Date();
|
|
231
|
+
const dt = now.toISOString().slice(0, 10);
|
|
232
|
+
const key = `otlp/dt=${dt}/${now.toISOString().replace(/[:.]/g, '-')}-${randomUUID()}.json`;
|
|
233
|
+
await s3Client.send(new PutObjectCommand({
|
|
234
|
+
Bucket: ARCHIVE_BUCKET,
|
|
235
|
+
Key: key,
|
|
236
|
+
Body: rawBody,
|
|
237
|
+
ContentType: 'application/json',
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Conditional raw-span write. Returns true if the span is NEW (write succeeded). */
|
|
242
|
+
async function writeSpanIfNew(user: string, s: ParsedSpan): Promise<boolean> {
|
|
243
|
+
const ttl = Math.floor(Date.now() / 1000) + SPAN_TTL_DAYS * 24 * 60 * 60;
|
|
244
|
+
try {
|
|
245
|
+
await dynamoClient.send(new PutItemCommand({
|
|
246
|
+
TableName: AI_USAGE_TABLE,
|
|
247
|
+
Item: {
|
|
248
|
+
pk: { S: `USER#${user}` },
|
|
249
|
+
sk: { S: `SPAN#${s.timestamp}#${s.spanId}` },
|
|
250
|
+
record_type: { S: 'OTEL_SPAN' },
|
|
251
|
+
tool: { S: s.tool },
|
|
252
|
+
model: { S: s.model },
|
|
253
|
+
input_tokens: { N: String(s.inputTokens) },
|
|
254
|
+
output_tokens: { N: String(s.outputTokens) },
|
|
255
|
+
cost_usd: { N: String(s.costUsd) },
|
|
256
|
+
project: { S: s.project },
|
|
257
|
+
device_id: { S: s.deviceId },
|
|
258
|
+
cost_estimated: { BOOL: s.costEstimated },
|
|
259
|
+
timestamp: { S: s.timestamp },
|
|
260
|
+
ttl: { N: String(ttl) },
|
|
261
|
+
},
|
|
262
|
+
// Dedup gate: deterministic span IDs make retried batches no-ops.
|
|
263
|
+
ConditionExpression: 'attribute_not_exists(pk)',
|
|
264
|
+
}));
|
|
265
|
+
return true;
|
|
266
|
+
} catch (e) {
|
|
267
|
+
if (e instanceof ConditionalCheckFailedException) return false;
|
|
268
|
+
throw e;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** ADD-increment the daily aggregate for a newly-seen span (bucketed by SPAN date). */
|
|
273
|
+
async function bumpDailyAggregate(user: string, s: ParsedSpan): Promise<void> {
|
|
274
|
+
const day = s.timestamp.slice(0, 10);
|
|
275
|
+
await dynamoClient.send(new UpdateItemCommand({
|
|
276
|
+
TableName: AI_USAGE_TABLE,
|
|
277
|
+
Key: {
|
|
278
|
+
pk: { S: `USER#${user}` },
|
|
279
|
+
sk: { S: `OTEL#DAY#${day}#${s.tool}` },
|
|
280
|
+
},
|
|
281
|
+
UpdateExpression:
|
|
282
|
+
'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',
|
|
284
|
+
ExpressionAttributeNames: { '#day': 'day' },
|
|
285
|
+
ExpressionAttributeValues: {
|
|
286
|
+
':in': { N: String(s.inputTokens) },
|
|
287
|
+
':out': { N: String(s.outputTokens) },
|
|
288
|
+
':cost': { N: String(s.costUsd) },
|
|
289
|
+
':one': { N: '1' },
|
|
290
|
+
':rt': { S: 'OTEL_DAY' },
|
|
291
|
+
':tool': { S: s.tool },
|
|
292
|
+
':day': { S: day },
|
|
293
|
+
':now': { S: new Date().toISOString() },
|
|
294
|
+
},
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Process spans with bounded concurrency. Returns count of newly-written spans. */
|
|
299
|
+
async function processSpans(user: string, spans: ParsedSpan[]): Promise<number> {
|
|
300
|
+
const CONCURRENCY = 20;
|
|
301
|
+
let written = 0;
|
|
302
|
+
for (let i = 0; i < spans.length; i += CONCURRENCY) {
|
|
303
|
+
const chunk = spans.slice(i, i + CONCURRENCY);
|
|
304
|
+
const results = await Promise.all(chunk.map(async (s) => {
|
|
305
|
+
const isNew = await writeSpanIfNew(user, s);
|
|
306
|
+
if (isNew) await bumpDailyAggregate(user, s);
|
|
307
|
+
return isNew;
|
|
308
|
+
}));
|
|
309
|
+
written += results.filter(Boolean).length;
|
|
310
|
+
}
|
|
311
|
+
return written;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function handleTraces(event: HttpApiEvent): Promise<HttpApiResponse> {
|
|
315
|
+
const user = resolveIdentity(event);
|
|
316
|
+
if (!user) {
|
|
317
|
+
return jsonResponse(401, { message: 'No resolvable identity claim in token' });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const rawBody = event.isBase64Encoded
|
|
321
|
+
? Buffer.from(event.body ?? '', 'base64').toString('utf-8')
|
|
322
|
+
: (event.body ?? '');
|
|
323
|
+
|
|
324
|
+
let payload: OtlpPayload;
|
|
325
|
+
try {
|
|
326
|
+
payload = JSON.parse(rawBody) as OtlpPayload;
|
|
327
|
+
} catch {
|
|
328
|
+
return jsonResponse(400, { message: 'Body is not valid JSON' });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { spans, rejected } = parseOtlpSpans(payload);
|
|
332
|
+
|
|
333
|
+
if (spans.length > MAX_BATCH_SIZE) {
|
|
334
|
+
return jsonResponse(400, { message: `Batch exceeds max_batch_size (${MAX_BATCH_SIZE})` });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Archive the raw batch first — the S3 OTLP archive is the external contract.
|
|
338
|
+
// Archive failures are fatal (client retries the batch; dedup gate makes it safe).
|
|
339
|
+
await archiveToS3(rawBody);
|
|
340
|
+
|
|
341
|
+
const written = await processSpans(user, spans);
|
|
342
|
+
console.log(
|
|
343
|
+
`[otel-receiver] user=${user} received=${spans.length + rejected} accepted=${spans.length} ` +
|
|
344
|
+
`new=${written} duplicates=${spans.length - written} rejected=${rejected}`,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// OTLP/HTTP success response; report malformed spans via partialSuccess.
|
|
348
|
+
if (rejected > 0) {
|
|
349
|
+
return jsonResponse(200, {
|
|
350
|
+
partialSuccess: { rejectedSpans: rejected, errorMessage: 'Spans missing required ai.* attributes or failed validation' },
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return jsonResponse(200, {});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---- Handler ----
|
|
357
|
+
|
|
358
|
+
export async function handler(event: HttpApiEvent): Promise<HttpApiResponse> {
|
|
359
|
+
const method = event.requestContext.http.method;
|
|
360
|
+
const path = event.rawPath;
|
|
361
|
+
|
|
362
|
+
if (method === 'GET' && path === '/.well-known/codeburn-export.json') {
|
|
363
|
+
return handleDiscovery();
|
|
364
|
+
}
|
|
365
|
+
if (method === 'POST' && path === '/v1/traces') {
|
|
366
|
+
return handleTraces(event);
|
|
367
|
+
}
|
|
368
|
+
return jsonResponse(404, { message: 'Not found' });
|
|
369
|
+
}
|
|
@@ -13,15 +13,18 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
|
13
13
|
import { PrismVpcConstruct } from './constructs/prism-vpc-construct';
|
|
14
14
|
import { GuardrailEnforcerConstruct } from './constructs/guardrail-enforcer-construct';
|
|
15
15
|
import { SecurityAgentConstruct } from './constructs/security-agent-construct';
|
|
16
|
+
import { OtelCollectorConstruct } from './constructs/otel-collector-construct';
|
|
16
17
|
import { NagSuppressions } from 'cdk-nag';
|
|
17
18
|
|
|
18
19
|
export class MetricsPipelineStack extends cdk.Stack {
|
|
19
20
|
public readonly eventBus: events.EventBus;
|
|
20
21
|
public readonly eventsTable: dynamodb.Table;
|
|
22
|
+
public readonly aiUsageTable: dynamodb.Table;
|
|
21
23
|
public readonly metadataTable: dynamodb.Table;
|
|
22
24
|
public readonly kmsKey: kms.Key;
|
|
23
25
|
public readonly guardrail: BedrockGuardrailConstruct;
|
|
24
26
|
public readonly securityAgent?: SecurityAgentConstruct;
|
|
27
|
+
public readonly otelCollector?: OtelCollectorConstruct;
|
|
25
28
|
|
|
26
29
|
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
|
27
30
|
super(scope, id, props);
|
|
@@ -127,6 +130,38 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
127
130
|
encryptionKey: prismKmsKey,
|
|
128
131
|
});
|
|
129
132
|
|
|
133
|
+
// -------------------------------------------------------
|
|
134
|
+
// DynamoDB AI-usage table — user-keyed, KMS encrypted
|
|
135
|
+
// pk = USER#<email>
|
|
136
|
+
// sk = COMMIT#<ts>#<sha>#<tool> | SPAN#<ts>#<spanId> | OTEL#DAY#<date>#<tool>
|
|
137
|
+
// Sparse GSIs (only commit-level items set them) invert the user key so the
|
|
138
|
+
// same table answers by-repo / by-PR / by-commit / global-by-date.
|
|
139
|
+
// -------------------------------------------------------
|
|
140
|
+
this.aiUsageTable = new dynamodb.Table(this, 'AiUsageTable', {
|
|
141
|
+
tableName: 'prism-d1-ai-usage',
|
|
142
|
+
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
|
|
143
|
+
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
|
|
144
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
145
|
+
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
|
|
146
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
147
|
+
timeToLiveAttribute: 'ttl',
|
|
148
|
+
encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED,
|
|
149
|
+
encryptionKey: prismKmsKey,
|
|
150
|
+
});
|
|
151
|
+
for (const [indexName, pkAttr, skAttr] of [
|
|
152
|
+
['by-repo', 'gsi_repo', 'gsi_repo_sk'],
|
|
153
|
+
['by-pr', 'gsi_pr', 'gsi_pr_sk'],
|
|
154
|
+
['by-commit', 'gsi_commit', 'gsi_commit_sk'],
|
|
155
|
+
['by-date', 'gsi_date', 'gsi_date_sk'],
|
|
156
|
+
] as const) {
|
|
157
|
+
this.aiUsageTable.addGlobalSecondaryIndex({
|
|
158
|
+
indexName,
|
|
159
|
+
partitionKey: { name: pkAttr, type: dynamodb.AttributeType.STRING },
|
|
160
|
+
sortKey: { name: skAttr, type: dynamodb.AttributeType.STRING },
|
|
161
|
+
projectionType: dynamodb.ProjectionType.ALL,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
// -------------------------------------------------------
|
|
131
166
|
// VPC for Lambda isolation (Pillar 6)
|
|
132
167
|
// Opt out with: cdk deploy -c skipVpc=true
|
|
@@ -253,6 +288,25 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
253
288
|
// -------------------------------------------------------
|
|
254
289
|
this.guardrail = new BedrockGuardrailConstruct(this, 'PrismGuardrail', createDefaultPrismGuardrailProps());
|
|
255
290
|
|
|
291
|
+
// -------------------------------------------------------
|
|
292
|
+
// OTEL Collector (opt-in — server side of `codeburn sync`)
|
|
293
|
+
// Enable with: npx cdk deploy --context enableOtelCollector=true
|
|
294
|
+
// BYO IdP: -c otelIssuer=... -c otelClientId=... [-c otelIdentityClaim=email]
|
|
295
|
+
// Per-user AI usage flows directly from codeburn sync to the
|
|
296
|
+
// ai-usage table (spans + daily aggregates).
|
|
297
|
+
// -------------------------------------------------------
|
|
298
|
+
const enableOtelCollector = this.node.tryGetContext('enableOtelCollector') === 'true';
|
|
299
|
+
if (enableOtelCollector) {
|
|
300
|
+
this.otelCollector = new OtelCollectorConstruct(this, 'OtelCollector', {
|
|
301
|
+
aiUsageTable: this.aiUsageTable,
|
|
302
|
+
kmsKey: prismKmsKey,
|
|
303
|
+
lambdaVpcProps,
|
|
304
|
+
externalIssuer: this.node.tryGetContext('otelIssuer') as string | undefined,
|
|
305
|
+
externalClientId: this.node.tryGetContext('otelClientId') as string | undefined,
|
|
306
|
+
identityClaim: this.node.tryGetContext('otelIdentityClaim') as string | undefined,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
256
310
|
// -------------------------------------------------------
|
|
257
311
|
// AWS Security Agent (opt-in — requires Security Agent access)
|
|
258
312
|
// Enable with: npx cdk deploy --context enableSecurityAgent=true
|
|
@@ -681,7 +735,7 @@ export class MetricsPipelineStack extends cdk.Stack {
|
|
|
681
735
|
{
|
|
682
736
|
id: 'AwsSolutions-IAM5',
|
|
683
737
|
reason: 'CDK grantReadData on DynamoDB generates wildcard for GSI index ARNs via token reference',
|
|
684
|
-
appliesTo: ['Resource::<EventsTableD24865E5.Arn>/index/*'],
|
|
738
|
+
appliesTo: ['Resource::<EventsTableD24865E5.Arn>/index/*', 'Resource::<AiUsageTable79F37CE0.Arn>/index/*'],
|
|
685
739
|
},
|
|
686
740
|
{ id: 'AwsSolutions-L1', reason: 'All Lambdas use nodejs22.x which is the latest Node.js runtime available' },
|
|
687
741
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commit-trailers.d.ts","sourceRoot":"","sources":["../../../../src/commands/git/commit-trailers.ts"],"names":[],"mappings":";;;;;;iBA4JqB;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;AAP/E,
|
|
1
|
+
{"version":3,"file":"commit-trailers.d.ts","sourceRoot":"","sources":["../../../../src/commands/git/commit-trailers.ts"],"names":[],"mappings":";;;;;;iBA4JqB;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;AAP/E,wBAgGE"}
|
|
@@ -3,7 +3,6 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
3
3
|
import { resolve, basename } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { collectKiroCli } from '../parse-code-tool/kiro-cli.js';
|
|
6
|
-
import { collectKiroIde } from '../parse-code-tool/kiro-ide.js';
|
|
7
6
|
const DEFAULT_CLAUDE_MODEL = 'us.anthropic.claude-sonnet-4-5-20250929-v1:0';
|
|
8
7
|
function gitRoot(explicit) {
|
|
9
8
|
if (explicit)
|
|
@@ -89,29 +88,32 @@ function runCodeburn(projectFilter, provider) {
|
|
|
89
88
|
/** Collect cumulative token usage for the given tool + project. */
|
|
90
89
|
async function collectUsage(tool, projectBasename, projectFilter) {
|
|
91
90
|
if (tool === 'kiro') {
|
|
91
|
+
// Kiro usage is tracked by codeburn natively (provider: kiro). codeburn reads
|
|
92
|
+
// Kiro's session files and provides a real-time cumulative view, unlike the
|
|
93
|
+
// prism-cli kiro-ide parser which only sees stale on-disk snapshots.
|
|
92
94
|
let out = null;
|
|
93
95
|
if (process.env.KIRO_SESSION_ID) {
|
|
94
96
|
out = await collectKiroCli(process.env.KIRO_SESSION_ID, projectBasename);
|
|
95
97
|
}
|
|
96
|
-
if (
|
|
97
|
-
out
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
if (out && out.overview.calls > 0)
|
|
99
|
+
return { data: out, source: 'kiro-cli' };
|
|
100
|
+
out = runCodeburn(projectFilter, 'kiro');
|
|
101
|
+
if (!out || out.overview.calls === 0)
|
|
102
|
+
out = runCodeburn(projectBasename, 'kiro');
|
|
103
|
+
if (out && out.overview.calls > 0)
|
|
104
|
+
return { data: out, source: 'kiro' };
|
|
105
|
+
return null;
|
|
100
106
|
}
|
|
101
|
-
// cursor → codeburn with the cursor provider filter (codeburn natively tracks
|
|
102
|
-
// Cursor usage). Isolating to --provider cursor keeps its snapshot delta from
|
|
103
|
-
// being polluted by other providers' totals.
|
|
104
107
|
if (tool === 'cursor') {
|
|
105
108
|
let out = runCodeburn(projectFilter, 'cursor');
|
|
106
109
|
if (!out || out.overview.calls === 0)
|
|
107
110
|
out = runCodeburn(projectBasename, 'cursor');
|
|
108
|
-
return out;
|
|
111
|
+
return out ? { data: out, source: 'cursor' } : null;
|
|
109
112
|
}
|
|
110
|
-
// claude-code / q-developer → codeburn (full-path slug, then basename fallback)
|
|
111
113
|
let out = runCodeburn(projectFilter);
|
|
112
114
|
if (!out || out.overview.calls === 0)
|
|
113
115
|
out = runCodeburn(projectBasename);
|
|
114
|
-
return out;
|
|
116
|
+
return out ? { data: out, source: tool } : null;
|
|
115
117
|
}
|
|
116
118
|
function sumTokens(o, key) {
|
|
117
119
|
return (o.models || []).reduce((s, m) => s + (m[key] || 0), 0);
|
|
@@ -166,21 +168,18 @@ export default {
|
|
|
166
168
|
let outputTokens = null;
|
|
167
169
|
let cost = null;
|
|
168
170
|
if (tool) {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
+
const result = await collectUsage(tool, projectBasename, projectFilter);
|
|
172
|
+
if (result) {
|
|
173
|
+
const { data: usage, source: usageSource } = result;
|
|
171
174
|
const curIn = sumTokens(usage, 'inputTokens');
|
|
172
175
|
const curOut = sumTokens(usage, 'outputTokens');
|
|
173
176
|
const curCost = usage.overview?.cost ?? 0;
|
|
174
177
|
const trackerDir = resolve(homedir(), '.prism', 'tokentracker');
|
|
175
|
-
// Snapshot baselines are keyed by (project,
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
// change are left orphaned (never read again); the first commit per
|
|
181
|
-
// (project, tool) finds no baseline and correctly reports a 0 delta while
|
|
182
|
-
// establishing the new baseline.
|
|
183
|
-
const trackerFile = resolve(trackerDir, `${projectBasename}.${tool}.json`);
|
|
178
|
+
// Snapshot baselines are keyed by (project, source) — NOT just (project, tool).
|
|
179
|
+
// Kiro has two sources (kiro-cli per-session vs kiro-ide lifetime) with
|
|
180
|
+
// vastly different cumulative totals. Sharing a tracker file between them
|
|
181
|
+
// produces massive false deltas when the fallback path switches.
|
|
182
|
+
const trackerFile = resolve(trackerDir, `${projectBasename}.${usageSource}.json`);
|
|
184
183
|
if (existsSync(trackerFile)) {
|
|
185
184
|
try {
|
|
186
185
|
const prev = JSON.parse(readFileSync(trackerFile, 'utf8'));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commit-trailers.js","sourceRoot":"","sources":["../../../../src/commands/git/commit-trailers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,cAAc,EAAuB,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"commit-trailers.js","sourceRoot":"","sources":["../../../../src/commands/git/commit-trailers.ts"],"names":[],"mappings":"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"}
|