Track responses and R2 calls (#2815)

This commit is contained in:
Kamil Kisiela 2023-09-14 11:25:58 +02:00 committed by GitHub
parent 86d884069e
commit c4d09cce6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 224 additions and 55 deletions

View file

@ -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)],
});
}
}

View file

@ -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,
);
}
},
);

View file

@ -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) {

View file

@ -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',
);
}
}

View file

@ -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(

View file

@ -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<Env> = {
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<Env> = {
if (response) {
return response;
}
return new Response('Not found', { status: 404 });
return createResponse(analytics, 'Not found', { status: 404 }, 'unknown');
});
} catch (error) {
console.error(error);

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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 }),