@prism-d1/cli 1.0.60 → 1.0.64
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 +42 -13
- 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":";;;;;;
|
|
1
|
+
{"version":3,"file":"commit-trailers.d.ts","sourceRoot":"","sources":["../../../../src/commands/git/commit-trailers.ts"],"names":[],"mappings":";;;;;;iBAwJqB;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;AAP/E,wBAgGE"}
|
|
@@ -38,6 +38,18 @@ function detectTool() {
|
|
|
38
38
|
origin = 'ai-generated';
|
|
39
39
|
tool = 'q-developer';
|
|
40
40
|
}
|
|
41
|
+
// Cursor (VS Code fork). Checked LAST so any commit made from within Cursor
|
|
42
|
+
// wins the single AI-Tool slot, even when another agent's session env vars are
|
|
43
|
+
// also present (e.g. a CLI agent run inside Cursor's integrated terminal).
|
|
44
|
+
// TERM_PROGRAM=vscode is NOT usable (Cursor inherits VS Code's value), so key
|
|
45
|
+
// off CURSOR_AGENT / CURSOR_TRACE_ID or the askpass path Cursor injects into
|
|
46
|
+
// its terminal + Source Control panel, which contains 'cursor'.
|
|
47
|
+
const cursorAskpass = `${env.VSCODE_GIT_ASKPASS_NODE || ''} ${env.VSCODE_GIT_ASKPASS_MAIN || ''} ${env.GIT_ASKPASS || ''}`.toLowerCase();
|
|
48
|
+
if (env.CURSOR_AGENT === '1' || env.CURSOR_TRACE_ID || cursorAskpass.includes('cursor')) {
|
|
49
|
+
origin = 'ai-generated';
|
|
50
|
+
tool = 'cursor';
|
|
51
|
+
model = '';
|
|
52
|
+
}
|
|
41
53
|
return { origin, tool, model };
|
|
42
54
|
}
|
|
43
55
|
/** Find a spec reference: an explicit Spec-Ref in the message wins, else a staged spec file. */
|
|
@@ -54,12 +66,17 @@ function detectSpecRef(repoRoot, msg) {
|
|
|
54
66
|
}
|
|
55
67
|
return '';
|
|
56
68
|
}
|
|
57
|
-
/** Run `codeburn report` and parse its JSON. Returns null if codeburn is absent or errors.
|
|
58
|
-
|
|
69
|
+
/** Run `codeburn report` and parse its JSON. Returns null if codeburn is absent or errors.
|
|
70
|
+
* `provider` (optional) maps to codeburn's `--provider` filter (e.g. 'cursor', 'claude').
|
|
71
|
+
* It is always a fixed internal literal — never user/LLM input — so the Windows shell
|
|
72
|
+
* interpolation below is not an injection vector. */
|
|
73
|
+
function runCodeburn(projectFilter, provider) {
|
|
59
74
|
const isWin = process.platform === 'win32';
|
|
75
|
+
const providerArgs = provider ? ['--provider', provider] : [];
|
|
76
|
+
const providerCmd = provider ? `--provider ${provider} ` : '';
|
|
60
77
|
const r = isWin
|
|
61
|
-
? spawnSync(`codeburn report -p all --project "${projectFilter}" --format json`, { encoding: 'utf8', shell: true })
|
|
62
|
-
: spawnSync('codeburn', ['report', '-p', 'all', '--project', projectFilter, '--format', 'json'], { encoding: 'utf8' });
|
|
78
|
+
? spawnSync(`codeburn report -p all ${providerCmd}--project "${projectFilter}" --format json`, { encoding: 'utf8', shell: true })
|
|
79
|
+
: spawnSync('codeburn', ['report', '-p', 'all', ...providerArgs, '--project', projectFilter, '--format', 'json'], { encoding: 'utf8' });
|
|
63
80
|
if (!r || r.status !== 0 || !r.stdout)
|
|
64
81
|
return null;
|
|
65
82
|
try {
|
|
@@ -76,16 +93,23 @@ async function collectUsage(tool, projectBasename, projectFilter) {
|
|
|
76
93
|
if (process.env.KIRO_SESSION_ID) {
|
|
77
94
|
out = await collectKiroCli(process.env.KIRO_SESSION_ID, projectBasename);
|
|
78
95
|
}
|
|
79
|
-
if (
|
|
80
|
-
out
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
if (out && out.overview.calls > 0)
|
|
97
|
+
return { data: out, source: 'kiro-cli' };
|
|
98
|
+
out = await collectKiroIde(projectBasename);
|
|
99
|
+
if (out && out.overview.calls > 0)
|
|
100
|
+
return { data: out, source: 'kiro-ide' };
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (tool === 'cursor') {
|
|
104
|
+
let out = runCodeburn(projectFilter, 'cursor');
|
|
105
|
+
if (!out || out.overview.calls === 0)
|
|
106
|
+
out = runCodeburn(projectBasename, 'cursor');
|
|
107
|
+
return out ? { data: out, source: 'cursor' } : null;
|
|
83
108
|
}
|
|
84
|
-
// claude-code / q-developer → codeburn (full-path slug, then basename fallback)
|
|
85
109
|
let out = runCodeburn(projectFilter);
|
|
86
110
|
if (!out || out.overview.calls === 0)
|
|
87
111
|
out = runCodeburn(projectBasename);
|
|
88
|
-
return out;
|
|
112
|
+
return out ? { data: out, source: tool } : null;
|
|
89
113
|
}
|
|
90
114
|
function sumTokens(o, key) {
|
|
91
115
|
return (o.models || []).reduce((s, m) => s + (m[key] || 0), 0);
|
|
@@ -140,13 +164,18 @@ export default {
|
|
|
140
164
|
let outputTokens = null;
|
|
141
165
|
let cost = null;
|
|
142
166
|
if (tool) {
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
167
|
+
const result = await collectUsage(tool, projectBasename, projectFilter);
|
|
168
|
+
if (result) {
|
|
169
|
+
const { data: usage, source: usageSource } = result;
|
|
145
170
|
const curIn = sumTokens(usage, 'inputTokens');
|
|
146
171
|
const curOut = sumTokens(usage, 'outputTokens');
|
|
147
172
|
const curCost = usage.overview?.cost ?? 0;
|
|
148
173
|
const trackerDir = resolve(homedir(), '.prism', 'tokentracker');
|
|
149
|
-
|
|
174
|
+
// Snapshot baselines are keyed by (project, source) — NOT just (project, tool).
|
|
175
|
+
// Kiro has two sources (kiro-cli per-session vs kiro-ide lifetime) with
|
|
176
|
+
// vastly different cumulative totals. Sharing a tracker file between them
|
|
177
|
+
// produces massive false deltas when the fallback path switches.
|
|
178
|
+
const trackerFile = resolve(trackerDir, `${projectBasename}.${usageSource}.json`);
|
|
150
179
|
if (existsSync(trackerFile)) {
|
|
151
180
|
try {
|
|
152
181
|
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":"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
|
|
1
|
+
{"version":3,"file":"commit-trailers.js","sourceRoot":"","sources":["../../../../src/commands/git/commit-trailers.ts"],"names":[],"mappings":"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"}
|