Первый взгляд на Lambda Powertools TypeScript я сделал еще в январе 2022 года. В то время я был очень рад этой библиотеке, но она сопровождалась предупреждением, что еще не готова к использованию в производстве. Объявление об общей доступности появилось 15 июля, так что пришло время взглянуть еще раз.
Оглавление
- Что изменилось?
- Поддержка ES-модулей
- Сравнения
- Логгер
- Метрики
- Трассировщик
- Дорожная карта
- Заключение
Что изменилось
Итак, что же изменилось в бета-версии? Взглянув на CHANGELOG, можно ответить, что за шесть месяцев изменилось не так уж много. Lambda Powertools TypeScript по-прежнему поддерживает декораторы классов, middy и ручной API. Он по-прежнему охватывает основные возможности протоколирования, метрики и трассировки, и никаких новых возможностей добавлено не было. За исключением некоторых исправлений ошибок и оптимизации, это все еще очень похожая библиотека, которую я предварительно рассматривал в январе.
Поддержка ES-модулей
Одним из изменений, которое я хотел бы увидеть, является поддержка ES-модулей. Интерес к ES Modules быстро растет в сообществе serverless, в основном из-за желания использовать Top-Level Await.
Lambda Powertools TypeScript не может быть использован напрямую как зависимость ES Modules, но вопрос открыт, поэтому, пожалуйста, подумайте о добавлении своего +1. Пока что можно обойтись require shim или трюками cjs, но было бы здорово увидеть встроенную поддержку ES Modules в Lambda Powertools TypeScript.
Сравнения
Учитывая, что я уже рассматривал эти модули в предыдущем посте, я решил сравнить модули Lambda Powertools TypeScript с аналогичными решениями. Я смотрю на API, размер поставляемого скрипта, холодный старт и время выполнения. Для сбора метрик я написал небольшое приложение с использованием Step Functions, которое может запускать множество экземпляров функции параллельно и снимать метрики.
Мой инструмент бенчмаркинга будет запускать каждую функцию 50 раз, стремясь достичь 20-процентного показателя холодного запуска. Примеры кода доступны на GitHub.
Логгер
Самый простой способ вести журнал в CloudWatch из AWS Lambda — это console.log
. Это не увеличивает объем вашей функции, отсутствует управление зависимостями, а запись в CloudWatch происходит асинхронно, поэтому операция не блокируется. Многие разработчики используют библиотеки для обеспечения структурированного формата журналов и контроля за многословностью протоколирования.
Тем не менее, мы можем использовать console
в качестве базового уровня из-за его простоты. Если нам нужна функция, которая просто пишет неструктурированные журналы, мы можем сделать что-то вроде этого.
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
Context,
} from 'aws-lambda';
export const handler = async (
event: APIGatewayProxyEventV2,
context: Context
): Promise<APIGatewayProxyResultV2> => {
console.log('event: ', event);
console.log('context: ', context);
return { statusCode: 200 };
};
Выход из журнала событий дает строгированный контекстный объект:
2022-07-19T12:10:45.503Z 2abe532e-2b26-46b5-9a65-884363160556 INFO context: {
callbackWaitsForEmptyEventLoop: [Getter/Setter],
succeed: [Function (anonymous)],
fail: [Function (anonymous)],
done: [Function (anonymous)],
functionVersion: '$LATEST',
functionName: 'LoggerConsole',
memoryLimitInMB: '128',
logGroupName: '/aws/lambda/LoggerConsole',
logStreamName: '2022/07/19/[$LATEST]384dbd25ffeb4af49bc22c2ac4f333df',
clientContext: undefined,
identity: undefined,
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456790:function:LoggerConsole',
awsRequestId: '2abe532e-2b26-46b5-9a65-884363160556',
getRemainingTimeInMillis: [Function: getRemainingTimeInMillis]
}
Это довольно шумно, и наличие этих методов succeed, fail и done не дает большой ценности.
С помощью Powertools мы можем добавить более полезный контекст в сообщения журнала.
import { Logger } from '@aws-lambda-powertools/logger';
import type { LambdaInterface } from '@aws-lambda-powertools/commons';
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
const logger = new Logger();
class Lambda implements LambdaInterface {
@logger.injectLambdaContext({ logEvent: true })
public async handler(
_event: APIGatewayProxyEventV2,
_context: Context
): Promise<void> {
logger.info('Here is some info!');
}
}
export const myFunction = new Lambda();
export const handler = myFunction.handler;
Теперь мы получаем структурированные журналы.
{
"cold_start": false,
"function_arn": "arn:aws:lambda:us-east-1:123456790:function:LoggerPowertools",
"function_memory_size": 128,
"function_name": "LoggerPowertools",
"function_request_id": "6eeaa0c9-e58f-45a7-bed2-a4b9d7e65d7e",
"level": "INFO",
"message": "Here is some info!",
"service": "service_undefined",
"timestamp": "2022-07-19T12:09:28.537Z",
"xray_trace_id": "1-62d69ef6-dfdbc4be0f59a76c57c52cf8"
}
Это намного проще для поиска и не включает бесполезные структурированные методы. Кроме того, мы получаем булеву cold_start.
Для сравнения я привел еще одну реализацию этой функции с использованием популярной и долговечной библиотеки winston. Давайте посмотрим, что у них получилось.
Функция | Avg Cold Start | Avg Duration | Размер кода |
---|---|---|---|
LoggerConsole | 137.24 | 1.63 | 771 |
LoggerPowertools | 157.58 | 1.86 | 78550 |
LoggerWinston | 184.17 | 1.62 | 233142 |
Версия без зависимостей всегда будет самой быстрой. Powertools добавляет около 78 кб, в то время как winston намного тяжелее — 232 кб. В любом случае мы не добавляем много задержки, но Powertools меньше и, следовательно, быстрее, и это дает нам метрику cold_start
.
Метрики
Часто, когда речь заходит о метриках, мы думаем о CPU, задержках и других рабочих метриках, и сервисы AWS обычно предоставляют их из коробки. Такое мышление может быть ошибочным, когда в итоге нам приходится использовать сторонние сервисы, такие как google analytics, для вывода критических бизнес-событий. Более простое решение заключается в том, чтобы приложение выдавало метрику при наступлении бизнес-события (например, регистрации клиента). У нас есть несколько вариантов, как это сделать: Мы можем использовать aws-sdk, мы можем использовать aws-embedded-metrics lib и теперь мы можем использовать Powertools Metrics. Какой вариант лучше? Давайте посмотрим.
Чтобы определить базовый уровень, давайте воспользуемся функцией, которая не испускает метрики.
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
export const handler = async (
_event: APIGatewayProxyEventV2,
_context: Context
): Promise<void> => {
const workflowSuccess = Math.random() > 0.5;
if (workflowSuccess) {
console.log('The workflow was successful!');
} else {
console.log('The workflow failed.');
}
};
Чтобы понять, успешен ли наш рабочий процесс, нам потребуется запросить журналы. Уф!
Давайте попробуем выдать метрики с помощью библиотеки @aws-sdk/client-cloudwatch
из aws-sdk-v3.
import {
CloudWatchClient,
MetricDatum,
PutMetricDataCommand,
} from '@aws-sdk/client-cloudwatch';
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
const client = new CloudWatchClient({});
export const handler = async (
_event: APIGatewayProxyEventV2,
_context: Context
): Promise<void> => {
const workflowSuccess = Math.random() > 0.5;
let metric: MetricDatum;
if (workflowSuccess) {
console.log('The workflow was successful!');
metric = { MetricName: 'WorkflowSuccess', Value: 1, Unit: 'Count' };
} else {
console.log('The workflow failed.');
metric = { MetricName: 'WorkflowFailure', Value: 1, Unit: 'Count' };
}
const command = new PutMetricDataCommand({
MetricData: [metric],
Namespace: 'SdkV3Metrics',
});
await client.send(command);
};
Теперь у нас есть несколько хороших метрик в CloudWatch!
Недостатком использования aws-sdk для этого является то, что он полагается на вызовы API и является несколько медленным. Мы можем попытаться добиться того же, используя aws-embedded-metrics. Чем это отличается от пользовательских метрик Cloudwatch? Она лучше, и коллега из Community Builder Вишну Прассад расскажет вам почему.
import { createMetricsLogger, Unit } from 'aws-embedded-metrics';
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
export const handler = async (
_event: APIGatewayProxyEventV2,
_context: Context
): Promise<void> => {
const workflowSuccess = Math.random() > 0.5;
const metrics = createMetricsLogger();
metrics.putDimensions({ Service: 'EMF' });
if (workflowSuccess) {
metrics.putMetric('WorkflowSuccess', 1, Unit.Count);
} else {
metrics.putMetric('WorkflowFailure', 1, Unit.Count);
}
await metrics.flush();
};
Помимо преимущества EMF, код стал немного лаконичнее.
import { LambdaInterface } from '@aws-lambda-powertools/commons';
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
const metrics = new Metrics({ namespace: 'Workflow' });
class Lambda implements LambdaInterface {
@metrics.logMetrics()
public async handler(
_event: APIGatewayProxyEventV2,
_context: Context
): Promise<void> {
const workflowSuccess = Math.random() > 0.5;
if (workflowSuccess) {
metrics.addMetric('WorkflowSuccess', MetricUnits.Count, 1);
} else {
metrics.addMetric('WorkflowFailure', MetricUnits.Count, 1);
}
}
}
export const myFunction = new Lambda();
export const handler = myFunction.handler;
Версия Powertools чуть более многословна из-за необходимости использовать декораторы классов, но все равно неплоха. В конечном итоге нас больше волнует производительность, поэтому давайте проверим эти цифры.
Функция | Avg Cold Start | Avg Duration | Размер кода |
---|---|---|---|
МетрикаНет | 135.25 | 0.93 | 826 |
MetricsEMF | 146.75 | 1.33 | 29456 |
MetricsSDKV3 | 225.37 | 32 | 257026 |
MetricsPowertools | 141.3 | 1.33 | 7942 |
Я мог бы подумать, что реализация Powertools здесь обернет aws-embedded-metrics, но, очевидно, это не так! Хотя фактическое преимущество Powertools в производительности по сравнению с aws-embedded-metrics незначительно, вы должны оценить, как они уменьшили размер.
Tracer
В случае с модулем Tracer, он действительно обернут aws-xray-sdk. Так почему же мы должны использовать Tracer вместо него? Если API приятнее, и он не добавляет много задержек, то это может стоить того. Вот пример использования aws-xray-sdk. В этом случае лямбда-функция отслеживает отдельную функцию, а также вызов SDK для (несколько бесполезного) получения свойств функции.
import {
GetFunctionCommand,
GetFunctionCommandOutput,
LambdaClient,
} from '@aws-sdk/client-lambda';
import { captureAsyncFunc, captureAWSv3Client } from 'aws-xray-sdk-core';
import type { Context } from 'aws-lambda';
const client = new LambdaClient({});
captureAWSv3Client(client);
const getFunction = async (
context: Context
): Promise<GetFunctionCommandOutput> => {
const command = new GetFunctionCommand({
FunctionName: context.functionName,
});
return client.send(command);
};
export const handler = (
_event: unknown,
context: Context
): Promise<GetFunctionCommandOutput> =>
captureAsyncFunc('methodWithCustomTrace', async (subsegment) => {
const fn = await getFunction(context);
subsegment?.close();
return fn;
});
Эта часть captureAsyncFunc
немного неудобна. Как Powertools делает это?
import { LambdaInterface } from '@aws-lambda-powertools/commons';
import { Tracer } from '@aws-lambda-powertools/tracer';
import {
GetFunctionCommand,
GetFunctionCommandOutput,
LambdaClient,
} from '@aws-sdk/client-lambda';
import type { Context } from 'aws-lambda';
const client = new LambdaClient({});
const tracer = new Tracer({ serviceName: 'powertoolsTracer' });
tracer.captureAWSv3Client(client);
class Lambda implements LambdaInterface {
@tracer.captureMethod()
public async methodWithCustomTrace(
context: Context
): Promise<GetFunctionCommandOutput> {
const command = new GetFunctionCommand({
FunctionName: context.functionName,
});
return client.send(command);
}
@tracer.captureLambdaHandler()
public async handler(
_event: unknown,
context: Context
): Promise<GetFunctionCommandOutput> {
return this.methodWithCustomTrace(context);
}
}
export const handlerClass = new Lambda();
export const handler = handlerClass.handler;
Я действительно начинаю относиться к декораторам классов для подобных случаев. Декораторы функций были бы лучше, но, как уже говорилось, в TypeScript их пока не существует.
Функция | Avg Cold Start | Avg Duration | Размер кода |
---|---|---|---|
TracerXRay | 266.91 | 50.56 | 410514 |
TracerPowertools | 265.42 | 48.25 | 417980 |
Проверяя показатели производительности, можно заметить, что Powertools очень мал, его вес составляет около 7 кб, и он практически не влияет на производительность. В данном случае Powertools немного быстрее, но я подозреваю, что в долгосрочной перспективе это обойдется в пару мс, так что это стоит того ради devexp.
Дорожная карта
И последнее, что следует учесть при принятии решения о внедрении Lambda Powertools, это то, какие функции могут появиться в будущем. Более зрелые библиотеки Lambda Powertools Python и Lambda Powertools Java включают ряд полезных утилит, которые мы, возможно, хотели бы видеть в Lambda Powertools TypeScript. Общая дорожная карта Lambda Powertools Roadmap не говорит нам многого, кроме ожидаемых библиотек dotnet и golang, но мы можем углубиться в специфические для TypeScript вопросы и заглянуть в ближайшее будущее. Похоже, что стабильность по-прежнему является главным приоритетом, но есть и несколько интересных пунктов, например RFC: Testing Factories for AWS Data Objects в крайней левой колонке.
Участие сообщества и голосование, несомненно, помогут в разработке дорожной карты Powertools.
Заключение
Для меня это не имеет смысла. Одна из проблем при написании Lambda заключается в том, что многие из наших зависимостей не были предназначены для Lambda. Эта библиотека, очевидно, была такой, и команда явно позаботилась о том, чтобы предоставить максимальную ценность в минимальном пакете. Эти основные утилиты продвигают лучшие практики в доступной и простой для использования форме. Пишете ли вы на TypeScript или JavaScript, вы можете наслаждаться хорошей поддержкой IDE и высокоуровневым API для реализации протоколирования, метрик и трассировки в Lambda.
ОБЛОЖКА