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
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
16 changes: 15 additions & 1 deletion lambdas/api-handler/src/handlers/letter-status-update.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { SQSEvent, SQSHandler } from "aws-lambda";
import { SQSEvent, SQSHandler, SQSRecord } from "aws-lambda";
import { Unit } from "aws-embedded-metrics";
import pino from "pino";
import {
UpdateLetterCommand,
UpdateLetterCommandSchema,
} from "../contracts/letters";
import { Deps } from "../config/deps";
import { mapToUpdateLetter } from "../mappers/letter-mapper";
import { MetricEntry, buildEMFObject } from "../utils/metrics";

export default function createLetterStatusUpdateHandler(
deps: Deps,
Expand All @@ -31,9 +34,20 @@ export default function createLetterStatusUpdateHandler(
correlationId: message.messageAttributes.CorrelationId.stringValue,
messageBody: message.body,
});
emitAndFlushMetricLog(message, deps.logger);
}
});

await Promise.all(tasks);
};
}

function emitAndFlushMetricLog(message: SQSRecord, logger: pino.Logger) {
const metric: MetricEntry = {
key: "statusUpdateFailed",
value: 1,
unit: Unit.Count,
};
const emf = buildEMFObject("letter-status-update", {}, metric);
logger.info(emf);
}
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: "Letters posted",
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);
}
13 changes: 6 additions & 7 deletions lambdas/api-handler/src/handlers/post-mi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { APIGatewayProxyHandler } from "aws-lambda";
import { MetricsLogger, metricScope } from "aws-embedded-metrics";
import postMIOperation from "../services/mi-operations";

Check warning on line 3 in lambdas/api-handler/src/handlers/post-mi.ts

View workflow job for this annotation

GitHub Actions / Test stage / Linting

Caution: `mi-operations.ts` has a default export `postMI`. This imports `postMI` as `postMIOperation`. Check if you meant to write `import postMI from '../services/mi-operations'` instead
import { ApiErrorDetail } from "../contracts/errors";
import ValidationError from "../errors/validation-error";
import { processError } from "../mappers/error-mapper";
Expand Down Expand Up @@ -42,6 +42,7 @@
try {
postMIRequest = PostMIRequestSchema.parse(JSON.parse(body));
} catch (error) {
emitErrorMetric(metrics, supplierId);
const typedError =
error instanceof Error
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
Expand Down Expand Up @@ -86,15 +87,13 @@
body: JSON.stringify(result, null, 2),
};
} catch (error) {
emitForSingleSupplier(
metrics,
"postMi",
supplierId,
1,
MetricStatus.Failure,
);
emitErrorMetric(metrics, supplierId);
return processError(error, commonIds.value.correlationId, deps.logger);
}
};
});
}

function emitErrorMetric(metrics: MetricsLogger, supplierId: string) {
emitForSingleSupplier(metrics, "postMi", supplierId, 1, MetricStatus.Failure);
}
33 changes: 33 additions & 0 deletions lambdas/api-handler/src/utils/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,36 @@ export enum MetricStatus {
Success = "success",
Failure = "failure",
}

export interface MetricEntry {
key: string;
value: number;
unit: Unit;
}

// build EMF object
export function buildEMFObject(
functionName: string,
dimensions: Record<string, string>,
metric: MetricEntry,
) {
const namespace = process.env.AWS_LAMBDA_FUNCTION_NAME || functionName;
return {
LogGroup: namespace,
ServiceName: namespace,
...dimensions,
_aws: {
Timestamp: Date.now(),
CloudWatchMetrics: [
{
Namespace: namespace,
Dimensions: [...Object.keys(dimensions), "ServiceName", "LogGroup"],
Metrics: [
{ Name: metric.key, Value: metric.value, Unit: metric.unit },
],
},
],
},
[metric.key]: metric.value,
};
}
Loading
Loading