Как DevStride переходит с Datadog на AWS X-Ray и Cloudwatch для наблюдения

Dec 02 2022
Более экономичный способ мониторинга вашего приложения
DevStride имеет более 200 лямбда-функций, которые нам нужны, чтобы убедиться, что они работают должным образом и работают правильно. Нам нужен инструмент, чтобы дать нам представление о том, как работает наша инфраструктура, и оценить общее состояние системы.

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.

Если вы хотите узнать больше о нашей архитектуре, в этом репозитории описаны многие шаблоны, которые мы используем.

Я планирую написать больше, так что следите за мной в Твиттере, пока я продолжаю писать о наших следующих шагах и других решениях, которые нам пришлось придумать.