Add request path to the cdn metrics (#3310)

This commit is contained in:
Kamil Kisiela 2023-11-07 09:46:26 +01:00 committed by GitHub
parent f34274641c
commit 0f289e5293
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 173 additions and 113 deletions

View file

@ -58,6 +58,7 @@ type Event =
| {
type: 'response';
statusCode: number;
requestPath: string;
};
export function createAnalytics(
@ -87,12 +88,12 @@ export function createAnalytics(
});
case 'r2':
return engines.r2.writeDataPoint({
blobs: [event.action, event.statusCode.toString()],
blobs: [event.action, event.statusCode.toString(), targetId],
indexes: [targetId.substring(0, 32)],
});
case 'response':
return engines.response.writeDataPoint({
blobs: [event.statusCode.toString()],
blobs: [event.statusCode.toString(), event.requestPath, targetId],
indexes: [targetId.substring(0, 32)],
});
case 'key-validation':

View file

@ -48,7 +48,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
): Promise<Response | null> => {
const headerKey = request.headers.get(authHeaderName);
if (headerKey === null) {
return new MissingAuthKeyResponse(analytics);
return new MissingAuthKeyResponse(analytics, request);
}
const isValid = await deps.isKeyValid(targetId, headerKey);
@ -57,7 +57,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
return null;
}
return new InvalidAuthKeyResponse(analytics);
return new InvalidAuthKeyResponse(analytics, request);
};
router.get(
@ -77,6 +77,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
status: 404,
},
request.params?.targetId ?? 'unknown',
request,
);
}
@ -94,6 +95,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
},
},
params.targetId,
request,
);
}
@ -133,6 +135,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
'Something went wrong, really wrong.',
{ status: 500 },
params.targetId,
request,
)
);
}
@ -145,10 +148,11 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
status: 304,
},
params.targetId,
request,
);
}
if (result.type === 'notFound') {
return createResponse(analytics, 'Not found.', { status: 404 }, params.targetId);
return createResponse(analytics, 'Not found.', { status: 404 }, params.targetId, request);
}
if (result.type === 'redirect') {
return createResponse(
@ -156,6 +160,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
'Found.',
{ status: 302, headers: { Location: result.location } },
params.targetId,
request,
);
}
},

View file

@ -4,7 +4,7 @@ import { Analytics } from './analytics';
const description = `Please refer to the documentation for more details: https://docs.graphql-hive.com/features/registry-usage`;
export class MissingTargetIDErrorResponse extends Response {
constructor(analytics: Analytics) {
constructor(analytics: Analytics, request: Request) {
super(
JSON.stringify({
code: 'MISSING_TARGET_ID',
@ -24,6 +24,7 @@ export class MissingTargetIDErrorResponse extends Response {
{
type: 'response',
statusCode: 400,
requestPath: request.url,
},
'unknown',
);
@ -31,7 +32,7 @@ export class MissingTargetIDErrorResponse extends Response {
}
export class InvalidArtifactTypeResponse extends Response {
constructor(artifactType: string, analytics: Analytics) {
constructor(artifactType: string, analytics: Analytics, request: Request) {
super(
JSON.stringify({
code: 'INVALID_ARTIFACT_TYPE',
@ -50,6 +51,7 @@ export class InvalidArtifactTypeResponse extends Response {
{
type: 'response',
statusCode: 400,
requestPath: request.url,
},
'unknown',
);
@ -57,7 +59,7 @@ export class InvalidArtifactTypeResponse extends Response {
}
export class MissingAuthKeyResponse extends Response {
constructor(analytics: Analytics) {
constructor(analytics: Analytics, request: Request) {
super(
JSON.stringify({
code: 'MISSING_AUTH_KEY',
@ -76,6 +78,7 @@ export class MissingAuthKeyResponse extends Response {
{
type: 'response',
statusCode: 400,
requestPath: request.url,
},
'unknown',
);
@ -83,7 +86,7 @@ export class MissingAuthKeyResponse extends Response {
}
export class InvalidAuthKeyResponse extends Response {
constructor(analytics: Analytics) {
constructor(analytics: Analytics, request: Request) {
super(
JSON.stringify({
code: 'INVALID_AUTH_KEY',
@ -102,6 +105,7 @@ export class InvalidAuthKeyResponse extends Response {
{
type: 'response',
statusCode: 403,
requestPath: request.url,
},
'unknown',
);
@ -109,7 +113,7 @@ export class InvalidAuthKeyResponse extends Response {
}
export class CDNArtifactNotFound extends Response {
constructor(artifactType: string, targetId: string, analytics: Analytics) {
constructor(artifactType: string, targetId: string, analytics: Analytics, request: Request) {
super(
JSON.stringify({
code: 'NOT_FOUND',
@ -128,6 +132,7 @@ export class CDNArtifactNotFound extends Response {
{
type: 'response',
statusCode: 404,
requestPath: request.url,
},
targetId,
);
@ -135,7 +140,7 @@ export class CDNArtifactNotFound extends Response {
}
export class InvalidArtifactMatch extends Response {
constructor(artifactType: string, targetId: string, analytics: Analytics) {
constructor(artifactType: string, targetId: string, analytics: Analytics, request: Request) {
super(
JSON.stringify({
code: 'INVALID_ARTIFACT_MATCH',
@ -154,6 +159,7 @@ export class InvalidArtifactMatch extends Response {
{
type: 'response',
statusCode: 400,
requestPath: request.url,
},
targetId,
);
@ -161,7 +167,7 @@ export class InvalidArtifactMatch extends Response {
}
export class UnexpectedError extends Response {
constructor(analytics: Analytics) {
constructor(analytics: Analytics, request: Request) {
super(
JSON.stringify({
code: 'UNEXPECTED_ERROR',
@ -180,6 +186,7 @@ export class UnexpectedError extends Response {
{
type: 'response',
statusCode: 500,
requestPath: request.url,
},
'unknown',
);

View file

@ -29,102 +29,140 @@ type SchemaArtifact = {
type ArtifactType = 'schema' | 'supergraph' | 'sdl' | 'metadata' | 'introspection';
const artifactTypes = ['schema', 'supergraph', 'sdl', 'metadata', 'introspection'] as const;
const createArtifactTypesHandlers = (
analytics: Analytics,
): Record<
ArtifactType,
(targetId: string, artifactType: string, rawValue: string, etag: string) => Response
> => ({
/**
* Returns SchemaArtifact or SchemaArtifact[], same way as it's stored in the storage
*/
schema: (targetId: string, artifactType: string, rawValue: string, etag: string) =>
createResponse(
analytics,
rawValue,
{
status: 200,
headers: {
'Content-Type': 'application/json',
etag,
function createArtifactTypesHandlers(analytics: Analytics) {
return {
/**
* Returns SchemaArtifact or SchemaArtifact[], same way as it's stored in the storage
*/
schema(
request: Request,
targetId: string,
artifactType: string,
rawValue: string,
etag: string,
) {
return 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) =>
createResponse(
analytics,
rawValue,
{
status: 200,
headers: {
etag,
targetId,
request,
);
},
/**
* Returns Federation Supergraph, we store it as-is.
*/
supergraph(
request: Request,
targetId: string,
artifactType: string,
rawValue: string,
etag: string,
) {
return 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);
}
targetId,
request,
);
},
sdl(request: Request, targetId: string, artifactType: string, rawValue: string, etag: string) {
if (rawValue.startsWith('[')) {
return new InvalidArtifactMatch(artifactType, targetId, analytics, request);
}
const parsed = JSON.parse(rawValue) as SchemaArtifact;
const parsed = JSON.parse(rawValue) as SchemaArtifact;
return createResponse(
analytics,
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) =>
createResponse(
analytics,
rawValue,
{
status: 200,
headers: {
'Content-Type': 'application/json',
etag,
targetId,
request,
);
},
/**
* Returns Metadata same way as it's stored in the storage
*/
metadata(
request: Request,
targetId: string,
artifactType: string,
rawValue: string,
etag: string,
) {
return 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);
}
targetId,
request,
);
},
introspection(
request: Request,
targetId: string,
artifactType: string,
rawValue: string,
etag: string,
) {
if (rawValue.startsWith('[')) {
return new InvalidArtifactMatch(artifactType, targetId, analytics, request);
}
const parsed = JSON.parse(rawValue) as SchemaArtifact;
const rawSdl = parsed.sdl;
const schema = buildSchema(rawSdl);
const introspection = introspectionFromSchema(schema);
const parsed = JSON.parse(rawValue) as SchemaArtifact;
const rawSdl = parsed.sdl;
const schema = buildSchema(rawSdl);
const introspection = introspectionFromSchema(schema);
return createResponse(
analytics,
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,
);
},
});
targetId,
request,
);
},
} satisfies Record<
ArtifactType,
(
request: Request,
targetId: string,
artifactType: string,
rawValue: string,
etag: string,
) => Response
>;
}
const VALID_ARTIFACT_TYPES = artifactTypes;
const AUTH_HEADER_NAME = 'x-hive-cdn-key';
@ -146,20 +184,20 @@ async function parseIncomingRequest(
if (!targetId) {
return {
error: new MissingTargetIDErrorResponse(analytics),
error: new MissingTargetIDErrorResponse(analytics, request),
};
}
const artifactType = (params[1] || 'schema') as ArtifactType;
if (!VALID_ARTIFACT_TYPES.includes(artifactType)) {
return { error: new InvalidArtifactTypeResponse(artifactType, analytics) };
return { error: new InvalidArtifactTypeResponse(artifactType, analytics, request) };
}
const headerKey = request.headers.get(AUTH_HEADER_NAME);
if (!headerKey) {
return { error: new MissingAuthKeyResponse(analytics) };
return { error: new MissingAuthKeyResponse(analytics, request) };
}
try {
@ -167,7 +205,7 @@ async function parseIncomingRequest(
if (!keyValid) {
return {
error: new InvalidAuthKeyResponse(analytics),
error: new InvalidAuthKeyResponse(analytics, request),
};
}
@ -182,7 +220,7 @@ async function parseIncomingRequest(
} catch (e) {
console.warn(`Failed to validate key for ${targetId}, error:`, e);
return {
error: new InvalidAuthKeyResponse(analytics),
error: new InvalidAuthKeyResponse(analytics, request),
};
}
}
@ -231,20 +269,26 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => {
const ifNoneMatch = request.headers.get('if-none-match');
if (ifNoneMatch && ifNoneMatch === etag) {
return createResponse(analytics, null, { status: 304 }, targetId);
return createResponse(analytics, null, { status: 304 }, targetId, request);
}
switch (artifactType) {
case 'schema':
return artifactTypesHandlers.schema(targetId, artifactType, rawValue, etag);
return artifactTypesHandlers.schema(request, targetId, artifactType, rawValue, etag);
case 'supergraph':
return artifactTypesHandlers.supergraph(targetId, artifactType, rawValue, etag);
return artifactTypesHandlers.supergraph(request, targetId, artifactType, rawValue, etag);
case 'sdl':
return artifactTypesHandlers.sdl(targetId, artifactType, rawValue, etag);
return artifactTypesHandlers.sdl(request, targetId, artifactType, rawValue, etag);
case 'introspection':
return artifactTypesHandlers.introspection(targetId, artifactType, rawValue, etag);
return artifactTypesHandlers.introspection(
request,
targetId,
artifactType,
rawValue,
etag,
);
case 'metadata':
return artifactTypesHandlers.metadata(targetId, artifactType, rawValue, etag);
return artifactTypesHandlers.metadata(request, targetId, artifactType, rawValue, etag);
default:
return createResponse(
analytics,
@ -253,13 +297,14 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => {
status: 500,
},
targetId,
request,
);
}
} else {
console.log(
`CDN Artifact not found for targetId=${targetId}, artifactType=${artifactType}, storageKeyType=${storageKeyType}`,
);
return new CDNArtifactNotFound(artifactType, targetId, analytics);
return new CDNArtifactNotFound(artifactType, targetId, analytics, request);
}
};
};

View file

@ -135,12 +135,12 @@ const handler: ExportedHandler<Env> = {
if (response) {
return response;
}
return createResponse(analytics, 'Not found', { status: 404 }, 'unknown');
return createResponse(analytics, 'Not found', { status: 404 }, 'unknown', request);
});
} catch (error) {
console.error(error);
sentry.captureException(error);
return new UnexpectedError(analytics);
return new UnexpectedError(analytics, request);
}
},
};

View file

@ -8,11 +8,13 @@ export function createResponse(
body: BodyInit | null,
init: ResponseInit,
targetId: string,
request: Request,
) {
analytics.track(
{
type: 'response',
statusCode: init.status ?? 999 /* indicates unknown status code, for some reason... */,
requestPath: request.url,
},
targetId,
);