Bagaimana DevStride berpindah dari Datadog ke AWS X-Ray dan Cloudwatch agar dapat diamati
DevStride memiliki lebih dari 200 fungsi lambda yang perlu kami pastikan berjalan seperti yang diharapkan dan bekerja dengan benar. Kami membutuhkan alat untuk memberi kami gambaran umum tentang kinerja infrastruktur kami dan menilai kesehatan sistem secara keseluruhan.
Apa yang kita butuhkan?
Kasus penggunaan kami sangat sederhana dan kami terutama perlu menjawab beberapa pertanyaan:
- Apa latency kita secara keseluruhan?
- Apa kegunaan kita?
- Apakah kami memiliki kesalahan dalam produksi?
- Dapatkan peringatan di Slack untuk setiap masalah baru yang muncul.
- Permudah pelacakan kesalahan dan temukan akar masalahnya.
- Korelasikan semua log dari awal hingga akhir eksekusi (di semua fungsi).
Datadog adalah layanan hebat dengan banyak fitur - jauh lebih banyak dari yang kami butuhkan saat ini. Kami terutama menggunakan APM, Log, dan RUM.
Meskipun Datadog menyediakan cara yang bagus untuk mencapai apa yang kami butuhkan, biaya adalah alasan utama kami pindah. Khususnya, biaya APM untuk tanpa server yang -saat ini - adalah $6 /bulan per fungsi + biaya tambahan berdasarkan penggunaan.
Bagaimana cara mengganti Datadog?
Seperti yang disebutkan sebelumnya, DevStride menggunakan infrastruktur tanpa server dan kami memiliki lebih dari 200 fungsi lambda yang harus kami terapkan dan pantau.
Di DevStride, kami menggunakan SST yang merupakan abstraksi dari AWS CDK . Untuk definisi infrastruktur dan kode fungsi kami, kami menggunakan TypeScript.
Kami ingin mengganti agen Datadog kami saat ini dengan AWS X-Ray dan CloudWatch dan itu memerlukan beberapa perubahan dalam kode fungsi dan infrastruktur kami.
Fungsi pengaturan
Untuk membuat transisi kami sesederhana mungkin, kami memutuskan untuk menggunakan AWS Lambda PowerTools baru . Paket ini menyederhanakan beberapa penyiapan dan sangat membantu.
Dari alat-alat listrik kami mendapatkan:
- Pelacak yang bisa kita gunakan untuk membuat anotasi jejak
- Logger untuk menganotasi log dengan jejak kami dan parameter yang membantu
- Middleware untuk membubuhi keterangan jejak/log secara otomatis
- Pembantu untuk menambahkan metrik khusus
Kami membuat pelacak dan logger dan mengekspos fungsionalitas yang dapat kami gunakan di aplikasi kami.
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;
}
Di DevStride, kami menggunakan middy untuk middleware kami dan kami abstrak dengan kelas. Ini adalah contoh penangan lambda dasar yang diperluas oleh semua penangan lainnya.
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>;
}
Penyiapan infrastruktur
Kami ingin membuatnya sesederhana mungkin untuk menyiapkan fungsi, jadi kami menambahkan konfigurasi yang diperlukan ke variabel lingkungan. Untuk dapat melakukannya dengan cepat, kami membuat utility
fungsi yang melakukan itu untuk semua fungsi kami. Selain itu, kami ingin dapat mendengarkan semua log, sehingga kami dapat mengirimkan peringatan ke Slack saat kami menemukan log kesalahan.
Pertama, kami menyiapkan tumpukan yang akan menerapkan fungsi lambda kami dan mendengarkan log kami.
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");
}
Kami juga menambahkan retensi log default dan konfigurasi pelacakan ke semua fungsi kami.
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),
},
})

Selain itu, Anda akan melihat semua detail jejak dan juga daftar jejak.

Dengan kode di atas, kita memiliki apa yang kita perlukan untuk memantau jejak dan penggunaan secara keseluruhan. Kita juga bisa tahu jika ada yang tidak beres; jejak akan menunjukkan semua kesalahan dan di mana kesalahan itu terjadi. Selain itu, jejak akan mengelompokkan log bersama.
Mengirim peringatan ke Slack
Salah satu persyaratan kami adalah kami perlu diberi tahu tentang kesalahan dan menemukan cara mudah untuk melihat jejak/log kesalahan.
Kami memutuskan cara termudah untuk melakukannya bagi kami adalah mengirim log kesalahan ke Slack.
Di DevStride, kami menggunakan CQRS dan semua penangan kami memanggil perintah/kueri yang menjalankan logika bisnis.
Inilah penangan log kesalahan kami:
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);
}
}

Apa berikutnya?
Ada lebih banyak perbaikan yang menurut saya dapat membantu kami memajukan pemantauan kami, seperti:
- Tambahkan lebih banyak metrik
- Anotasi jejak dengan lebih banyak data
- Anotasi log dengan lebih banyak data
- Tambahkan lebih banyak logika ke pengendali log kesalahan untuk hanya mengirim kesalahan baru ke Slack
Jika Anda ingin tahu lebih banyak tentang arsitektur kami, repo ini menjelaskan banyak pola yang kami gunakan.
Saya berencana untuk menulis lebih banyak, jadi ikuti saya di Twitter karena saya terus memposting tentang langkah selanjutnya dan solusi lain yang harus kami buat.