Skip to content

Commit 8aa203c

Browse files
committed
Tracing v2
1 parent 5ff1f28 commit 8aa203c

File tree

19 files changed

+2581
-1220
lines changed

19 files changed

+2581
-1220
lines changed

apps/sim/app/api/billing/update-cost/route.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
55
import { recordUsage } from '@/lib/billing/core/usage-log'
66
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
7+
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
78
import { checkInternalApiKey } from '@/lib/copilot/request/http'
9+
import { withIncomingGoSpan } from '@/lib/copilot/request/otel'
810
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
911
import { type AtomicClaimResult, billingIdempotency } from '@/lib/core/idempotency/service'
1012
import { generateRequestId } from '@/lib/core/utils/request'
@@ -26,8 +28,28 @@ const UpdateCostSchema = z.object({
2628
/**
2729
* POST /api/billing/update-cost
2830
* Update user cost with a pre-calculated cost value (internal API key auth required)
31+
*
32+
* Parented under the Go-side `sim.update_cost` span via W3C traceparent
33+
* propagation. Every mothership request that bills should therefore show
34+
* the Go client span AND this Sim server span sharing one trace, with
35+
* the actual usage/overage work nested below.
2936
*/
3037
export async function POST(req: NextRequest) {
38+
return withIncomingGoSpan(
39+
req.headers,
40+
TraceSpan.CopilotBillingUpdateCost,
41+
{
42+
'http.method': 'POST',
43+
'http.route': '/api/billing/update-cost',
44+
},
45+
async (span) => updateCostInner(req, span),
46+
)
47+
}
48+
49+
async function updateCostInner(
50+
req: NextRequest,
51+
span: import('@opentelemetry/api').Span,
52+
): Promise<NextResponse> {
3153
const requestId = generateRequestId()
3254
const startTime = Date.now()
3355
let claim: AtomicClaimResult | null = null
@@ -37,6 +59,8 @@ export async function POST(req: NextRequest) {
3759
logger.info(`[${requestId}] Update cost request started`)
3860

3961
if (!isBillingEnabled) {
62+
span.setAttribute('billing.outcome', 'billing_disabled')
63+
span.setAttribute('http.status_code', 200)
4064
return NextResponse.json({
4165
success: true,
4266
message: 'Billing disabled, cost update skipped',
@@ -52,6 +76,8 @@ export async function POST(req: NextRequest) {
5276
const authResult = checkInternalApiKey(req)
5377
if (!authResult.success) {
5478
logger.warn(`[${requestId}] Authentication failed: ${authResult.error}`)
79+
span.setAttribute('billing.outcome', 'auth_failed')
80+
span.setAttribute('http.status_code', 401)
5581
return NextResponse.json(
5682
{
5783
success: false,
@@ -69,6 +95,8 @@ export async function POST(req: NextRequest) {
6995
errors: validation.error.issues,
7096
body,
7197
})
98+
span.setAttribute('billing.outcome', 'invalid_body')
99+
span.setAttribute('http.status_code', 400)
72100
return NextResponse.json(
73101
{
74102
success: false,
@@ -83,6 +111,17 @@ export async function POST(req: NextRequest) {
83111
validation.data
84112
const isMcp = source === 'mcp_copilot'
85113

114+
span.setAttributes({
115+
'user.id': userId,
116+
'gen_ai.request.model': model,
117+
'billing.source': source,
118+
'billing.cost_usd': cost,
119+
'gen_ai.usage.input_tokens': inputTokens,
120+
'gen_ai.usage.output_tokens': outputTokens,
121+
'billing.is_mcp': isMcp,
122+
...(idempotencyKey ? { 'billing.idempotency_key': idempotencyKey } : {}),
123+
})
124+
86125
claim = idempotencyKey
87126
? await billingIdempotency.atomicallyClaim('update-cost', idempotencyKey)
88127
: null
@@ -93,6 +132,8 @@ export async function POST(req: NextRequest) {
93132
userId,
94133
source,
95134
})
135+
span.setAttribute('billing.outcome', 'duplicate_idempotency_key')
136+
span.setAttribute('http.status_code', 409)
96137
return NextResponse.json(
97138
{
98139
success: false,
@@ -157,6 +198,9 @@ export async function POST(req: NextRequest) {
157198
cost,
158199
})
159200

201+
span.setAttribute('billing.outcome', 'billed')
202+
span.setAttribute('http.status_code', 200)
203+
span.setAttribute('billing.duration_ms', duration)
160204
return NextResponse.json({
161205
success: true,
162206
data: {
@@ -191,6 +235,9 @@ export async function POST(req: NextRequest) {
191235
)
192236
}
193237

238+
span.setAttribute('billing.outcome', 'internal_error')
239+
span.setAttribute('http.status_code', 500)
240+
span.setAttribute('billing.duration_ms', duration)
194241
return NextResponse.json(
195242
{
196243
success: false,

apps/sim/app/api/copilot/api-keys/validate/route.ts

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { checkServerSideUsageLimits } from '@/lib/billing/calculations/usage-monitor'
8+
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
89
import { checkInternalApiKey } from '@/lib/copilot/request/http'
10+
import { withIncomingGoSpan } from '@/lib/copilot/request/otel'
911

1012
const logger = createLogger('CopilotApiKeysValidate')
1113

@@ -14,54 +16,83 @@ const ValidateApiKeySchema = z.object({
1416
})
1517

1618
export async function POST(req: NextRequest) {
17-
try {
18-
const auth = checkInternalApiKey(req)
19-
if (!auth.success) {
20-
return new NextResponse(null, { status: 401 })
21-
}
19+
// Incoming-from-Go: extracts traceparent so this handler's work shows
20+
// up as a child of the Go-side `sim.validate_api_key` span in the same
21+
// trace. If there's no traceparent (manual curl / browser), the helper
22+
// falls back to a new root span.
23+
return withIncomingGoSpan(
24+
req.headers,
25+
TraceSpan.CopilotAuthValidateApiKey,
26+
{
27+
'http.method': 'POST',
28+
'http.route': '/api/copilot/api-keys/validate',
29+
},
30+
async (span) => {
31+
try {
32+
const auth = checkInternalApiKey(req)
33+
if (!auth.success) {
34+
span.setAttribute('copilot.validate.outcome', 'internal_auth_failed')
35+
span.setAttribute('http.status_code', 401)
36+
return new NextResponse(null, { status: 401 })
37+
}
2238

23-
const body = await req.json().catch(() => null)
39+
const body = await req.json().catch(() => null)
40+
const validationResult = ValidateApiKeySchema.safeParse(body)
41+
if (!validationResult.success) {
42+
logger.warn('Invalid validation request', { errors: validationResult.error.errors })
43+
span.setAttribute('copilot.validate.outcome', 'invalid_body')
44+
span.setAttribute('http.status_code', 400)
45+
return NextResponse.json(
46+
{
47+
error: 'userId is required',
48+
details: validationResult.error.errors,
49+
},
50+
{ status: 400 }
51+
)
52+
}
2453

25-
const validationResult = ValidateApiKeySchema.safeParse(body)
54+
const { userId } = validationResult.data
55+
span.setAttribute('user.id', userId)
2656

27-
if (!validationResult.success) {
28-
logger.warn('Invalid validation request', { errors: validationResult.error.errors })
29-
return NextResponse.json(
30-
{
31-
error: 'userId is required',
32-
details: validationResult.error.errors,
33-
},
34-
{ status: 400 }
35-
)
36-
}
57+
const [existingUser] = await db.select().from(user).where(eq(user.id, userId)).limit(1)
58+
if (!existingUser) {
59+
logger.warn('[API VALIDATION] userId does not exist', { userId })
60+
span.setAttribute('copilot.validate.outcome', 'user_not_found')
61+
span.setAttribute('http.status_code', 403)
62+
return NextResponse.json({ error: 'User not found' }, { status: 403 })
63+
}
3764

38-
const { userId } = validationResult.data
65+
logger.info('[API VALIDATION] Validating usage limit', { userId })
66+
const { isExceeded, currentUsage, limit } = await checkServerSideUsageLimits(userId)
67+
span.setAttributes({
68+
'billing.usage.current': currentUsage,
69+
'billing.usage.limit': limit,
70+
'billing.usage.exceeded': isExceeded,
71+
})
3972

40-
const [existingUser] = await db.select().from(user).where(eq(user.id, userId)).limit(1)
41-
if (!existingUser) {
42-
logger.warn('[API VALIDATION] userId does not exist', { userId })
43-
return NextResponse.json({ error: 'User not found' }, { status: 403 })
44-
}
73+
logger.info('[API VALIDATION] Usage limit validated', {
74+
userId,
75+
currentUsage,
76+
limit,
77+
isExceeded,
78+
})
4579

46-
logger.info('[API VALIDATION] Validating usage limit', { userId })
80+
if (isExceeded) {
81+
logger.info('[API VALIDATION] Usage exceeded', { userId, currentUsage, limit })
82+
span.setAttribute('copilot.validate.outcome', 'usage_exceeded')
83+
span.setAttribute('http.status_code', 402)
84+
return new NextResponse(null, { status: 402 })
85+
}
4786

48-
const { isExceeded, currentUsage, limit } = await checkServerSideUsageLimits(userId)
49-
50-
logger.info('[API VALIDATION] Usage limit validated', {
51-
userId,
52-
currentUsage,
53-
limit,
54-
isExceeded,
55-
})
56-
57-
if (isExceeded) {
58-
logger.info('[API VALIDATION] Usage exceeded', { userId, currentUsage, limit })
59-
return new NextResponse(null, { status: 402 })
60-
}
61-
62-
return new NextResponse(null, { status: 200 })
63-
} catch (error) {
64-
logger.error('Error validating usage limit', { error })
65-
return NextResponse.json({ error: 'Failed to validate usage' }, { status: 500 })
66-
}
87+
span.setAttribute('copilot.validate.outcome', 'ok')
88+
span.setAttribute('http.status_code', 200)
89+
return new NextResponse(null, { status: 200 })
90+
} catch (error) {
91+
logger.error('Error validating usage limit', { error })
92+
span.setAttribute('copilot.validate.outcome', 'internal_error')
93+
span.setAttribute('http.status_code', 500)
94+
return NextResponse.json({ error: 'Failed to validate usage' }, { status: 500 })
95+
}
96+
},
97+
)
6798
}

0 commit comments

Comments
 (0)