로그는 console.log를 남기는 것이 아니다

vx_developer·어제

개발하다가

목록 보기
44/45
post-thumbnail

프로그래밍을 처음 배울 때 로그는 보통 코드가 실행되는 동안 값을 확인하기 위해 출력하는 메시지라고 배운다.

console.log("Rental started");

이 코드는 자전거 대여가 시작되는 지점까지 프로그램이 실행되었다는 사실을 개발자에게 보여준다.

기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 서비스를 운영하면 메시지를 출력하는 것보다 더 많은 판단이 필요하다.

  • 어떤 사건을 기록해야 하는가?
  • 한 번의 요청이 여러 서비스를 거칠 때 기록을 어떻게 연결하는가?
  • 성공, 예상된 거절, 시스템 오류를 어떻게 구분하는가?
  • 사용자 정보와 인증 토큰이 로그에 남지 않게 하려면 어떻게 해야 하는가?
  • 로그를 얼마나 오래 보관하고 누가 볼 수 있게 할 것인가?
  • 로그가 유실되거나 저장 시스템이 멈추면 서비스는 어떻게 동작해야 하는가?

로그는 console.log를 남기는 것이 아니다.

로그는 운영 중인 서비스에서 언제, 어디서, 누구에 의해, 어떤 사건이 발생했고 그 결과가 무엇이었는지를 다시 추적할 수 있게 만드는 구조화된 기록이다.


출력된 문장만으로는 실제 사건을 재구성하기 어렵다

크리스가 공유 자전거 대여 서비스를 개발한다고 생각해 보자.

사용자가 앱에서 자전거 잠금 해제를 요청하면 서버는 대여 가능 여부를 확인하고, 잠금 장치에 명령을 보내고, 대여 기록을 생성한다.

처음에는 다음과 같이 로그를 남길 수 있다.

console.log("Unlock requested");
console.log("Bike unlocked");

개발 중에는 실행 순서를 확인할 수 있다. 그러나 운영 환경에서 수천 명이 동시에 요청하면 이 문장만으로는 어떤 사용자의 어떤 자전거에 관한 기록인지 알 수 없다.

실패한 경우도 비슷하다.

console.log("Unlock failed");

이 로그에는 원인을 판단할 정보가 없다.

  • 자전거가 이미 대여 중이었는가?
  • 잠금 장치가 응답하지 않았는가?
  • 사용자의 결제 수단이 유효하지 않았는가?
  • 네트워크 요청이 시간 초과되었는가?
  • 어느 서버 버전에서 발생했는가?

운영 로그는 개발자의 현재 화면을 위한 메모가 아니다. 문제가 발생한 뒤 당시 상황을 재구성할 수 있는 기록이어야 한다.


문장이 아니라 사건의 구조를 기록해야 한다

다음 로그는 조금 더 많은 정보를 포함한다.

console.log(
  `User ${userId} unlocked bike ${bikeId}`
);

하지만 하나의 문자열 안에 정보가 섞여 있다. 사용자별 검색, 실패 원인별 집계, 특정 버전 비교를 하려면 문장을 다시 해석해야 한다.

구조화된 로그는 사건과 문맥을 필드로 나눈다.

logger.info({
  event: "rental.unlock_succeeded",
  requestId,
  accountId,
  bikeId,
  rentalId,
  service: "rental-api",
  serviceVersion: "2.4.1",
  durationMs: 184,
});

이 기록은 다음 질문에 직접 답할 수 있다.

  • 무슨 사건인가? rental.unlock_succeeded
  • 어떤 요청에서 발생했는가? requestId
  • 어떤 계정과 자전거인가? accountId, bikeId
  • 생성된 대여는 무엇인가? rentalId
  • 어느 서비스와 버전에서 발생했는가?
  • 처리에는 얼마나 걸렸는가?

필드 이름이 일정하면 로그 수집 시스템에서 조건을 조합해 검색하고 집계할 수 있다.

event = rental.unlock_succeeded
AND serviceVersion = 2.4.1
AND durationMs > 1000

사람이 읽기 좋은 문장도 필요할 수 있다. 그러나 자동 검색에 필요한 의미를 문장 안에만 숨겨서는 안 된다.


사건 이름은 구현 단계가 아니라 서비스의 의미를 설명해야 한다

크리스가 함수 이름을 그대로 로그에 남긴다고 생각해 보자.

logger.info({
  event: "handleRequest.completed",
});

이 이름은 코드 실행이 끝났다는 사실만 알려준다. 사용자가 무엇을 하려 했고 서비스에서 무엇이 바뀌었는지는 드러나지 않는다.

도메인 사건을 이름으로 사용하면 의미가 분명해진다.

logger.info({
  event: "rental.started",
  rentalId,
  bikeId,
  accountId,
});

rental.started는 함수나 파일 구조가 바뀌어도 유지할 수 있다. 운영자도 코드를 열어보지 않고 사건의 의미를 이해할 수 있다.

사건 이름은 일관된 규칙을 따르는 편이 낫다.

rental.unlock_requested
rental.unlock_rejected
rental.started
rental.ended
payment.authorization_failed
bike.lock_connection_failed

이름이 안정적이면 대시보드, 검색 조건, 알림 규칙이 로그 문구 변경 때문에 깨지지 않는다.

로그는 코드가 어느 줄을 통과했는지만 보여주는 것이 아니라 서비스에서 일어난 변화를 설명해야 한다.


하나의 요청에는 끝까지 유지되는 식별자가 필요하다

자전거 잠금 해제는 하나의 서버 안에서 끝나지 않을 수 있다.

API 서버가 계정 상태를 확인하고, 잠금 장치 서비스에 명령을 보내고, 결제 서비스에서 보증금을 승인할 수 있다.

각 서비스가 별도의 로그를 남기면 시간만으로 기록을 연결하기 어렵다.

const requestId =
  request.headers["x-request-id"] ??
  crypto.randomUUID();

서버는 기존 요청 ID를 검증해 사용하거나 새로운 ID를 생성할 수 있다. 이 값을 후속 요청에도 전달한다.

await lockService.unlock({
  bikeId,
  requestId,
});

await paymentService.authorizeDeposit({
  accountId,
  requestId,
});

각 서비스도 같은 식별자를 기록한다.

logger.info({
  event: "bike.unlock_command_sent",
  requestId,
  bikeId,
});

요청 흐름은 다음처럼 연결된다.

flowchart TD
    A[잠금 해제 요청] --> B[대여 API]
    B --> C[잠금 장치 서비스]
    B --> D[결제 서비스]
    C --> E[중앙 로그 저장소]
    D --> E
    B --> E

requestId나 분산 추적의 traceId를 사용하면 서로 다른 서비스의 기록을 하나의 사용자 행동으로 묶을 수 있다.

다만 외부에서 받은 요청 ID도 검증 전까지 신뢰할 수 없다. 길이와 문자 형식을 제한하지 않으면 검색 방해나 로그 주입에 사용될 수 있다.

import { z } from "zod";

const RequestIdSchema = z
  .string()
  .uuid();

const suppliedRequestId =
  RequestIdSchema.safeParse(
    request.headers["x-request-id"]
  );

const requestId = suppliedRequestId.success
  ? suppliedRequestId.data
  : crypto.randomUUID();

이 코드는 올바른 UUID만 이어받고 나머지는 서버가 새로 생성한다.


로그 레벨은 메시지의 감정이 아니라 운영 행동을 결정한다

모든 기록을 error로 남기면 중요한 장애가 평범한 사건 속에 묻힌다. 반대로 실제 실패를 info로 남기면 경보가 필요한 상황을 놓칠 수 있다.

로그 레벨은 팀 안에서 의미가 합의되어야 한다.

레벨공유 자전거 서비스에서의 의미
debug개발 또는 제한된 진단에 필요한 내부 처리 정보
info대여 시작·종료처럼 정상적으로 완료된 중요한 사건
warn요청은 처리했지만 비정상 징후나 복구 가능한 문제가 있음
error한 작업이 실패했으며 조사 또는 복구가 필요함
fatal프로세스가 정상적으로 계속 실행될 수 없음

예를 들어 사용자가 이미 대여 중인 자전거를 선택한 것은 시스템 장애가 아니다.

logger.info({
  event: "rental.unlock_rejected",
  requestId,
  bikeId,
  accountId,
  reason: "bike_already_rented",
});

서비스가 예상한 비즈니스 규칙에 따라 요청을 거절했으므로 info로 기록할 수 있다.

반면 잠금 장치 서비스가 응답하지 않았다면 기술적 실패다.

logger.error({
  event: "bike.lock_connection_failed",
  requestId,
  bikeId,
  errorCode: "LOCK_GATEWAY_TIMEOUT",
  retryable: true,
});

이 기록은 외부 장치 연결 실패이며 재시도 가능하다는 사실을 설명한다.

레벨은 “좋은 일인가, 나쁜 일인가”를 표시하는 장식이 아니다. 누가 언제 확인하고 어떤 대응을 해야 하는지 결정하는 운영 신호다.


예외 메시지만 기록하면 서비스의 실패 이유가 사라진다

다음 코드는 예외 객체만 출력한다.

try {
  await lockService.unlock(bikeId);
} catch (error) {
  console.error(error);
}

스택 트레이스는 코드 위치를 찾는 데 도움이 된다. 하지만 사용자가 무엇을 시도했는지, 어떤 자전거가 대상이었는지, 재시도가 가능한지는 알기 어렵다.

서비스 문맥을 함께 기록하는 편이 낫다.

try {
  await lockService.unlock({
    bikeId,
    requestId,
  });
} catch (error) {
  logger.error({
    event: "bike.unlock_failed",
    requestId,
    bikeId,
    retryable: isRetryableLockError(error),
    error: serializeSafeError(error),
  });

  throw error;
}

이 코드는 예외를 삼키지 않고 상위 계층으로 다시 전달한다. 로그에는 진단에 필요한 문맥과 안전하게 정리된 오류 정보가 남는다.

같은 예외를 여러 계층에서 반복해서 기록하면 동일한 실패가 여러 건처럼 보일 수 있다.

  • 하위 계층은 오류에 의미 있는 문맥을 추가하거나 그대로 전달한다.
  • 요청 경계는 최종 처리 결과를 한 번 기록한다.
  • 재시도 계층은 각 시도와 최종 실패를 구분한다.

어느 계층이 사건을 소유하고 기록할지 정해야 중복 로그를 줄일 수 있다.


외부 입력은 로그에 기록할 때도 검증 전까지 신뢰할 수 없다

잠금 해제 요청의 본문 전체를 기록하면 문제를 빠르게 찾을 수 있을 것처럼 보인다.

logger.info({
  event: "rental.unlock_requested",
  body: request.body,
});

그러나 요청 본문에는 예상하지 못한 필드, 매우 긴 문자열, 줄바꿈 문자, 개인정보가 포함될 수 있다.

외부 입력은 검증 전까지 신뢰할 수 없다. 로그에 남긴다고 예외가 되지 않는다.

import { z } from "zod";

const UnlockRequestSchema = z.object({
  bikeId: z.string().uuid(),
  stationId: z.string().uuid(),
});

const result =
  UnlockRequestSchema.safeParse(request.body);

if (!result.success) {
  logger.warn({
    event: "rental.input_validation_failed",
    requestId,
    invalidFields: result.error.issues.map(
      (issue) => issue.path.join(".")
    ),
  });

  throw new InvalidUnlockRequestError();
}

const input = result.data;

이 로그는 검증 실패가 발생한 필드만 기록한다. 검증되지 않은 원본 본문은 남기지 않는다.

사용자 입력을 자유 형식 메시지에 이어 붙이면 줄바꿈과 구분 문자를 이용한 로그 주입 위험도 생길 수 있다.

console.log(
  `Unlock failed: ${request.body.reason}`
);

구조화된 로거를 사용하고, 필드 길이와 허용 형식을 제한하며, 출력 형식에 맞게 인코딩해야 한다.


필요한 문맥과 기록해서는 안 되는 데이터를 구분해야 한다

로그는 조사를 위해 충분한 정보를 가져야 하지만 모든 데이터를 담아서는 안 된다.

다음 코드는 과도한 정보를 기록한다.

logger.info({
  event: "rental.started",
  user: currentUser,
  headers: request.headers,
  paymentMethod,
});

객체 전체에는 이메일, 전화번호, 세션 쿠키, 액세스 토큰, 결제 정보가 포함될 수 있다.

필요한 식별자와 결과만 선택해야 한다.

logger.info({
  event: "rental.started",
  requestId,
  accountId: currentUser.id,
  bikeId,
  rentalId,
  paymentResult: "authorized",
});

이 기록은 사건을 추적할 수 있지만 사용자 객체와 인증 정보를 복사하지 않는다.

일반적으로 다음 값은 원문 그대로 로그에 남기지 않아야 한다.

  • 비밀번호
  • 세션 ID와 액세스 토큰
  • API 키와 암호화 키
  • 데이터베이스 연결 문자열
  • 결제 카드와 은행 정보
  • 필요하지 않은 이메일, 전화번호, 위치 이력
  • 전체 요청·응답 본문
  • 비밀값이 포함될 수 있는 HTTP 헤더

사용자 식별이 필요하더라도 이름이나 이메일 대신 내부 계정 ID 또는 목적에 맞게 가명 처리한 값을 사용할 수 있다.

로그는 문제를 해결할 만큼 자세해야 하지만 새로운 개인정보 저장소가 되어서는 안 된다.


로그와 감사 기록은 목적이 다르다

애플리케이션 로그에는 다음과 같은 기록이 들어갈 수 있다.

logger.info({
  event: "rental.ended",
  rentalId,
  durationSeconds,
});

이 기록은 운영 상태를 확인하고 문제를 진단하는 데 유용하다. 그러나 사용자의 요금 분쟁을 판단하는 유일한 근거로 사용하기에는 부족할 수 있다.

대여의 Source of Truth는 데이터베이스에 저장된 대여 상태와 정산 데이터다.

type Rental = {
  id: string;
  accountId: string;
  bikeId: string;
  startedAt: Date;
  endedAt: Date | null;
  status: "active" | "completed" | "cancelled";
};

로그가 유실되거나 보존 기간이 끝나도 현재 대여 상태는 유지되어야 한다.

관리자가 요금을 수정하거나 대여를 강제로 종료한 기록처럼 책임 추적이 필요한 사건은 별도의 감사 기록으로 관리할 수 있다.

type RentalAuditEvent = {
  eventId: string;
  rentalId: string;
  actorId: string;
  action: "force_end" | "fare_adjusted";
  reasonCode: string;
  occurredAt: Date;
};

감사 기록은 누가 어떤 권한으로 어떤 변경을 했는지 증명하는 목적을 가진다. 일반 디버그 로그와 접근 권한, 보존 기간, 변경 방지 수준이 다를 수 있다.

  • 애플리케이션 로그는 진단과 운영을 돕는다.
  • 감사 기록은 중요한 행동의 책임과 변경 이력을 보존한다.
  • 데이터베이스는 서비스가 기억해야 하는 현재 사실의 Source of Truth다.

모든 기록을 하나의 로그 스트림으로 처리하면 목적과 보존 책임이 흐려진다.


로그의 양은 많을수록 좋은 것이 아니다

모든 함수 진입과 모든 반복 처리를 기록하면 정보는 많아진다.

for (const bike of nearbyBikes) {
  logger.debug({
    event: "bike.distance_calculated",
    bikeId: bike.id,
    distanceMetres: bike.distanceMetres,
  });
}

주변에 자전거가 많고 요청도 많다면 로그 건수가 빠르게 증가한다.

과도한 로그는 다음 비용을 만든다.

  • 애플리케이션의 직렬화와 전송 비용
  • 네트워크 사용량
  • 저장 공간과 검색 비용
  • 민감정보가 포함될 가능성
  • 중요한 사건이 묻히는 문제
  • 보존과 삭제 정책의 복잡성

개별 계산이 아니라 요청 전체의 결과를 기록할 수 있다.

logger.info({
  event: "bike.nearby_search_completed",
  requestId,
  resultCount: nearbyBikes.length,
  durationMs,
});

이 기록은 사용자가 경험한 결과와 처리 시간을 설명하면서 로그 양을 제한한다.

상세 진단이 필요하면 특정 환경이나 일부 요청에만 debug 로그를 활성화하거나 샘플링할 수 있다. 다만 보안 사건과 필수 감사 기록까지 임의로 제외해서는 안 된다.


보존 기간은 저장 공간이 아니라 목적에서 결정된다

모든 로그를 영구히 보관하면 나중에 유용할 것처럼 보인다. 그러나 오래된 로그에는 개인정보와 내부 시스템 정보가 계속 남는다.

로그 종류마다 목적과 기간을 정해야 한다.

로그 종류주된 목적보존 판단
상세 디버그 로그단기 장애 분석짧게 보관하거나 제한적으로 수집
요청·오류 로그운영 분석과 장애 대응운영 요구에 맞는 기간
보안 사건 로그침해 탐지와 조사보안·법적 요구를 반영
감사 기록중요한 변경의 책임 추적정책과 규제에 맞게 별도 관리
성능 원시 로그병목 분석집계 후 원본을 더 빨리 삭제 가능

보존 정책에는 저장 기간뿐 아니라 다음 내용도 포함된다.

  • 누가 로그를 조회할 수 있는가?
  • 조회 자체를 기록하는가?
  • 저장 중이거나 전송 중인 로그를 어떻게 보호하는가?
  • 삭제 시 백업과 복제본도 함께 처리되는가?
  • 로그가 변조되거나 수집이 중단되었음을 탐지할 수 있는가?

로그는 생성 순간부터 삭제까지 생명주기를 가진 데이터다.


로그 저장 실패가 서비스 실패로 번질지 결정해야 한다

중앙 로그 저장소가 잠시 응답하지 않을 수 있다.

모든 요청이 로그 저장 완료를 기다리게 하면 로그 시스템 장애가 대여 서비스 장애로 이어질 수 있다.

await remoteLogStorage.write(logRecord);
await rentalService.start(input);

이 구조에서는 로그 저장소가 느릴 때 대여도 시작되지 않는다.

일반 운영 로그는 표준 출력이나 비동기 전송 경로로 보내고 실행 환경이 수집하도록 구성할 수 있다.

logger.info({
  event: "rental.start_requested",
  requestId,
  bikeId,
});

애플리케이션 코드는 로그 수집 시스템의 원격 저장 완료를 직접 기다리지 않는다.

그렇다고 로그 유실을 무시해도 된다는 뜻은 아니다.

  • 버퍼가 가득 찼을 때 어떻게 처리하는가?
  • 전송 실패를 별도의 지표로 감지하는가?
  • 프로세스 종료 전에 남은 로그를 얼마나 기다리는가?
  • 필수 감사 기록은 일반 운영 로그보다 강한 보장이 필요한가?
  • 로그 처리 실패가 메모리나 디스크를 고갈시키지 않는가?

일반 로그, 보안 로그, 감사 기록은 필요한 전달 보장이 다를 수 있다.


로그는 모니터링 전체를 대신하지 않는다

대여 실패 건수를 알아보기 위해 매번 로그를 검색할 수 있다.

logger.error({
  event: "rental.start_failed",
  errorCode: "LOCK_GATEWAY_TIMEOUT",
});

하지만 운영 상태를 지속적으로 판단하려면 로그 외의 신호도 필요하다.

신호주로 답하는 질문
로그특정 사건에서 무슨 일이 발생했는가?
메트릭실패율과 응답 시간은 얼마나 변했는가?
트레이스하나의 요청이 어느 서비스에서 지연되었는가?
감사 기록누가 중요한 변경을 수행했는가?

예를 들어 잠금 해제 실패율은 메트릭으로 집계할 수 있다.

rentalStartFailures.add(1, {
  reason: "lock_gateway_timeout",
});

경보는 일정 시간 동안 실패율이 임계값을 넘었을 때 발생시킬 수 있다. 담당자는 알림을 받은 뒤 관련 traceId나 requestId로 상세 로그를 찾는다.

로그, 메트릭, 트레이스는 서로 경쟁하는 기술이 아니다. 서로 다른 질문에 답하며 함께 운영 상황을 설명한다.


로그를 설계하기 전에 무엇을 물어야 하는가

어떤 사건을 기록해야 하는가

  1. 사용자가 시작한 중요한 행동은 무엇인가?
  2. 서비스의 상태가 바뀌는 지점은 어디인가?
  3. 실패 원인을 나중에 구분할 수 있는가?
  4. 인증·권한·입력 검증 실패를 기록하는가?
  5. 단순 함수 실행이 아니라 도메인 사건을 기록하는가?

사건을 다시 연결할 수 있는가

  1. 모든 요청에 requestId 또는 traceId가 있는가?
  2. 후속 서비스에도 같은 식별자를 전달하는가?
  3. rentalId, bikeId, accountId처럼 필요한 식별자가 포함되는가?
  4. 서비스 이름과 배포 버전을 알 수 있는가?
  5. 사건 발생 시각과 수집 시각을 구분해야 하는가?

레벨과 오류 분류가 일관적인가

  1. 예상된 비즈니스 거절과 시스템 오류를 구분하는가?
  2. warn과 error가 필요한 운영 대응과 연결되는가?
  3. 재시도 가능한 실패인지 알 수 있는가?
  4. 오류 코드가 안정적인 분류값으로 존재하는가?
  5. 같은 예외를 여러 계층에서 중복 기록하지 않는가?

민감한 정보를 제외했는가

  1. 비밀번호, 토큰, API 키가 기록되지 않는가?
  2. 요청 본문과 헤더 전체를 무조건 저장하지 않는가?
  3. 이메일과 위치 정보가 실제로 필요한가?
  4. 사용자 객체 대신 필요한 식별자만 선택하는가?
  5. 외부 입력을 검증하고 안전하게 인코딩하는가?

로그의 책임과 Source of Truth를 구분했는가

  1. 로그가 사라져도 현재 서비스 상태를 알 수 있는가?
  2. 비즈니스 사실은 데이터베이스에 저장되는가?
  3. 감사가 필요한 사건을 일반 로그와 분리했는가?
  4. 로그를 근거로 자동 조치할 때 위조와 중복을 고려했는가?
  5. 각 기록의 소유자와 변경 방지 요구가 정해져 있는가?

비용과 생명주기를 설계했는가

  1. 로그 한 건이 아니라 전체 트래픽에서 발생할 양을 계산했는가?
  2. 환경과 레벨별 수집 범위를 정했는가?
  3. 보존 기간이 목적과 법적 요구에 맞는가?
  4. 조회 권한과 전송·저장 보호가 적용되는가?
  5. 로그 저장 실패와 수집 중단을 감지할 수 있는가?

흔한 실수는 로그를 개발 중 출력문의 연장으로 보는 것이다

"Something went wrong"만 기록한다

대상, 요청, 실패 원인이 없어 사건을 재구성할 수 없다.

안정적인 사건 이름과 필요한 문맥을 함께 기록해야 한다.

모든 정보를 하나의 문자열에 넣는다

검색과 집계를 위해 문장을 다시 해석해야 한다.

시간, 사건 이름, 식별자, 결과, 오류 코드를 구조화된 필드로 분리해야 한다.

모든 실패를 error로 남긴다

예상된 비즈니스 거절과 실제 시스템 장애가 섞인다.

레벨을 운영 대응 기준으로 정의해야 한다.

요청과 사용자 객체 전체를 기록한다

토큰, 개인정보, 결제 정보가 로그 저장소에 복제될 수 있다.

목적에 필요한 필드만 허용 목록 방식으로 선택해야 한다.

로그를 비즈니스 데이터처럼 사용한다

로그 유실이나 보존 종료가 서비스 상태 손실로 이어질 수 있다.

데이터베이스의 원본 사실, 감사 기록, 운영 로그를 구분해야 한다.

디버그 로그를 운영에서 무제한 수집한다

비용과 검색 잡음이 증가하고 중요한 사건이 묻힌다.

환경, 레벨, 샘플링, 보존 기간을 함께 설계해야 한다.

로그 저장이 실패하면 사용자 요청도 무조건 실패시킨다

관측 시스템의 장애가 핵심 서비스 장애로 전파될 수 있다.

기록의 중요도에 따라 비동기 수집과 강한 전달 보장을 구분해야 한다.


로그의 핵심은 운영 중 일어난 일을 다시 설명할 수 있게 하는 데 있다

프로그래밍을 처음 배울 때는 로그를 실행 중인 값을 확인하는 출력문이라고 이해해도 충분하다.

console.log("Rental started");

하지만 실제 서비스를 운영할 때는 메시지를 출력했는지보다 사건을 다시 구성할 수 있는지가 중요하다.

  • 어떤 도메인 사건이 발생했는가?
  • 언제, 어느 서비스와 버전에서 발생했는가?
  • 어떤 요청과 데이터에 관련된 사건인가?
  • 성공, 예상된 거절, 기술적 실패 중 무엇인가?
  • 원인을 분류할 안정적인 오류 코드가 있는가?
  • 여러 서비스의 기록을 하나의 흐름으로 연결할 수 있는가?
  • 민감정보와 검증되지 않은 입력을 제외했는가?
  • 로그와 데이터베이스의 Source of Truth를 구분했는가?
  • 보존 기간, 접근 권한, 비용이 정의되어 있는가?
  • 수집이 멈추거나 로그가 유실되었음을 발견할 수 있는가?

로그는 console.log를 남기는 것이 아니다.

로그는 운영 중인 서비스에서 언제, 어디서, 누구에 의해, 어떤 사건이 발생했고 그 결과가 무엇이었는지를 다시 추적할 수 있게 만드는 구조화된 기록이다.


참고 자료

profile
Vision eXperience Developer

0개의 댓글