From c4d09cce6fab19156c4b1d60e7fba60222446c00 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Thu, 14 Sep 2023 11:25:58 +0200 Subject: [PATCH] Track responses and R2 calls (#2815) --- packages/services/cdn-worker/src/analytics.ts | 17 +++- .../cdn-worker/src/artifact-handler.ts | 60 +++++++++--- .../cdn-worker/src/artifact-storage-reader.ts | 10 ++ packages/services/cdn-worker/src/errors.ts | 49 ++++++++++ packages/services/cdn-worker/src/handler.ts | 95 ++++++++++++------- packages/services/cdn-worker/src/index.ts | 7 +- .../services/cdn-worker/src/key-validation.ts | 18 ++++ .../cdn-worker/src/tracked-response.ts | 21 ++++ packages/services/server/src/index.ts | 2 +- 9 files changed, 224 insertions(+), 55 deletions(-) create mode 100644 packages/services/cdn-worker/src/tracked-response.ts diff --git a/packages/services/cdn-worker/src/analytics.ts b/packages/services/cdn-worker/src/analytics.ts index 483ce2be9..e983f7e59 100644 --- a/packages/services/cdn-worker/src/analytics.ts +++ b/packages/services/cdn-worker/src/analytics.ts @@ -49,6 +49,15 @@ type Event = | { type: 'error'; value: [string, string] | [string]; + } + | { + type: 'r2'; + action: 'HEAD artifact' | 'GET cdn-legacy-keys' | 'GET cdn-access-token'; + statusCode: number; + } + | { + type: 'response'; + statusCode: number; }; export function createAnalytics( @@ -68,7 +77,7 @@ export function createAnalytics( case 'artifact': return engines.usage.writeDataPoint({ blobs: [event.version, event.value, targetId], - indexes: [targetId.substr(0, 32)], + indexes: [targetId.substring(0, 32)], }); case 'error': return engines.error.writeDataPoint({ @@ -83,7 +92,7 @@ export function createAnalytics( event.value.version, event.value.isValid ? 'valid' : 'invalid', ], - indexes: [targetId.substr(0, 32)], + indexes: [targetId.substring(0, 32)], }); case 'cache-write': return engines.keyValidation.writeDataPoint({ @@ -92,12 +101,12 @@ export function createAnalytics( event.value.version, event.value.isValid ? 'valid' : 'invalid', ], - indexes: [targetId.substr(0, 32)], + indexes: [targetId.substring(0, 32)], }); case 's3-key-validation': return engines.keyValidation.writeDataPoint({ blobs: ['s3-key-validation', event.value.version, event.value.status], - indexes: [targetId.substr(0, 32)], + indexes: [targetId.substring(0, 32)], }); } } diff --git a/packages/services/cdn-worker/src/artifact-handler.ts b/packages/services/cdn-worker/src/artifact-handler.ts index df2ffd867..5cd17e57e 100644 --- a/packages/services/cdn-worker/src/artifact-handler.ts +++ b/packages/services/cdn-worker/src/artifact-handler.ts @@ -1,12 +1,11 @@ import itty from 'itty-router'; import zod from 'zod'; -import { createFetch, type Request } from '@whatwg-node/fetch'; +import { type Request } from '@whatwg-node/fetch'; import { createAnalytics, type Analytics } from './analytics'; import { type ArtifactsType } from './artifact-storage-reader'; import { InvalidAuthKeyResponse, MissingAuthKeyResponse } from './errors'; import type { KeyValidator } from './key-validation'; - -const { Response } = createFetch({ useNodeFetch: true }); +import { createResponse } from './tracked-response'; type ArtifactRequestHandler = { getArtifactAction: ( @@ -67,19 +66,35 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { const parseResult = ParamsModel.safeParse(request.params); if (parseResult.success === false) { - return new Response('Not found.', { status: 404 }); + analytics.track( + { type: 'error', value: ['invalid-params'] }, + request.params?.targetId ?? 'unknown', + ); + return createResponse( + analytics, + 'Not found.', + { + status: 404, + }, + request.params?.targetId ?? 'unknown', + ); } const params = parseResult.data; /** Legacy handling for old client SDK versions. */ if (params.artifactType === 'schema') { - return new Response('Found.', { - status: 301, - headers: { - Location: request.url.replace('/schema', '/services'), + return createResponse( + analytics, + 'Found.', + { + status: 301, + headers: { + Location: request.url.replace('/schema', '/services'), + }, }, - }); + params.targetId, + ); } const maybeResponse = await authenticate(request, params.targetId); @@ -113,20 +128,35 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { if (!result) { return ( deps.fallback?.(request, params) ?? - new Response('Something went wrong, really wrong.', { status: 500 }) + createResponse( + analytics, + 'Something went wrong, really wrong.', + { status: 500 }, + params.targetId, + ) ); } if (result.type === 'notModified') { - return new Response('', { - status: 304, - }); + return createResponse( + analytics, + '', + { + status: 304, + }, + params.targetId, + ); } if (result.type === 'notFound') { - return new Response('Not found.', { status: 404 }); + return createResponse(analytics, 'Not found.', { status: 404 }, params.targetId); } if (result.type === 'redirect') { - return new Response('Found.', { status: 302, headers: { Location: result.location } }); + return createResponse( + analytics, + 'Found.', + { status: 302, headers: { Location: result.location } }, + params.targetId, + ); } }, ); diff --git a/packages/services/cdn-worker/src/artifact-storage-reader.ts b/packages/services/cdn-worker/src/artifact-storage-reader.ts index 53b36e486..37879b41b 100644 --- a/packages/services/cdn-worker/src/artifact-storage-reader.ts +++ b/packages/services/cdn-worker/src/artifact-storage-reader.ts @@ -1,3 +1,4 @@ +import type { Analytics } from './analytics'; import { AwsClient } from './aws'; const presignedUrlExpirationSeconds = 60; @@ -23,6 +24,7 @@ export class ArtifactStorageReader { }, /** The public URL in case the public S3 endpoint differs from the internal S3 endpoint. E.g. within a docker network. */ publicUrl: string | null, + private analytics: Analytics | null, ) { this.publicUrl = publicUrl ? new URL(publicUrl) : null; } @@ -72,6 +74,14 @@ export class ArtifactStorageReader { }, }, ); + this.analytics?.track( + { + type: 'r2', + statusCode: response.status, + action: 'HEAD artifact', + }, + targetId, + ); if (response.status === 200) { if (etagValue && response.headers.get('etag') === etagValue) { diff --git a/packages/services/cdn-worker/src/errors.ts b/packages/services/cdn-worker/src/errors.ts index 3925133c9..e440056ec 100644 --- a/packages/services/cdn-worker/src/errors.ts +++ b/packages/services/cdn-worker/src/errors.ts @@ -20,6 +20,13 @@ export class MissingTargetIDErrorResponse extends Response { ); analytics.track({ type: 'error', value: ['missing_target_id'] }, 'unknown'); + analytics.track( + { + type: 'response', + statusCode: 400, + }, + 'unknown', + ); } } @@ -39,6 +46,13 @@ export class InvalidArtifactTypeResponse extends Response { }, ); analytics.track({ type: 'error', value: ['invalid_artifact_type', artifactType] }, 'unknown'); + analytics.track( + { + type: 'response', + statusCode: 400, + }, + 'unknown', + ); } } @@ -58,6 +72,13 @@ export class MissingAuthKeyResponse extends Response { }, ); analytics.track({ type: 'error', value: ['missing_auth_key'] }, 'unknown'); + analytics.track( + { + type: 'response', + statusCode: 400, + }, + 'unknown', + ); } } @@ -77,6 +98,13 @@ export class InvalidAuthKeyResponse extends Response { }, ); analytics.track({ type: 'error', value: ['invalid_auth_key'] }, 'unknown'); + analytics.track( + { + type: 'response', + statusCode: 403, + }, + 'unknown', + ); } } @@ -96,6 +124,13 @@ export class CDNArtifactNotFound extends Response { }, ); analytics.track({ type: 'error', value: ['artifact_not_found', artifactType] }, targetId); + analytics.track( + { + type: 'response', + statusCode: 404, + }, + targetId, + ); } } @@ -115,6 +150,13 @@ export class InvalidArtifactMatch extends Response { }, ); analytics.track({ type: 'error', value: ['invalid_artifact_match', artifactType] }, targetId); + analytics.track( + { + type: 'response', + statusCode: 400, + }, + targetId, + ); } } @@ -134,5 +176,12 @@ export class UnexpectedError extends Response { }, ); analytics.track({ type: 'error', value: ['unexpected_error'] }, 'unknown'); + analytics.track( + { + type: 'response', + statusCode: 500, + }, + 'unknown', + ); } } diff --git a/packages/services/cdn-worker/src/handler.ts b/packages/services/cdn-worker/src/handler.ts index 61f76e9c3..2593728f1 100644 --- a/packages/services/cdn-worker/src/handler.ts +++ b/packages/services/cdn-worker/src/handler.ts @@ -9,6 +9,7 @@ import { MissingTargetIDErrorResponse, } from './errors'; import type { KeyValidator } from './key-validation'; +import { createResponse } from './tracked-response'; async function createETag(value: string) { const myText = new TextEncoder().encode(value); @@ -38,23 +39,33 @@ const createArtifactTypesHandlers = ( * Returns SchemaArtifact or SchemaArtifact[], same way as it's stored in the storage */ schema: (targetId: string, artifactType: string, rawValue: string, etag: string) => - new Response(rawValue, { - status: 200, - headers: { - 'Content-Type': 'application/json', - etag, + createResponse( + analytics, + rawValue, + { + status: 200, + headers: { + 'Content-Type': 'application/json', + etag, + }, }, - }), + targetId, + ), /** * Returns Federation Supergraph, we store it as-is. */ supergraph: (targetId: string, artifactType: string, rawValue: string, etag: string) => - new Response(rawValue, { - status: 200, - headers: { - etag, + createResponse( + analytics, + rawValue, + { + status: 200, + headers: { + etag, + }, }, - }), + targetId, + ), sdl: (targetId: string, artifactType: string, rawValue: string, etag: string) => { if (rawValue.startsWith('[')) { return new InvalidArtifactMatch(artifactType, targetId, analytics); @@ -62,24 +73,34 @@ const createArtifactTypesHandlers = ( const parsed = JSON.parse(rawValue) as SchemaArtifact; - return new Response(parsed.sdl, { - status: 200, - headers: { - etag, + return createResponse( + analytics, + parsed.sdl, + { + status: 200, + headers: { + etag, + }, }, - }); + targetId, + ); }, /** * Returns Metadata same way as it's stored in the storage */ metadata: (targetId: string, artifactType: string, rawValue: string, etag: string) => - new Response(rawValue, { - status: 200, - headers: { - 'Content-Type': 'application/json', - etag, + createResponse( + analytics, + rawValue, + { + status: 200, + headers: { + 'Content-Type': 'application/json', + etag, + }, }, - }), + targetId, + ), introspection: (targetId: string, artifactType: string, rawValue: string, etag: string) => { if (rawValue.startsWith('[')) { return new InvalidArtifactMatch(artifactType, targetId, analytics); @@ -90,13 +111,18 @@ const createArtifactTypesHandlers = ( const schema = buildSchema(rawSdl); const introspection = introspectionFromSchema(schema); - return new Response(JSON.stringify(introspection), { - status: 200, - headers: { - 'Content-Type': 'application/json', - etag, + return createResponse( + analytics, + JSON.stringify(introspection), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + etag, + }, }, - }); + targetId, + ); }, }); @@ -205,7 +231,7 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => { const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch && ifNoneMatch === etag) { - return new Response(null, { status: 304 }); + return createResponse(analytics, null, { status: 304 }, targetId); } switch (artifactType) { @@ -220,9 +246,14 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => { case 'metadata': return artifactTypesHandlers.metadata(targetId, artifactType, rawValue, etag); default: - return new Response(null, { - status: 500, - }); + return createResponse( + analytics, + null, + { + status: 500, + }, + targetId, + ); } } else { console.log( diff --git a/packages/services/cdn-worker/src/index.ts b/packages/services/cdn-worker/src/index.ts index 84bf34be5..ffa24098f 100644 --- a/packages/services/cdn-worker/src/index.ts +++ b/packages/services/cdn-worker/src/index.ts @@ -7,6 +7,7 @@ import { AwsClient } from './aws'; import { UnexpectedError } from './errors'; import { createRequestHandler } from './handler'; import { createIsKeyValid } from './key-validation'; +import { createResponse } from './tracked-response'; type Env = { S3_ENDPOINT: string; @@ -45,14 +46,14 @@ const handler: ExportedHandler = { endpoint: env.S3_ENDPOINT, }; - const artifactStorageReader = new ArtifactStorageReader(s3, null); - const analytics = createAnalytics({ usage: env.USAGE_ANALYTICS, error: env.ERROR_ANALYTICS, keyValidation: env.KEY_VALIDATION_ANALYTICS, }); + const artifactStorageReader = new ArtifactStorageReader(s3, null, analytics); + const isKeyValid = createIsKeyValid({ waitUntil: p => ctx.waitUntil(p), getCache: () => caches.open('artifacts-auth'), @@ -130,7 +131,7 @@ const handler: ExportedHandler = { if (response) { return response; } - return new Response('Not found', { status: 404 }); + return createResponse(analytics, 'Not found', { status: 404 }, 'unknown'); }); } catch (error) { console.error(error); diff --git a/packages/services/cdn-worker/src/key-validation.ts b/packages/services/cdn-worker/src/key-validation.ts index 3ae6e290e..fbbfc3df0 100644 --- a/packages/services/cdn-worker/src/key-validation.ts +++ b/packages/services/cdn-worker/src/key-validation.ts @@ -126,6 +126,15 @@ const handleLegacyCDNAccessToken = async (args: { }, ); + args.analytics?.track( + { + type: 'r2', + statusCode: key.status, + action: 'GET cdn-legacy-keys', + }, + args.targetId, + ); + if (key.status !== 200) { return withCache(false); } @@ -238,6 +247,15 @@ async function handleCDNAccessToken( }, ); + deps.analytics?.track( + { + type: 'r2', + statusCode: key.status, + action: 'GET cdn-access-token', + }, + targetId, + ); + if (key.status !== 200) { return withCache(false); } diff --git a/packages/services/cdn-worker/src/tracked-response.ts b/packages/services/cdn-worker/src/tracked-response.ts new file mode 100644 index 000000000..8283b644d --- /dev/null +++ b/packages/services/cdn-worker/src/tracked-response.ts @@ -0,0 +1,21 @@ +import { createFetch } from '@whatwg-node/fetch'; +import type { Analytics } from './analytics'; + +const { Response } = createFetch({ useNodeFetch: true }); + +export function createResponse( + analytics: Analytics, + body: BodyInit | null, + init: ResponseInit, + targetId: string, +) { + analytics.track( + { + type: 'response', + statusCode: init.status ?? 999 /* indicates unknown status code, for some reason... */, + }, + targetId, + ); + + return new Response(body, init); +} diff --git a/packages/services/server/src/index.ts b/packages/services/server/src/index.ts index 0c9482359..6748aa642 100644 --- a/packages/services/server/src/index.ts +++ b/packages/services/server/src/index.ts @@ -380,7 +380,7 @@ export async function main() { bucketName: env.s3.bucketName, }; - const artifactStorageReader = new ArtifactStorageReader(s3, env.s3.publicUrl); + const artifactStorageReader = new ArtifactStorageReader(s3, env.s3.publicUrl, null); const artifactHandler = createArtifactRequestHandler({ isKeyValid: createIsKeyValid({ s3, analytics: null, getCache: null, waitUntil: null }),