Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { SQSBatchItemFailure, SQSEvent, SQSHandler } from "aws-lambda";
import { PublishCommand } from "@aws-sdk/client-sns";
import { LetterEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events";
import { mapLetterToCloudEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-mapper";
import { Unit } from "aws-embedded-metrics";
import pino from "pino";
import {
UpdateLetterCommand,
UpdateLetterCommandSchema,
} from "../contracts/letters";
import { Deps } from "../config/deps";
import { MetricEntry, MetricStatus, buildEMFObject } from "../utils/metrics";

export default function createTransformAmendmentEventHandler(
deps: Deps,
Expand Down Expand Up @@ -39,6 +42,11 @@ export default function createTransformAmendmentEventHandler(
messageId: message.messageId,
correlationId: message.messageAttributes.CorrelationId.stringValue,
});
emitSuccessMetrics(
updateLetterCommand.supplierId,
updateLetterCommand.status,
deps.logger,
);
} catch (error) {
deps.logger.error({
description: "Error processing letter status update",
Expand All @@ -52,7 +60,7 @@ export default function createTransformAmendmentEventHandler(
});

await Promise.all(tasks);

emitFailedItems(batchItemFailures, deps.logger);
return { batchItemFailures };
};
}
Expand All @@ -66,3 +74,43 @@ function buildSnsCommand(
Message: JSON.stringify(letterEvent),
});
}

function emitSuccessMetrics(
supplierId: string,
status: string,
logger: pino.Logger,
) {
const dimensions: Record<string, string> = {
supplier: supplierId,
status,
};
const metric: MetricEntry = {
key: MetricStatus.Success,
value: 1,
unit: Unit.Count,
};
const emf = buildEMFObject("amendment-event-transformer", dimensions, metric);
logger.info(emf);
}

function emitFailedItems(
batchFailures: SQSBatchItemFailure[],
logger: pino.Logger,
) {
for (const item of batchFailures) {
const dimensions: Record<string, string> = {
identifier: item.itemIdentifier,
};
const metric: MetricEntry = {
key: MetricStatus.Failure,
value: 1,
unit: Unit.Count,
};
const emf = buildEMFObject(
"amendment-event-transformer",
dimensions,
metric,
);
logger.info(emf);
}
}
1 change: 0 additions & 1 deletion lambdas/api-handler/src/handlers/get-letters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { mapToGetLettersResponse } from "../mappers/letter-mapper";
import type { Deps } from "../config/deps";
import { MetricStatus, emitForSingleSupplier } from "../utils/metrics";

// List letters Handlers
// The endpoint should only return pending letters for now
const status = "PENDING";

Expand Down
14 changes: 10 additions & 4 deletions lambdas/api-handler/src/handlers/patch-letter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default function createPatchLetterHandler(
try {
patchLetterRequest = PatchLetterRequestSchema.parse(JSON.parse(body));
} catch (error) {
emitErrorMetric(metrics, supplierId);
const typedError =
error instanceof Error
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
Expand All @@ -79,6 +80,7 @@ export default function createPatchLetterHandler(
);

if (updateLetterCommand.id !== letterId) {
emitErrorMetric(metrics, supplierId);
throw new ValidationError(
ApiErrorDetail.InvalidRequestLetterIdsMismatch,
);
Expand All @@ -100,12 +102,16 @@ export default function createPatchLetterHandler(
body: "",
};
} catch (error) {
metrics.putDimensions({
supplier: supplierId,
});
metrics.putMetric(MetricStatus.Success, 1, Unit.Count);
emitErrorMetric(metrics, supplierId);
return processError(error, commonIds.value.correlationId, deps.logger);
}
};
});
}

function emitErrorMetric(metrics: MetricsLogger, supplierId: string) {
metrics.putDimensions({
supplier: supplierId,
});
metrics.putMetric(MetricStatus.Failure, 1, Unit.Count);
}
186 changes: 99 additions & 87 deletions lambdas/api-handler/src/handlers/post-letters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { APIGatewayProxyHandler } from "aws-lambda";
import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics";
import { Unit } from "aws-embedded-metrics";
import pino from "pino";
import type { Deps } from "../config/deps";
import { ApiErrorDetail } from "../contracts/errors";
import {
Expand All @@ -13,7 +14,7 @@ import { mapToUpdateCommands } from "../mappers/letter-mapper";
import { enqueueLetterUpdateRequests } from "../services/letter-operations";
import { extractCommonIds } from "../utils/common-ids";
import { assertNotEmpty, requireEnvVar } from "../utils/validation";
import { MetricStatus } from "../utils/metrics";
import { MetricEntry, MetricStatus, buildEMFObject } from "../utils/metrics";

function duplicateIdsExist(postLettersRequest: PostLettersRequest) {
const ids = postLettersRequest.data.map((item) => item.id);
Expand All @@ -23,17 +24,23 @@ function duplicateIdsExist(postLettersRequest: PostLettersRequest) {
/**
* emits metrics of successful letter updates, including the supplier and grouped by status
*/
function emitMetics(
metrics: MetricsLogger,
function emitSuccessMetrics(
supplierId: string,
statusesMapping: Map<string, number>,
logger: pino.Logger,
) {
for (const [status, count] of statusesMapping) {
metrics.putDimensions({
const dimensions: Record<string, string> = {
supplier: supplierId,
eventType: status,
});
metrics.putMetric(MetricStatus.Success, count, Unit.Count);
status,
};
const metric: MetricEntry = {
key: MetricStatus.Success,
value: count,
unit: Unit.Count,
};
const emf = buildEMFObject("postLetters", dimensions, metric);
logger.info(emf);
}
}

Expand All @@ -48,92 +55,97 @@ function populateStatusesMap(updateLetterCommands: UpdateLetterCommand[]) {
export default function createPostLettersHandler(
deps: Deps,
): APIGatewayProxyHandler {
return metricScope((metrics: MetricsLogger) => {
return async (event) => {
const commonIds = extractCommonIds(
event.headers,
event.requestContext,
deps,
);
return async (event) => {
const commonIds = extractCommonIds(
event.headers,
event.requestContext,
deps,
);

if (!commonIds.ok) {
return processError(
commonIds.error,
commonIds.correlationId,
deps.logger,
);
}
if (!commonIds.ok) {
return processError(
commonIds.error,
commonIds.correlationId,
deps.logger,
);
}

const maxUpdateItems = requireEnvVar(deps.env, "MAX_LIMIT");
requireEnvVar(deps.env, "QUEUE_URL");
const maxUpdateItems = requireEnvVar(deps.env, "MAX_LIMIT");
requireEnvVar(deps.env, "QUEUE_URL");

const { supplierId } = commonIds.value;
metrics.setNamespace(
process.env.AWS_LAMBDA_FUNCTION_NAME || "postLetters",
const { supplierId } = commonIds.value;
try {
const body = assertNotEmpty(
event.body,
new ValidationError(ApiErrorDetail.InvalidRequestMissingBody),
);

let postLettersRequest: PostLettersRequest;

try {
const body = assertNotEmpty(
event.body,
new ValidationError(ApiErrorDetail.InvalidRequestMissingBody),
);
postLettersRequest = PostLettersRequestSchema.parse(JSON.parse(body));
} catch (error) {
const typedError =
error instanceof Error
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
cause: error,
})
: error;
throw typedError;
}

let postLettersRequest: PostLettersRequest;

try {
postLettersRequest = PostLettersRequestSchema.parse(JSON.parse(body));
} catch (error) {
const typedError =
error instanceof Error
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
cause: error,
})
: error;
throw typedError;
}

deps.logger.info({
description: "Received post letters request",
supplierId: commonIds.value.supplierId,
letterIds: postLettersRequest.data.map((letter) => letter.id),
correlationId: commonIds.value.correlationId,
});

if (postLettersRequest.data.length > maxUpdateItems) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestLettersToUpdate,
{ args: [maxUpdateItems] },
);
}

if (duplicateIdsExist(postLettersRequest)) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestDuplicateLetterId,
);
}

const updateLetterCommands: UpdateLetterCommand[] = mapToUpdateCommands(
postLettersRequest,
supplierId,
);
const statusesMapping = populateStatusesMap(updateLetterCommands);
await enqueueLetterUpdateRequests(
updateLetterCommands,
commonIds.value.correlationId,
deps,
deps.logger.info({
description: "Received post letters request",
supplierId: commonIds.value.supplierId,
letterIds: postLettersRequest.data.map((letter) => letter.id),
correlationId: commonIds.value.correlationId,
});

if (postLettersRequest.data.length > maxUpdateItems) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestLettersToUpdate,
{ args: [maxUpdateItems] },
);
}

emitMetics(metrics, supplierId, statusesMapping);
return {
statusCode: 202,
body: "",
};
} catch (error) {
metrics.putDimensions({
supplier: supplierId,
});
metrics.putMetric(MetricStatus.Failure, 1, Unit.Count);
return processError(error, commonIds.value.correlationId, deps.logger);
if (duplicateIdsExist(postLettersRequest)) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestDuplicateLetterId,
);
}
};
});

const updateLetterCommands: UpdateLetterCommand[] = mapToUpdateCommands(
postLettersRequest,
supplierId,
);
const statusesMapping = populateStatusesMap(updateLetterCommands);
await enqueueLetterUpdateRequests(
updateLetterCommands,
commonIds.value.correlationId,
deps,
);

emitSuccessMetrics(supplierId, statusesMapping, deps.logger);
return {
statusCode: 202,
body: "",
};
} catch (error) {
// error metrics
emitErrorMetrics(supplierId, deps.logger);

return processError(error, commonIds.value.correlationId, deps.logger);
}
};
}

function emitErrorMetrics(supplierId: string, logger: pino.Logger) {
const dimensions: Record<string, string> = { supplier: supplierId };
const metric: MetricEntry = {
key: MetricStatus.Failure,
value: 1,
unit: Unit.Count,
};
const emf = buildEMFObject("postLetters", dimensions, metric);
logger.info(emf);
}
Loading
Loading