로그는 에러 추적을 위해 필요하다.
에러가 발생했을때 어떤 api가 호출 되었는지, 해당 api가 어떤 함수를 트리거 하는지, DB는 무엇을 호출하는지 등등의 정보가 있어야 정확하게 어디서부터 문제를 해결해야하는지 파악할 수 있다.
특히 MSA와 같은 아키텍쳐 모델들은 특정 서비스에서 장애가 생길 경우, 로깅이 정확할수록 상호 작용하는 모든 서비스들을 확인할 필요 없이 어떤 서비스의 에러인지 정확하게 파악하고 처리할 수 있다.
이런 로그 추적 시스템들을 분산 로그 추적 시스템(Distributed Tracing)이라 한다.
앞으로 남길 로그들은 총 세가지가 있다.
속성 | 설명 |
---|---|
traceId | 전체 작업을 추적하기 위한 고유 식별자 |
spanId | 현재 "span"의 고유 식별자 |
parentSpanId | 현재 "span"의 부모 "span"의 고유 식별자 |
traceState | 분산 추적 시스템에서 사용하는 다른 메타데이터를 포함. 이러한 메타데이터는 일반적으로 사용자 정의 속성이며, "span context"를 통해 전달 |
이외에도 span에는 시작시간, 종료시간 및 tag("span"과 관련된 추가 정보를 제공하는 데 사용. 예를 들어, "HTTP" 요청의 경우, "tags"는 요청 URL, HTTP 메소드, 응답 코드 등을 포함) 등이 있습니다.
${서비스명(혹은 함수명}, ~~ 에러 발생
)과 같이 남겨 빠르게 해당 함수를 찾고 어떤 에러인지 파악 할 수 있도록 하였다.aws는 firelens를 사용하여 편리하게 ecs 배포 설정이 가능하다.
AWS FireLens는 ECS의 task와 Fluent Bit(또는 Fluentd) 사이드카를 배포하고 로그를 라우팅할 수 있는 로그 드라이버입니다. AWS FireLens를 사용하면 배포 스크립트를 수정하지 않고도 컨테이너 로그를 스토리지 및 분석 도구로 보낼 수 있습니다. AWS Fargate 에 대한 몇 가지 구성 업데이트를 통해 대상을 선택하고 선택적으로 필터를 정의하여 필요한 위치로 컨테이너 로그를 보내도록 FireLens 에 지시합니다.
!! executionRoleArn 반드시 Cloudwatch logs에 대한 권한이 있어야 한다
Fluent-bit를 사용하여 로그 전송
아래와 같이 Fargate를 사용하여 작업을 정의해주면 된다. ecs fargate 설정은 JSON으로 새 계정 생성을 클릭후 아래를 복사 붙여 넣기 하면 된다. ({}안의 내용은 변경)
"containerDefinitions": [
{
"name": "{appName}",
"image": "{appImage}",
"portMappings": [
{
"name": "{your app}-port-tcp",
"containerPort": {app port},
"hostPort": {app port},
"protocol": "tcp"
}
],
"essential": true,
"logConfiguration": {
"logDriver": "awsfirelens",
"options": {
"AWS_Auth": "On",
"AWS_Region": "ap-northeast-2",
"Host": "{opensearch url}",
"Index": "{Index}",
"Name": "es",
"Port": "443",
"Suppress_Type_Name": "On",
"tls": "On"
}
}
},
{
"name": "log-router",
"image": "906394416424.dkr.ecr.ap-northeast-2.amazonaws.com/aws-for-fluent-bit:stable",
"memoryReservation": 50,
"essential": true,
"user": "0",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-create-group": "true",
"awslogs-group": "firelens-container",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "firelens"
}
},
"firelensConfiguration": {
"type": "fluentbit",
"options": {
"config-file-type": "file",
"config-file-value": "/fluent-bit/configs/parse-json.conf"
}
}
}
],
참고로 Suppress_Type_Name 을 설정해줘야 es7 이상, 혹은 opensearch 최신 버전에서 mapping type 삭제로 인해 생기는 에러를 막을 수 있다. (사용하는 버전이 elasticsearch 6 버전 이하면 Suppress_Type_Name을 삭제하고 "type":{타입이름}을 설정해줘야 한다.)
opensearch 햄버거 메뉴 -> Stack management를 클릭 후 indices 클릭
Create Index Pattern 클릭 후 {인덱스명} 혹은 {인덱스명}- 과 같이 입력한 이후
타임필드를 클릭하여 시계열로 데이터를 뽑을 수 있도록 field를 설정해주면 된다.
이후 다시 햄버거 메뉴를 클릭하여 Discover에 들어가면
내가 설정한 인덱스를 볼 수 있으며, 인덱스별로 들어온 로그를 시간순으로 확인할 수 있다.
Jaeger는 분산 시스템에서의 추적(tracing)을 위한 오픈소스 도구이다.
Jaeger는 Google의 Dapper 및 Uber의 Zipkin 추적 시스템에서 영감을 받아 개발되었다. Jaeger는 CNCF(Cloud Native Computing Foundation)의 오픈소스 프로젝트 중 하나로서, Kubernetes 및 Istio와 같은 CNCF 프로젝트와 통합이 가능하며, 여러 언어 및 프레임워크에서 사용할 수 있는 API를 제공한다.
Jaegger는 사용하는 포트가 많은데 기본 포트는 아래와 같다.
Collector port: 14267/tcp, 14268/tcp
Jaeger Collector가 수집한 추적 데이터를 수신하는 데 사용됩니다.
14267/tcp는 gRPC 프로토콜을 사용하며, 14268/tcp는 Thrift 프로토콜을 사용합니다.
Query port: 16686/tcp
Jaeger UI가 사용하는 포트입니다.
웹 브라우저에서 Jaeger UI를 열면, 해당 포트를 통해 UI가 띄워집니다.
Agent port: 5775/udp, 6831/udp, 6832/udp
Jaeger Agent가 수집한 추적 데이터를 Collector로 전송하는 데 사용됩니다.
HTTP port: 16686/tcp
Jaeger UI에 대한 HTTP 요청을 처리하는 데 사용됩니다.
Admin HTTP port: 14269/tcp
Jaeger Collector와 Agent의 관리 기능에 대한 HTTP 요청을 처리하는 데 사용됩니다.
Health check port: 16687/tcp
Jaeger 서비스의 상태를 확인하는 데 사용됩니다.
이중에서 주로 개발자가 사용하는 포트는 14268 포트와 16686 포트이다.
{Jaegger IP 주소}:14268/api/traces로 트레이싱 정보를 날리고 {Jaegger IP 주소}:16686 로 Jaegger ui에 접속하기 때문이다.
여기에 추가적으로 사용할 포트는 아래와 같다.
Jaegger 역시 간단하게 ecs fargate로 띄울 수 있다.
Jaegger all-in-one 이미지를 사용하면 수월하게 띄울 수 있다.
"containerDefinitions": [
{
"name": "dev-container-jaeger",
"image": "jaegertracing/all-in-one:latest",
"cpu": 0,
"portMappings": [
{
"name": "dev-container-jaeger-14269-tcp",
"containerPort": 14269,
"hostPort": 14269,
"protocol": "tcp"
},
{
"name": "dev-container-jaeger-14268-tcp",
"containerPort": 14268,
"hostPort": 14268,
"protocol": "tcp"
},
{
"containerPort": 6832,
"hostPort": 6832,
"protocol": "udp"
},
{
"containerPort": 6831,
"hostPort": 6831,
"protocol": "udp"
},
{
"containerPort": 5775,
"hostPort": 5775,
"protocol": "udp"
},
{
"name": "dev-container-jaeger-14250-tcp",
"containerPort": 14250,
"hostPort": 14250,
"protocol": "tcp"
},
{
"name": "dev-container-jaeger-16685-tcp",
"containerPort": 16685,
"hostPort": 16685,
"protocol": "tcp"
},
{
"name": "dev-container-jaeger-5778-tcp",
"containerPort": 5778,
"hostPort": 5778,
"protocol": "tcp"
},
{
"name": "dev-container-jaeger-16686-tcp",
"containerPort": 16686,
"hostPort": 16686,
"protocol": "tcp"
},
{
"name": "dev-container-jaeger-9411-tcp",
"containerPort": 9411,
"hostPort": 9411,
"protocol": "tcp"
}
],
"essential": true,
"command": [
"--collector.zipkin.host-port",
"9411"
],
"environment": [
{
"name": "ES_SERVER_URLS",
"value": "{opensearch url}"
},
{
"name": "SPAN_STORAGE_TYPE",
"value": "elasticsearch"
}
],
"mountPoints": [],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/{ecs 서비스 이름}",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "ecs"
}
}
}
],
위를 복사 붙여넣기 하여 Jaegger가 잘 뜬 것을 확인하면, 이제 앱 서비스에서 Jaegger 쪽으로 api를 호출하여 trace 정보를 넘겨야 한다.
Node는 아래와 같이 tracing.ts를 작성하면 된다.
import {
SimpleSpanProcessor,
ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-base';
import { api, node, NodeSDK } from '@opentelemetry/sdk-node';
import process from 'process';
const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base');
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { B3Propagator } from '@opentelemetry/propagator-b3';
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
const jaegerExporter = new JaegerExporter({
endpoint: 'http://{Jaegger IP}:14268/api/traces',
});
const { trace } = require('@opentelemetry/api');
const traceConsoleExporter = new ConsoleSpanExporter();
// 로컬로 실행할 경우 Jaeger에 trace 기록 X, console로만 trace 찍음.
const traceExporter =
process.env.NODE_ENV === `local` ? traceConsoleExporter : jaegerExporter;
const spanProcessor =
process.env.NODE_ENV === `development`
? new SimpleSpanProcessor(traceExporter)
: new BatchSpanProcessor(traceExporter);
// Set B3 Propagator
api.propagation.setGlobalPropagator(new B3Propagator());
export const otelSDK = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: `{서비스 명}`,
}),
spanProcessor,
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
new NestInstrumentation(),
new WinstonInstrumentation(),
],
});
// gracefully shut down the SDK on process exit
process.on('SIGTERM', () => {
otelSDK
.shutdown()
.then(
() => console.log('SDK shut down successfully'),
(err) => console.log('Error shutting down SDK', err),
)
.finally(() => process.exit(0));
});
이후 main.ts에서 아래와 같이 실행시키면 된다.
async function bootstrap() {
// 제일 처음에 otelSDK를 실행 시켜야 함.
await otelSDK.start();
const app = await NestFactory.create(AppModule, {cors: true});
// winston을 사용하여 로그를 남김
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
await app.listen(3000);
}
bootstrap();