Как DevStride переходит с Datadog на AWS X-Ray и Cloudwatch для наблюдения
DevStride имеет более 200 лямбда-функций, которые нам нужны, чтобы убедиться, что они работают должным образом и работают правильно. Нам нужен инструмент, чтобы дать нам представление о том, как работает наша инфраструктура, и оценить общее состояние системы.
Что нам нужно?
Наш вариант использования очень прост, и нам в основном нужно ответить на пару вопросов:
- Какова наша общая задержка?
- Каково наше использование?
- Есть ли у нас ошибки в производстве?
- Получайте оповещения в Slack о любой новой проблеме, которая появляется.
- Упростите отслеживание ошибок и нахождение основной причины.
- Сопоставьте все журналы от начала до конца выполнения (по всем функциям).
Datadog — отличный сервис с множеством функций — значительно больше, чем нам сейчас нужно. В основном мы использовали APM, Logs и RUM.
Несмотря на то, что Datadog предоставляет отличный способ добиться того, что нам нужно, стоимость является основной причиной, по которой мы переезжаем. В частности, стоимость APM для бессерверных решений , которая на данный момент составляет 6 долларов США в месяц за функцию + дополнительные расходы в зависимости от использования.
Чем мы заменяем Datadog?
Как упоминалось ранее, DevStride использует бессерверную инфраструктуру, и у нас есть более 200 лямбда-функций, которые мы должны развернуть и отслеживать.
В DevStride мы используем SST , который является абстракцией AWS CDK . Для наших определений инфраструктуры и кода функций мы используем TypeScript.
Мы хотим заменить наш текущий агент Datadog на AWS X-Ray и CloudWatch , а для этого требуется несколько изменений в нашем функциональном коде и инфраструктуре.
Функции настройки
Чтобы сделать наш переход максимально простым, мы решили использовать новый AWS Lambda PowerTools . Этот пакет упрощает некоторые настройки и очень полезен.
Из электроинструментов получаем:
- Трассировщик , который мы можем использовать для аннотирования трасс
- Регистратор для аннотирования журналов нашими трассировками и полезными параметрами .
- Промежуточное ПО для автоматической аннотации трассировок/журналов
- Помощники для добавления пользовательских метрик
Мы создаем трассировщик и регистратор и раскрываем функции, которые мы можем использовать в нашем приложении.
import { Tracer } from '@aws-lambda-powertools/tracer';
import { Logger } from '@aws-lambda-powertools/logger';
import { Config } from '@serverless-stack/node/config';
let tracer: Tracer;
let logger: Logger;
// Only initiate the tracer in production
// We call Config.APP to make sure that the tracer is initiated after
// all the secrets are loaded from SST
if (process.env.NODE_ENV === 'production' && Config.APP) {
tracer = new Tracer();
logger = new Logger();
tracer.provider.setLogger(logger);
}
// Annotate the trace with metadata
export function addTags(tags: { [key: string]: any }) {
if (!tracer) {
return;
}
for (const key of Object.keys(tags)) {
tracer.putMetadata(key, tags[key]);
}
}
// Annotate the trace with user metadata
export function setUser(authUser: AuthUser) {
if (!tracer) {
return;
}
tracer.putAnnotation('user_email', authUser.email);
tracer.putAnnotation('user_username', authUser.username);
tracer.putAnnotation('user_name', authUser.name);
tracer.putAnnotation('user_id', authUser.userId);
tracer.putAnnotation('user_status', authUser.status);
}
export function getTracer() {
return tracer;
}
export function getLogger() {
return logger;
}
В DevStride мы используем middy для промежуточного программного обеспечения и абстрагируемся с помощью классов. Это пример базового лямбда-обработчика, который расширяется всеми остальными обработчиками.
export abstract class LambdaBaseHandler<Event, Response> {
constructor(
protected logger: Logger,
protected middlewares?: Array<MiddlewareObj>
) {}
make(): MiddyfiedHandler<Event, Response> {
const mdw = [];
const tracer = getTracer();
if (process.env.NODE_ENV === 'production' && tracer) {
// Add the tracer middleware
mdw.push(captureLambdaHandler(tracer, { captureResponse: false }));
// Add the logger middleware
mdw.push(injectLambdaContext(logger));
}
mdw.push(
...(this.middlewares || []),
);
return middy(this.execute.bind(this)).use(mdw);
}
//
protected execute(event: Event): Promise<Response> {
this.logger.setContext(`${this.constructor.name}:execute`);
return this.handle(event);
}
// When you extend this class you implement this handler with your logic
protected abstract handle(event: Event): Promise<Response>;
}
Настройка инфраструктуры
Мы хотим максимально упростить настройку функций, поэтому добавили необходимые конфигурации в переменные окружения. Чтобы сделать это быстро, мы создали функцию utility
, которая делает это для всех наших функций. Кроме того, мы хотим иметь возможность прослушивать все журналы, чтобы мы могли отправлять оповещения в Slack, когда находим журналы ошибок.
Во-первых, мы настраиваем стек, который будет развертывать нашу лямбда-функцию и прослушивать наши журналы.
import * as sst from "@serverless-stack/resources";
import { StackContext } from "@serverless-stack/resources";
import { CfnPermission } from "aws-cdk-lib/aws-lambda";
export function MonitoringStack({ stack }: StackContext) {
const errorLogHandler = new sst.Function(stack, `ErrorLogHandler`, {
handler:
"src/check-error-logs.handler",
bind: [
// slack instructions https://api.slack.com/messaging/webhooks
new sst.Config.Parameter(stack, "SLACK_WEBHOOK_URL", {
value: process.env.SLACK_WEBHOOK_URL || "",
}),
],
});
const permission = new CfnPermission(
stack,
"ErrorLogLambdaPermission",
{
action: "lambda:InvokeFunction",
functionName: errorLogHandler.functionArn,
principal: "logs.amazonaws.com",
}
);
return {
errorLogHandler,
permission,
};
}
export function addInstrumentation(stack: sst.Stack, module: string) {
if (!process.env.IS_LOCAL) {
const { errorLogHandler, permission } = use(MonitoringStack);
deferredFunctions.push(() => {
stack.getAllFunctions().forEach((f) => {
const sf = new logs.SubscriptionFilter(stack, f.id + "Subscription", {
logGroup: f.logGroup,
destination: new destinations.LambdaDestination(errorLogHandler, {
addPermissions: false,
}),
// the filter pattern depends on what your logs look like
filterPattern: logs.FilterPattern.anyTerm("ERROR", "error"),
});
// this is a hack to make cloudformation wait for the permissions to be set
sf.node.addDependency(permission);
f.addEnvironment("POWERTOOLS_SERVICE_NAME", module);
f.addEnvironment("POWERTOOLS_METRICS_NAMESPACE", module);
});
});
}
}
export function MyModuleStack({ stack }: StackContext) {
const apiStack = use(ApiStack);
apiStack.api.addRoutes(stack, {
"GET /": {
function: "src/myFunc.handler",
},
});
addInstrumentation(stack, "myModule");
}
Мы также добавляем сохранение журнала по умолчанию и настройку трассировки для всех наших функций.
app.setDefaultFunctionProps({
tracing: process.env.IS_LOCAL ? "disabled" : "active",
logRetention: "two_weeks",
// we had to do this because we have a lot of lambda functions
// and we reached rate limiting issues in cloudformation
logRetentionRetryOptions: {
maxRetries: 12,
base: Duration.millis(500),
},
})

Кроме того, вы увидите все детали трассировки, а также список трассировок.

С приведенным выше кодом у нас есть то, что нам нужно для мониторинга наших трассировок и общего использования. Мы также можем знать, если что-то идет не так; traces покажет все ошибки и места их возникновения. Кроме того, трассировки группируют журналы вместе.
Отправка оповещений в Slack
Одним из наших требований было то, что нам нужно было получать уведомления об ошибках и найти простой способ просмотра трассировки/журналов ошибок.
Мы решили, что самый простой способ сделать это для нас — отправить журналы ошибок в Slack.
В DevStride мы используем CQRS , и все наши обработчики вызывают команды/запросы, которые выполняют бизнес-логику.
Вот наш обработчик журналов ошибок:
export class CheckErrorLogsLambdaHandler extends LambdaBaseHandler<
CloudWatchLogsEvent,
boolean
> {
constructor(logger: Logger) {
super(logger);
}
protected async handle(event: CloudWatchLogsEvent): Promise<boolean> {
const payload = JSON.parse(
zlib.unzipSync(Buffer.from(event.awslogs.data, 'base64')).toString()
);
const logs = payload.logEvents.map((e: any) => {
try {
const data = JSON.parse(e.message.replaceAll('\n', ''));
return {
message: data.message,
service: data.service,
timestamp: data.timestamp,
xrayTraceId: data.xray_trace_id,
correlationId: data.correlationId,
context: data.context,
};
} catch {
return {
message: e.message,
};
}
});
const response: Result<boolean> = await CommandBus.execute(
new CheckErrorLogsCommand({
correlationId: this.getCorrelationId(event),
logs: logs,
logGroup: payload.logGroup,
logStream: payload.logStream,
})
);
if (response.isErr) {
throw response.error;
}
return response.unwrap();
}
}
export class CheckErrorLogsService extends CommandHandlerBase<CheckErrorLogsCommand> {
constructor(logger: Logger) {
super(CheckErrorLogsCommand, logger);
}
async handle(
command: CheckErrorLogsCommand
): Promise<Result<boolean, Error>> {
const logGroupUrl = `https://${
process.env.REGION
}.console.aws.amazon.com/cloudwatch/home?region=${
process.env.REGION
}#logsV2:log-groups/log-group/${encodeURIComponent(command.logGroup)}`;
const logStreamUrl = `https://${
process.env.REGION
}.console.aws.amazon.com/cloudwatch/home?region=${
process.env.REGION
}#logsV2:log-groups/log-group/${encodeURIComponent(
command.logGroup
)}/log-events/${encodeURIComponent(command.logStream)}`;
const errorBlocks = [];
for (const log of command.logs) {
const traceUrl = `https://${process.env.REGION}.console.aws.amazon.com/cloudwatch/home?region=${process.env.REGION}#xray:traces/${log.xrayTraceId}`;
if (!log.service) {
errorBlocks.push(
...[
{
color: '#D13213',
blocks: [
{
type: 'divider',
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: log.message,
},
},
],
},
]
);
}
errorBlocks.push(
...[
{
color: '#D13213',
blocks: [
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*TraceId:*\n<${traceUrl}|${log.xrayTraceId}>`,
},
{
type: 'mrkdwn',
text: `*CorrelationId:*\n${log.correlationId}`,
},
],
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*Environment:*\n${process.env.STAGE}`,
},
{
type: 'mrkdwn',
text: `*Service:*\n${log.service}`,
},
],
},
{
type: 'section',
text: {
text: `*Context:* ${log.context}`,
type: 'mrkdwn',
},
},
{
type: 'section',
text: {
text: '*Message:*\n' + '```' + log.message + '```',
type: 'mrkdwn',
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'Go to trace',
emoji: true,
},
url: traceUrl,
},
],
},
],
},
]
);
}
const functionName = command.logGroup.replace('/aws/lambda/', '');
await axios.post(Config.SLACK_WEBHOOK_URL, {
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `${functionName} error logs`,
emoji: true,
},
},
],
attachments: [
{
color: '#D13213',
blocks: [
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*LogGroup:*\n<${logGroupUrl}|${command.logGroup}>`,
},
{
type: 'mrkdwn',
text: `*LogStream:*\n<${logStreamUrl}|${command.logStream}>`,
},
],
},
],
},
...errorBlocks,
],
});
return Result.ok(true);
}
}

Что дальше?
Есть и другие улучшения, которые, я думаю, могли бы помочь нам улучшить наш мониторинг, например:
- Добавьте больше показателей
- Аннотируйте трассы дополнительными данными
- Аннотировать журналы с большим количеством данных
- Добавьте больше логики в обработчик журналов ошибок, чтобы отправлять только новые ошибки в Slack.
Если вы хотите узнать больше о нашей архитектуре, в этом репозитории описаны многие шаблоны, которые мы используем.
Я планирую написать больше, так что следите за мной в Твиттере, пока я продолжаю писать о наших следующих шагах и других решениях, которые нам пришлось придумать.