Rails.logger
를 사용하지 마세요- 구조화된 (JSON) 로깅을 사용하세요
- 다중 목적지 로깅
- 예외 처리
- 기본 로그 위치
- 새로운 로그 파일에 대한 추가 단계
- Kibana에서 새로운 로그 파일 찾기 (GitLab.com 전용)
- 로깅 가시성 제어
로깅 개발 지침
GitLab 로그는 문제를 진단하는 데 있어 관리자와 GitLab 팀 구성원 모두에게 중요한 역할을 합니다.
Rails.logger
를 사용하지 마세요
현재 Rails.logger
로 호출된 모든 내용은 production.log
에 저장되며, 이 로그는 Rails 로그와 개발자가 코드베이스에 삽입한 다른 호출의 혼합물을 포함합니다. 예를 들면:
Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200
Processing by Projects::TreeController#show as HTML
Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"}
...
Namespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]]
CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]]
CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members".
(1.4ms) SELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened')) [["target_project_id", 18]]
Rendered layouts/nav/_project.html.haml (28.0ms)
Rendered layouts/_collapse_button.html.haml (0.2ms)
Rendered layouts/_flash.html.haml (0.1ms)
Rendered layouts/_page.html.haml (32.9ms)
Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms)
이 로그는 여러 가지 문제를 가지고 있습니다:
-
종종 타임스탬프나 기타 맥락 정보(예: 프로젝트 ID 또는 사용자)가 부족합니다.
-
여러 줄에 걸쳐 있어 Elasticsearch를 통해 찾기 어렵습니다.
-
공통 구조가 부족하여 Logstash나 Fluentd와 같은 로그 포워더가 파싱하기 어렵습니다. 이로 인해 검색하기도 어렵습니다.
현재 GitLab.com에서는 production.log
의 메시지가 양과 노이즈 때문에 Elasticsearch에 의해 인덱싱되지 않습니다. 로그는 Google Stackdriver에 기록되지만, 거기서도 로그를 검색하기가 더 어렵습니다. 더 많은 세부정보는 GitLab.com 로깅 문서를 참조하세요.
구조화된 (JSON) 로깅을 사용하세요
구조화된 로깅은 이러한 문제를 해결합니다. API 요청의 예를 고려해 봅시다:
{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30}
한 줄에 사용자에게 필요한 모든 정보를 포함했습니다: 타임스탬프, HTTP 방법 및 경로, 사용자 ID 등입니다.
JSON 로깅 사용 방법
프로젝트 가져오기 작업에서 발생하는 이벤트를 로깅하고 싶다고 가정해 보겠습니다. 가져오기 작업이 진행되는 동안 생성된 이슈, 병합 요청 등을 로깅하고 싶습니다. 다음은 할 일입니다:
-
GitLab 로그 목록을 확인하여 로그 메시지가 기존 로그 파일 중 하나에 포함될 수 있는지 확인합니다.
- 적절한 장소가 없다면, 새로운 파일 이름을 만드는 것을 고려하되, 그렇게 하는 것이 적절한지 유지 관리와 확인하십시오. 로그 파일은 사람들이 한 곳에서 관련 로그를 쉽게 검색할 수 있도록 해야 합니다. 예를 들어,
geo.log
는 GitLab Geo와 관련된 모든 로그를 포함합니다. 새 파일을 만들려면:- 파일 이름을 선택합니다 (예:
importer_json.log
). -
Gitlab::JsonLogger
의 새로운 하위 클래스를 생성합니다:module Gitlab module Import class Logger < ::Gitlab::JsonLogger def self.file_name_noext 'importer' end end end end
기본적으로
Gitlab::JsonLogger
는 로그 항목에 애플리케이션 컨텍스트 메타데이터를 포함합니다. 로거가 애플리케이션 요청 외부에서 호출될 것으로 예상되거나(예:rake
작업에서) 애플리케이션 컨텍스트 구축에 관여할 수 있는 저수준 코드(예: 데이터베이스 연결 코드)에 의해 호출될 경우, 로거 클래스에 대해 클래스 메서드exclude_context!
를 호출해야 합니다. 다음과 같이:module Gitlab module Database module LoadBalancing class Logger < ::Gitlab::JsonLogger exclude_context! def self.file_name_noext 'database_load_balancing' end end end end end
-
로그를 기록하고 싶은 클래스에서 로거를 인스턴스 변수로 초기화할 수 있습니다:
attr_accessor :logger def initialize @logger = ::Import::Framework::Logger.build end
매번 로그를 남길 때마다 새로운 로거를 생성하는 것은 불필요한 오버헤드를 추가하기 때문에 로거를 메모이제이션하는 것이 유용합니다.
- 파일 이름을 선택합니다 (예:
-
이제 코드에 로그 메시지를 삽입합니다. 로그를 추가할 때는 모든 컨텍스트를 키-값 쌍으로 포함하는지 확인합니다:
# BAD logger.info("Unable to create project #{project.id}")
# GOOD logger.info(message: "Unable to create project", project_id: project.id)
- 로그 메시지의 공통 기본 구조를 만드는 것이 중요합니다. 예를 들어, 모든 메시지에
current_user_id
와project_id
가 포함되어 특정 시간에 사용자별로 활동을 검색하기 쉽게 만들 수 있습니다.
JSON 로깅에 대한 암시적 스키마
Elasticsearch와 같은 구조화된 로그를 색인화하는 시스템을 사용할 때는 각 로그 필드의 유형에 대한 스키마가 있습니다(비록 그 스키마가 암시적/유추적일 수 있습니다). 필드 값의 유형에 일관성을 유지하는 것이 중요합니다. 그렇지 않으면 이러한 필드에서 검색/필터링할 수 있는 기능이 중단되거나 전체 로그 이벤트가 삭제될 수도 있습니다. 이 섹션의 대부분이 Elasticsearch에 특정하게 표현되어 있지만, 로그를 색인화하기 위해 사용할 수 있는 많은 시스템에 개념이 적용될 것입니다. GitLab.com은 로그 데이터를 색인화하기 위해 Elasticsearch를 사용합니다.
필드 유형이 명시적으로 매핑되지 않는 한, Elasticsearch는 해당 필드 값을 처음으로 본 경우 필드 유형을 유추합니다. 서로 다른 유형의 필드 값이 있는 후속 인스턴스는 색인화되지 않거나, 경우에 따라(스칼라/객체 충돌) 전체 로그 라인이 삭제됩니다.
GitLab.com의 로깅 Elasticsearch는
ignore_malformed
설정이 되어 있어,
더 간단한 매핑 충돌(예: 숫자/문자열)이 있는 경우에도 문서를 색인화할 수 있게 해 주지만, 영향을 받는 필드에서 색인화가 중단됩니다.
예시:
# GOOD
logger.info(message: "Import error", error_code: 1, error: "I/O failure")
# BAD
logger.info(message: "Import error", error: 1)
logger.info(message: "Import error", error: "I/O failure")
# WORST
logger.info(message: "Import error", error: "I/O failure")
logger.info(message: "Import error", error: { message: "I/O failure" })
리스트 요소는 동일한 유형이어야 합니다:
# GOOD
logger.info(a_list: ["foo", "1", "true"])
# BAD
logger.info(a_list: ["foo", 1, true])
리소스:
클래스 속성 포함
구조화된 로그는 특정 코드 위치에서 기록된 모든 항목을 찾을 수 있도록 항상 class
속성을 포함해야 합니다.
자동으로 class
속성을 추가하려면
Gitlab::Loggable
모듈을 포함하고 build_structured_payload
메서드를 사용하면 됩니다.
class MyClass
include ::Gitlab::Loggable
def my_method
logger.info(build_structured_payload(message: 'log message', project_id: project_id))
end
private
def logger
@logger ||= Gitlab::AppJsonLogger.build
end
end
로깅 지속 시간
시간대와 유사하게, 로그를 기록할 때 적절한 시간 단위를 선택하는 것은 피할 수 있는 오버헤드를 초래할 수 있습니다. 따라서 초, 밀리초 또는 다른 단위 중에서 선택해야 할 때는 _초_를 부동소수점 형식으로 선택하는 것이 좋습니다
(즉, 마이크로초 정밀도, Gitlab::InstrumentationHelper::DURATION_PRECISION
).
로그에서 타이밍을 추적하기 쉽게 만들기 위해 로그 키가 _s
를 접미사로 가지고, 이름에 duration
이 포함되어 있는지 확인하세요
(예: view_duration_s
).
다중 목적지 로깅
GitLab은 구조화된 로그에서 JSON 로그로 전환했습니다. 그러나 다중 목적지 로깅을 통해 로그를 여러 형식으로 기록할 수 있습니다.
다중 목적지 로깅 사용하는 방법
MultiDestinationLogger
에서 상속받는 새 로거 클래스를 만들고, LOGGERS
상수에 로거 배열을 추가하세요. 로거는 Gitlab::Logger
에서 상속받는 클래스여야 합니다. 예를 들어, 다음 예제의 사용자 정의 로거는 각각 Gitlab::Logger
와 Gitlab::JsonLogger
에서 상속받을 수 있습니다.
로거 중 하나를 primary_logger
로 지정해야 합니다. primary_logger
는 이 다중 목적지 로거에 대한 정보가 애플리케이션에서 표시될 때 사용됩니다
(예: Gitlab::Logger.read_latest
메서드를 사용할 때).
다음 예제는 정의된 LOGGERS
중 하나를 primary_logger
로 설정합니다.
module Gitlab
class FancyMultiLogger < Gitlab::MultiDestinationLogger
LOGGERS = [UnstructuredLogger, StructuredLogger].freeze
def self.loggers
LOGGERS
end
def primary_logger
UnstructuredLogger
end
end
end
이제 이 다중 로거에서 일반 로깅 메서드를 호출할 수 있습니다. 예를 들면:
FancyMultiLogger.info(message: "Information")
이 메시지는 FancyMultiLogger.loggers
에 등록된 각 로거에 의해 기록됩니다.
로깅을 위한 문자열 또는 해시 전달
MultiDestinationLogger
에 문자열 또는 해시를 전달할 때, 로그 줄은 설정된 LOGGERS
종류에 따라 다르게 형식화될 수 있습니다.
예를 들어, 이전 예제에서 로거를 부분적으로 정의해 봅시다:
module Gitlab
# AppTextLogger와 유사
class UnstructuredLogger < Gitlab::Logger
...
end
# AppJsonLogger와 유사
class StructuredLogger < Gitlab::JsonLogger
...
end
end
다음은 메시지가 두 로거에 의해 어떻게 처리될지에 대한 예입니다.
- 문자열을 전달할 때
FancyMultiLogger.info("Information")
# UnstructuredLogger
I, [2020-01-13T18:48:49.201Z #5647] INFO -- : Information
# StructuredLogger
{:severity=>"INFO", :time=>"2020-01-13T11:02:41.559Z", :correlation_id=>"b1701f7ecc4be4bcd4c2d123b214e65a", :message=>"Information"}
- 해시를 전달할 때
FancyMultiLogger.info({:message=>"This is my message", :project_id=>123})
# UnstructuredLogger
I, [2020-01-13T19:01:17.091Z #11056] INFO -- : {"message"=>"Message", "project_id"=>"123"}
# StructuredLogger
{:severity=>"INFO", :time=>"2020-01-13T11:06:09.851Z", :correlation_id=>"d7e0886f096db9a8526a4f89da0e45f6", :message=>"This is my message", :project_id=>123}
로그 컨텍스트 메타데이터 (Rails 또는 Grape 요청을 통한)
Gitlab::ApplicationContext
는 요청 생명 주기에서 메타데이터를 저장하며, 이는 웹 요청 또는 Sidekiq 로그에 추가될 수 있습니다.
API, Rails 및 Sidekiq 로그에는 이 컨텍스트 정보를 담고 있는 meta.
로 시작하는 필드가 포함되어 있습니다.
진입점은 다음에서 확인할 수 있습니다:
속성 추가하기
새 속성을 추가할 때는 위의 진입점 컨텍스트 내에서 노출되도록 해야 합니다.
-
with_context
(또는push
) 메서드에 해시로 전달합니다 (메서드 또는 변수가 즉시 평가되지 않아야 하는 경우 Proc를 전달해야 합니다). -
Gitlab::ApplicationContext
가 이러한 새로운 값을 수용하도록 변경합니다. -
새로운 속성이
Labkit::Context
에서 수용되도록 합니다.
Kibana에서 시각화를 생성하는 방법에 대한 추가 지식은 HOWTO: Sidekiq 메타데이터 로그 사용하기를 참조하세요.
컨텍스트의 필드는 현재 웹 요청을 통해 트리거된 Sidekiq 작업에 대해서만 기록됩니다. 자세한 내용은 후속 작업을 참조하세요.
로그 컨텍스트 메타데이터 (작업을 통한)
추가 메타데이터는 ApplicationWorker#log_extra_metadata_on_done
메서드를 사용하여 작업에 첨부할 수 있습니다. 이 메서드를 사용하면 메타데이터가 작업 완료 페이로드와 함께 나중에 Kibana에 기록됩니다.
class MyExampleWorker
include ApplicationWorker
def perform(*args)
# 작업이 수행됩니다.
# ...
# value의 내용은 Kibana의 `json.extra.my_example_worker.my_key` 아래에 나타납니다.
log_extra_metadata_on_done(:my_key, value)
end
end
이 예제를 참조하면 ExpireArtifactsWorker
의 각 실행마다 파괴된 아티팩트 수를 기록하는 방법을 확인할 수 있습니다.
예외 처리
예외를 포착하고 추적하고 싶을 때가 종종 발생합니다.
수동으로 예외를 기록하는 것은 허용되지 않으며, 그 이유는 다음과 같습니다:
-
수동으로 기록한 예외는 기밀 데이터를 유출할 수 있습니다.
-
수동으로 기록한 예외는 종종 백트레이스를 정리해야 하며, 이로 인해 보일러플레이트가 줄어듭니다.
-
수동으로 기록한 예외는 Sentry에 추적되어야 하는 경우가 많습니다.
-
수동으로 기록된 예외는
correlation_id
를 사용하지 않아 문제 발생 시 요청, 사용자 및 컨텍스트에 연결하기 어렵습니다. -
수동으로 기록된 예외는 종종 여러 파일에 분산되어 있어 모든 로그 파일을 스크랩하는 부담이 증가합니다.
중복을 피하고 일관된 동작을 보장하기 위해 Gitlab::ErrorTracking
은 예외를 추적하기 위한 도우미 메서드를 제공합니다:
-
Gitlab::ErrorTracking.track_and_raise_exception
: 이 메서드는 로그를 기록하고, 예외가 Sentry에 전송되는 경우(구성이 되어 있을 때) 예외를 다시 발생시킵니다. -
Gitlab::ErrorTracking.track_exception
: 이 메서드는 로그를 기록하고, 예외를 Sentry에 전송합니다(구성이 되어 있을 때). -
Gitlab::ErrorTracking.log_exception
: 이 메서드는 예외를 로그에만 기록하고, 예외를 Sentry에 보내지 않습니다. -
Gitlab::ErrorTracking.track_and_raise_for_dev_exception
: 이 메서드는 로그를 기록하고, 예외를 Sentry에 전송하며(구성이 되어 있을 때) 개발 및 테스트 환경을 위한 예외를 다시 발생시킵니다.
아래 예제에 제시된 것처럼 Gitlab::ErrorTracking.track_and_raise_exception
및 Gitlab::ErrorTracking.track_exception
만 사용하도록 권장합니다.
각 추적된 예외에 대한 더 많은 컨텍스트를 제공하기 위해 추가적인 매개변수를 추가하는 것을 고려하세요.
예시
class MyService < ::BaseService
def execute
project.perform_expensive_operation
success
rescue => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
error('예외가 발생했습니다')
end
end
class MyService < ::BaseService
def execute
project.perform_expensive_operation
success
rescue => e
Gitlab::ErrorTracking.track_and_raise_exception(e, project_id: project.id)
end
end
기본 로그 위치
Self-managed 사용자 및 GitLab.com의 경우, GitLab은 두 가지 방법으로 배포됩니다:
Omnibus GitLab 로그 처리
Omnibus GitLab은 /var/log/gitlab
내의 구성 요소별 디렉토리에서 로그를 기록합니다:
# ls -al /var/log/gitlab
total 200
drwxr-xr-x 27 root root 4096 Apr 29 20:28 .
drwxrwxr-x 19 root syslog 4096 Aug 5 04:08 ..
drwx------ 2 gitlab-prometheus root 4096 Aug 6 04:08 alertmanager
drwx------ 2 root root 4096 Aug 6 04:08 crond
drwx------ 2 git root 4096 Aug 6 04:08 gitaly
drwx------ 2 git root 4096 Aug 6 04:08 gitlab-exporter
drwx------ 2 git root 4096 Aug 6 04:08 gitlab-kas
drwx------ 2 git root 45056 Aug 6 13:18 gitlab-rails
drwx------ 2 git root 4096 Aug 5 04:18 gitlab-shell
drwx------ 2 git root 4096 May 24 2023 gitlab-sshd
drwx------ 2 git root 4096 Aug 6 04:08 gitlab-workhorse
drwxr-xr-x 2 root root 12288 Aug 1 00:20 lets-encrypt
drwx------ 2 root root 4096 Aug 6 04:08 logrotate
drwx------ 2 git root 4096 Aug 6 04:08 mailroom
drwxr-x--- 2 root gitlab-www 12288 Aug 6 00:18 nginx
drwx------ 2 gitlab-prometheus root 4096 Aug 6 04:08 node-exporter
drwx------ 2 gitlab-psql root 4096 Aug 6 15:00 pgbouncer
drwx------ 2 gitlab-psql root 4096 Aug 6 04:08 postgres-exporter
drwx------ 2 gitlab-psql root 4096 Aug 6 04:08 postgresql
drwx------ 2 gitlab-prometheus root 4096 Aug 6 04:08 prometheus
drwx------ 2 git root 4096 Aug 6 04:08 puma
drwxr-xr-x 2 root root 32768 Aug 1 21:32 reconfigure
drwx------ 2 gitlab-redis root 4096 Aug 6 04:08 redis
drwx------ 2 gitlab-redis root 4096 Aug 6 04:08 redis-exporter
drwx------ 2 registry root 4096 Aug 6 04:08 registry
drwx------ 2 gitlab-redis root 4096 May 6 06:30 sentinel
drwx------ 2 git root 4096 Aug 6 13:05 sidekiq
위의 예에서 알 수 있듯이, 다음 구성 요소는 다음 디렉토리에 로그를 저장합니다:
구성 요소 | 로그 디렉토리 |
---|---|
GitLab Rails | /var/log/gitlab/gitlab-rails |
Gitaly | /var/log/gitlab/gitaly |
Sidekiq | /var/log/gitlab/sidekiq |
GitLab Workhorse | /var/log/gitlab/gitlab-workhorse |
GitLab Rails 디렉토리는 위의 Ruby 코드와 함께 사용되는 로그 파일을 찾고자 할 때 아마도 살펴보아야 할 곳입니다.
logrotate
는 모든 *.log 파일을 감시하는 데 사용됩니다.
클라우드 네이티브 GitLab 로그 처리
클라우드 네이티브 GitLab 팟은 추가 하위 디렉토리를 생성하지 않고 GitLab 로그를 직접 /var/log/gitlab
에 기록합니다. 예를 들어, webservice
팟은 한 컨테이너에서 gitlab-workhorse
를 실행하고 다른 컨테이너에서 puma
를 실행합니다. 후자의 로그 파일 디렉토리는 다음과 같습니다:
git@gitlab-webservice-default-bbd9647d9-fpwg5:/$ ls -al /var/log/gitlab
total 181420
drwxr-xr-x 2 git git 4096 Aug 2 22:58 .
drwxr-xr-x 4 root root 4096 Aug 2 22:57 ..
-rw-r--r-- 1 git git 0 Aug 2 18:22 .gitkeep
-rw-r--r-- 1 git git 46524128 Aug 6 20:18 api_json.log
-rw-r--r-- 1 git git 19009 Aug 2 22:58 application_json.log
-rw-r--r-- 1 git git 157 Aug 2 22:57 auth_json.log
-rw-r--r-- 1 git git 1116 Aug 2 22:58 database_load_balancing.log
-rw-r--r-- 1 git git 67 Aug 2 22:57 grpc.log
-rw-r--r-- 1 git git 0 Aug 2 22:57 production.log
-rw-r--r-- 1 git git 138436632 Aug 6 20:18 production_json.log
-rw-r--r-- 1 git git 48 Aug 2 22:58 puma.stderr.log
-rw-r--r-- 1 git git 266 Aug 2 22:58 puma.stdout.log
-rw-r--r-- 1 git git 67 Aug 2 22:57 service_measurement.log
-rw-r--r-- 1 git git 67 Aug 2 22:57 sidekiq_client.log
-rw-r--r-- 1 git git 733809 Aug 6 20:18 web_exporter.log
gitlab-logger
는 /var/log/gitlab
의 모든 파일을 tail 처리하는 데 사용됩니다. 각 로그 라인은 필요한 경우 JSON으로 변환되어 stdout
로 전송되므로 kubectl logs
를 통해 확인할 수 있습니다.
새로운 로그 파일에 대한 추가 단계
-
로그 보존 설정을 고려하세요. 기본적으로 Omnibus는
/var/log/gitlab/gitlab-rails/*.log
에 있는 모든 로그를 매시간 회전시키고 최대 30개의 압축 파일을 보관합니다. GitLab.com에서는 그 설정이 오직 6개의 압축 파일만 보관합니다. 이 설정은 대부분의 사용자에게 적합해야 하지만, Omnibus GitLab에서 세부 조정이 필요할 수 있습니다. -
GitLab.com에서는 GitLab Rails에 의해 생성된 모든 새로운 JSON 로그 파일이 자동으로 Elasticsearch로 전송되며 (Kibana에서 확인 가능), GitLab Rails Kubernetes 팟에 있습니다. Gitaly 노드에서 파일을 전달해야 하는 경우 프로덕션 트래커에 이슈를 제출하거나
gitlab_fluentd
프로젝트에 병합 요청을 제출하세요. 이 예제를 참조하세요. -
GitLab CE/EE 문서와 GitLab.com 런북을 반드시 업데이트하세요.
Kibana에서 새로운 로그 파일 찾기 (GitLab.com 전용)
GitLab.com에서는 GitLab Rails에서 생성된 모든 새로운 JSON 로그 파일이 자동으로 Elasticsearch로 전송되며 (Kibana에서 사용 가능), GitLab Rails Kubernetes 팟에서 사용됩니다. Kibana의 json.subcomponent
필드를 사용하면 서로 다른 종류의 로그 파일로 필터링할 수 있습니다. 예를 들어 production_json.log
에서 전달된 항목의 경우 json.subcomponent
는 production_json
이 됩니다.
또한 Web/API 팟에서의 로그 파일은 Sidekiq 팟에서의 로그 파일과는 다른 인덱스로 전송된다는 점도 주목할 가치가 있습니다. 로그를 남기는 위치에 따라 다른 인덱스 패턴에서 로그를 찾을 수 있습니다.
로깅 가시성 제어
로그의 증가로 인해 미인정된 메시지의 누적이 발생할 수 있습니다. 새로운 로그 메시지를 추가할 때 전체 로깅 볼륨이 10% 이상 증가하지 않도록 하세요.
사용 중단 알림
예상되는 사용 중단 알림의 볼륨이 큰 경우:
- 개발 환경에서만 로그를 남깁니다.
- 필요하다면 테스트 환경에서도 로그를 남깁니다.