블로그 | NGINX

NGINX JavaScript 모듈을 사용한 진단 로깅

NGINX-F5-수평-검정-유형-RGB의 일부
리엄 크릴리 썸네일
리암 크릴리
2020년 9월 29일 게시

 

오류 로그를 조정하지 않고 프로덕션에서 문제 해결

편집자 - 이 블로그는 NGINX 및 NGINX Plus를 사용한 로깅을 논의하는 여러 블로그 중 하나입니다. 다음도 참조하세요.

또한 NGINX JavaScript 모듈의 사용 사례에 대한 많은 블로그 중 하나이기도 합니다. 전체 목록은 NGINX JavaScript 모듈의 사용 사례를 참조하세요.

NGINX는 모든 규모의 조직이 미션 크리티컬 웹사이트, 애플리케이션, API를 실행하는 데 도움을 줍니다. 규모와 배포 인프라 선택에 관계없이 프로덕션에서 실행하는 것은 쉽지 않습니다. 이 글에서는 프로덕션 배포의 어려운 점 중 하나인 로깅에 대해 이야기해보겠습니다. 더 구체적으로, 불필요한 데이터에 압도당하지 않으면서도 문제 해결을 위해 적절한 양의 자세한 로그를 수집하는 균형 잡힌 방법에 대해 논의합니다.

로깅 기본 사항

NGINX는 클라이언트 요청에 대한 액세스 로깅 과 문제가 발생할 경우를 위한 오류 로깅이라는 두 가지 로깅 메커니즘을 제공합니다. 이러한 메커니즘은 HTTP와 스트림(TCP/UDP) 모듈에서 모두 사용할 수 있지만, 여기에서는 HTTP 트래픽에 초점을 맞춥니다. ( 디버그 심각도 수준을 사용하는 세 번째 로깅 메커니즘도 있지만, 여기서는 다루지 않겠습니다.)

일반적인 기본 NGINX 로깅 구성은 다음과 같습니다.

http { 
log_format main 
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main; # 'main' 형식을 사용하여 기록
error_log /var/log/nginx/error.log warn; # 'warn' 심각도 수준까지 기록
...
}

log_format 지시어는 access_log 지시어가 구성에 포함될 때 생성되는 로그 항목의 내용과 구조를 설명합니다. 위의 예는 많은 웹 서버에서 사용하는 CLF( Common Log Format )의 확장입니다. error_log 지시어를 사용하면 기록할 메시지의 심각도 수준을 지정하지만, 고정된 항목의 내용이나 형식은 지정하지 않습니다. 자세한 내용은 다음 섹션에서 설명하겠습니다.

NGINX 로깅의 다른 주목할 만한 측면은 다음과 같습니다.

  • 로깅 지침은 하위 수준 구성 컨텍스트에 자동으로 상속됩니다. 예를 들어, http 컨텍스트의 access_log 지시문은 모든 server{} 블록에 적용됩니다.
  • 자식 컨텍스트의 로깅 지시문은 상속된 지시문을 재정의합니다.
  • 동일한 컨텍스트에 여러 개의 로깅 지침이 존재할 수 있습니다. 예를 들어, 두 개의 access_log 지시어를 사용하여 표준 CLF 로그 파일과 더 자세한 두 번째 로그를 모두 생성할 수 있습니다.

생산에서의 로깅의 현실

일반적으로 액세스 로그를 사용하여 분석 및 사용 통계를 제공하고, 오류 로그를 사용하여 장애 감지 및 문제 해결을 시도합니다. 하지만 생산 시스템을 운영하는 것은 그렇게 간단한 일이 아닙니다. 다음은 몇 가지 일반적인 문제입니다.

  • 액세스 로그에는 문제 해결에 필요한 충분한 세부 정보가 없습니다.
  • 오류 로그는 정보 심각도 수준에서 세부 정보를 잘 공개하지만 일반적인 작업에는 너무 자세합니다.
  • 오류 로그 형식은 고정되어 있으므로 특정 관심 변수를 포함하도록 사용자 정의할 수 없습니다.
  • 오류 로그의 항목에는 요청 컨텍스트가 포함되지 않으며 해당 액세스 로그 항목과 일치시키기 어렵습니다.

게다가, 프로덕션 환경에서 로깅 세부 정보를 추가하거나 제거하기 위해 NGINX 구성을 변경하려면 변경 제어 프로세스를 거쳐 구성을 다시 배포해야 할 수도 있습니다. 완전히 안전하지만 "왜 급증이 나타나는가"와 같은 라이브 문제를 해결할 때는 다소 번거롭습니다. 4엑스 / 5xx 오류?”. 물론, 클러스터 전반에서 동일한 트래픽을 처리하는 NGINX 인스턴스가 여러 개 있는 경우 이러한 문제는 더욱 커집니다.

오류에 대한 두 번째 액세스 로그 사용

각 요청에 대해 수집된 데이터를 풍부하게 하기 위해 액세스 로그의 형식을 사용자 지정하는 것은 향상된 분석을 위한 일반적인 접근 방식이지만 진단이나 문제 해결에는 확장성이 좋지 않습니다. 기본 액세스 로그에 두 가지 작업을 요청하는 것은 인위적인 해결책입니다. 왜냐하면 일반적인 분석보다 문제 해결을 위해 필요한 정보가 훨씬 더 많기 때문입니다. 주요 액세스 로그에 수많은 변수를 추가하면 가끔만 유용한 데이터로 인해 로그 볼륨이 크게 늘어날 수 있습니다.

대신 두 번째 액세스 로그를 사용하여 디버깅이 필요한 오류가 발생할 때만 로그에 기록할 수 있습니다. access_log 지시문은 if 매개변수를 사용하여 조건부 로깅을 지원합니다. 즉, 지정된 변수가 0이 아니고 비어 있지 않은 값으로 평가되는 경우에만 요청이 로깅됩니다.

map $status $is_error { 400 1; # 잘못된 요청, 만료된 클라이언트 인증서 포함 495 1; # 클라이언트 인증서 오류 502 1; # 잘못된 게이트웨이(업스트림 서버를 선택할 수 없음) 504 1; # 게이트웨이 시간 초과(선택한 업스트림에 연결할 수 없음) default 0; } access_log /var/log/nginx/access_debug.log access_debug if=$is_error; # 진단 로깅 access_log /var/log/nginx/access.log main;

이 구성을 사용하면 $status 변수를 블록을 통해 전달하여 $is_error 변수의 값을 확인한 다음, 이 값을 access_log 지시문의 if 매개변수로 평가합니다. $is_error가 다음과 같이 평가되는 경우1 access_debug.log 파일에 특별한 로그 항목을 기록합니다.

그러나 이 구성은 궁극적으로 해결되어 상태가 되는 요청 처리 중에 발생한 오류를 감지하지 못합니다.200 좋아요 . 이러한 예 중 하나는 NGINX가 여러 개의 업스트림 서버 간에 로드 밸런싱을 수행하는 경우입니다. 선택된 서버에서 오류가 발생하면 NGINX는 proxy_next_upstream 지시문에서 구성된 조건에 따라 요청을 다음 서버로 전달합니다. 업스트림 서버 중 하나가 성공적으로 응답하는 한 클라이언트는 상태가 기록되는 성공적인 응답을 받습니다.200 . 그러나 재시도로 인해 사용자 경험이 좋지 않을 수 있으며, 업스트림 서버가 정상적이지 않다는 사실을 바로 알아채지 못할 수도 있습니다. 결국 우리는 로그인했습니다200 .

NGINX가 여러 개의 업스트림 서버로 프록시를 시도하는 경우, 해당 주소는 모두 $upstream_addr 변수에 캡처됩니다. 다른 $upstream_* 변수에도 동일한 것이 적용되는데, 예를 들어 $upstream_status 는 시도한 각 서버의 응답 코드를 캡처합니다. 따라서 이러한 변수에 여러 항목이 있으면 뭔가 나쁜 일이 일어났다는 것을 알 수 있습니다. 즉, 적어도 업스트림 서버 중 하나에 문제가 있을 가능성이 큽니다.

요청이 여러 개의 상류 서버로 프록시된 경우에도 access_debug.log 에 기록하는 건 어떨까요?

map $upstream_status $multi_upstreams { "~," 1; # 쉼표가 있음 default 0; } map $status $is_error { 400 1; # 잘못된 요청, 만료된 클라이언트 인증서 포함 495 1; # 클라이언트 인증서 오류 502 1; # 잘못된 게이트웨이(업스트림 서버를 선택할 수 없음) 504 1; # 게이트웨이 시간 초과(선택한 업스트림에 연결할 수 없음) default $multi_upstreams; # 두 개 이상의 업스트림 서버를 시도한 경우 } access_log /var/log/nginx/access_debug.log access_debug if=$is_error; # 진단 로깅 access_log /var/log/nginx/access.log main; # 일반 로깅

여기서는 또 다른 블록을 사용하여 $multi_upstreams 를 생성합니다. 이 변수의 값은 $upstream_status 에 쉼표( , )가 있는지 여부에 따라 달라집니다. 쉼표는 상태 코드가 두 개 이상 있고, 따라서 두 개 이상의 업스트림 서버가 발견되었음을 의미합니다. 이 새로운 변수는 $status가 나열된 오류 코드 중 하나가 아닐 때 $is_error 의 값을 결정합니다.

지금은 일반 액세스 로그와 오류가 있는 요청을 담은 특수 access_debug.log 파일이 있지만, 아직 access_debug 로그 형식은 정의하지 않았습니다. 이제 문제를 진단하는 데 필요한 모든 데이터가 access_debug.log 파일에 있는지 확인해 보겠습니다.

JavaScript를 사용한 JSON 로깅

진단 데이터를 access_debug.log 에 입력하는 것은 어렵지 않습니다. NGINX는 HTTP 처리와 관련된 100개 이상의 변수를 제공하며, 필요한 만큼의 변수를 캡처하는 특수한 log_format 지시문을 정의할 수 있습니다. 하지만 이러한 목적에 맞춰 단순한 로그 형식을 구축하는 데는 몇 가지 단점이 있습니다.

  • 사용자 정의 형식이므로 로그 파서가 이를 읽는 방법을 학습해야 합니다.
  • 항목은 라이브 문제 해결 중에 사람이 읽기에는 매우 길고 어려울 수 있습니다.
  • 항목을 해석하려면 지속적으로 로그 형식을 참조해야 합니다.
  • "모든 요청 헤더"와 같은 비결정적 값을 기록하는 것은 불가능합니다.

NGINX JavaScript 모듈<.htmla> (njs)을 사용하여 JSON과 같은 구조화된 형식으로 로그 항목을 작성하면 이러한 과제를 해결할 수 있습니다. JSON 형식은 다음과 같은 로그 처리 시스템에서도 널리 지원됩니다. 스플렁크, 로그스태시, 그레이로그, 그리고 로글리. log_format 구문을 JavaScript 함수로 오프로드하면 기본 JSON 구문의 이점을 누릴 수 있으며, 모든 NGINX 변수와 njs ' r ' 객체 의 추가 데이터에 접근할 수 있습니다.

js_import conf.d/json_log.js;js_set $json_debug_log json_log.debugLog;

log_format access_debug escape=none $json_debug_log; # njs로 오프로드 
access_log /var/log/nginx/access_debug.log access_debug if=$is_error;

js_import 지시어는 JavaScript 코드가 포함된 파일을 지정하고 이를 모듈로 가져옵니다. 코드 자체는 여기에서 찾을 수 있습니다. access_debug 로그 형식을 사용하여 액세스 로그 항목을 작성할 때마다 $json_debug_log 변수가 평가됩니다. 이 변수는 js_set 지시문에서 정의한 debugLog JavaScript 함수를 실행하여 평가됩니다.

JavaScript 코드와 NGINX 구성을 결합하면 다음과 같은 진단 로그가 생성됩니다.

$ tail --lines=1 /var/log/nginx/access_debug.log | jq { "타임스탬프": "2020-09-21T11:25:55+00:00", "연결": { "요청_수": 1, "경과 시간": 0.555, "파이프라인": false, "ssl": { "프로토콜": "TLSv1.2", "암호": "ECDHE-RSA-AES256-GCM-SHA384", "세션_ID": "b302f76a70dfec92f6bd72de5732692481ebecbbc69a4d81c900ae4dc928485c", "세션_재사용": 거짓, "클라이언트_인증서": { "상태": "없음" } } }, "요청": { "클라이언트": "127.0.0.1", "포트": 443, "호스트": "foo.example.com", "메서드": "GET", "uri": "/one", "http_version": 1.1, "수신된 바이트": 87, "헤더": { "호스트": "foo.example.com:443", "사용자 에이전트": "curl/7.64.1", "수락": "*/*" } }, "업스트림": [ { "서버 주소": "10.37.0.71", "서버 포트": 443, "connect_time": null, "header_time": null, "response_time": 0.551, "전송된 바이트": 0, "수신된 바이트": 0, "상태": 504 }, { "서버 주소": "10.37.0.72", "서버 포트": 443, "연결_시간": 0.004, "헤더_타임": 0.004, "응답 시간": 0.004, "전송된 바이트": 92, "수신된 바이트": 4161, "상태": 200 } ], "응답": { "상태": 200, "전송된 바이트": 186, "헤더": { "콘텐츠 유형": "텍스트/html", "콘텐츠 길이": "4161" } } }

JSON 형식을 사용하면 전반적인 HTTP 연결(SSL/TLS 포함), 요청, 업스트림, 응답과 관련된 정보에 대해 별도의 객체를 가질 수 있습니다. 첫 번째 업스트림(10.37.0.71)이 상태를 반환한 방식에 주목하세요. 504 (게이트웨이 타임아웃) NGINX가 성공적으로 응답한 다음 업스트림(10.37.0.72)을 시도하기 전에. 0.5초 시간 초과( 업스트림 객체의 첫 번째 요소에서 response_time 으로 보고됨)는 이 성공적인 응답에 대한 전체 지연 시간( 연결 객체에서 elapsed_time 으로 보고됨)의 대부분을 차지합니다.

다음은 만료된 클라이언트 인증서로 인해 발생한 클라이언트 오류에 대한 (잘린) 로그 항목의 또 다른 예입니다.

{ 
"타임스탬프": "2020-09-22T10:20:50+00:00",
"연결": {
"ssl": {
"프로토콜": "TLSv1.2",
"암호": "ECDHE-RSA-AES256-GCM-SHA384",
"세션_ID": "30711efbe047c38a98c2209cc4b5f196988dcf2d7f1f2c269fde7269c370432e",
"세션_재사용": 거짓,
"클라이언트_인증서": {
"상태": "실패: 인증서가 만료되었습니다",
"serial": "1006",
"지문": "0c47cc4bd0fefbc2ac6363345cfbbf295594fe8d",
"제목": "이메일 주소=liam@nginx.com,CN=test01,OU=데모 CA,O=nginx,ST=CA,C=US",
"발급자": "CN=데모 중간 CA, OU=데모 CA, O=nginx, ST=CA, C=US",
"시작": "9월 20일 12:00:11 2019 GMT",
"만료": "9월 20일 12:00:11 2020 GMT",
"만료됨": true,
...
"응답": {
"상태": 400,
"바이트_전송": 283,
"헤더": {
"콘텐츠 유형": "텍스트/html",
"콘텐츠 길이": "215"
}
}

요약

오류가 발생할 때만 풍부한 진단 데이터를 생성함으로써 재구성을 수행하지 않고도 실시간으로 문제를 해결할 수 있습니다. 마지막 바이트가 클라이언트로 전송된 후 로깅 단계에서 오류가 감지될 때만 JavaScript 코드가 실행되므로 요청이 성공하더라도 영향을 받지 않습니다.

전체 구성은 GitHub 에서 제공됩니다. 여러분 환경에서 시도해보시는 건 어떨까요? 아직 NGINX Plus를 사용하고 있지 않다면 오늘 무료 30일 체험판을 시작하거나 당사에 문의하여 사용 사례에 대해 논의해 보세요 .


"이 블로그 게시물에는 더 이상 사용할 수 없거나 더 이상 지원되지 않는 제품이 참조될 수 있습니다. 사용 가능한 F5 NGINX 제품과 솔루션에 대한 최신 정보를 보려면 NGINX 제품군을 살펴보세요. NGINX는 이제 F5의 일부가 되었습니다. 이전의 모든 NGINX.com 링크는 F5.com의 유사한 NGINX 콘텐츠로 리디렉션됩니다."