Webhook을 수신하는 서버의 요청 로그를 nginx, Filebeat, Logstash, Elasticsearch에서 수집

마에오키



거의 자신을위한 메모.
 

채팅봇을 만들려면 Webhook을 받는 너를 만들어야 한다.
보통이라면, 망설이지 않고 AWS Lambda와 API Gateway와 CloudWatch logs를 사용한다고 생각하고, 그것이 좋다.

다만 이번에는 여러 사정으로 Microsoft의 Azure를 사용해야 했다.

Azure Functions 사용하면 괜찮습니까? 라고 생각할지도 모른다. 아무리 어디 언제는 (익숙하지 않은 자신에게는) 매우 사용하기 어렵다. AWS Lambda의 돈을 주면 좋을 텐데, 왜 이런 것을 마이크로소프트는 릴리스 했어?
레퍼런스나 튜토리얼이 죽을 만큼 알기 힘들고, 모든 동기 부여를 가지고 갔기 때문에, 포기하고 Azure VM으로 서버 세워서, 거기에서 Webhook 받아 돌려주는 너를 만들기로 했다.

그 때, 웹 어플리케이션 측에서 로그를 취득하는 것만으로는, 뭔가의 박자에 어플리케이션 서버가 에러로 떨어지거나 하면 로그를 흘리게 되기 때문에, 전단의 nginx측에서, Webhook의 로그(JSON)를 전부 남길 수 있도록 했다.



nginx에서 JSON 요청 본문 기록



기본 LogFormat은
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

이런 식으로 POST한 내용은 남겨주지 않는다.

그래서 일단 JSON 바디를 남기기 위해

/etc/nginx/conf.d/webhook.conf
log_format with_body '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for" "$request_body"';

server {
    listen       443 ssl;
    server_name  ....;
    ssl_certificate /etc/letsencrypt/live/..../fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/..../privkey.pem;

    set_real_ip_from   '10.0.0.0/8';
    real_ip_header     X-Forwarded-For;
    proxy_set_header   X-Forwarded-Scheme $http_x_forwarded_proto;
    proxy_set_header   Host $host;
    resolver           127.0.0.11 valid=5s;

    location /webhook {
        if ($request_method !~ ^(POST)$ ) {
            return 405;
        }

        access_log /var/log/nginx/post_webhook.log with_body;
        proxy_pass http://localhost:8080;
    }
}

이런 느낌으로 $request_body 첨부의 LogFormat를 사용하도록 nginx의 conf에 쓴다. (server나 location 안에 log_format 지시어는 쓸 수 없어, 이 위치)

Filebeat에서 로그 파일의 위치 사용자 정의



Filebeat는 디폴트라고 /var/log/nginx/access.log 를 보러 가게 되어 있으므로,

/etc/filebeat/modules.d/nginx.yml
- module: nginx
  # Access logs
  access:
    enabled: true

    var.paths:
      - /var/log/nginx/post_webhook.log

이런 식으로 한다.

Logstash에서 JSON 문자열을 형식화



이것이 이번 가운데 제일 빠졌다.

nginx로 기록한 JSON은 이스케이프 처리되었으며,
{\x22text\x22:\x22abscd\x22,\x22textFormat\x22:\x22plain\x22,\x22type\x22:\x22message\x22,\x22timestamp\x22:\x222019-10-04T18:35:27.5194926Z\x22,\x22localTimestamp\x22:\x222019-10-05T03:35:27.5194926+09:00\x22,\x22id\x22:\x221570214127504\x22,\x22channelId\x22:\x22msteams\x22,\x22serviceUrl\x22:\x22https://smba.trafficmanager.net/apac/\x22,\x22from\x22:{\x22id\x22:\x2229:1EaRHI2dlI.....WPdO92g\x22,\x22name\x22:\x22\xE5\xB2\xA9\xE6\x9C\xA8 \xE7\xA5\x90\xE8\xBC\x94\x22,\x22aadObjectId\x22:\x22541478d2-6a03-47b3-aeb7-8675cdd45db3\x22},\x22conversation\x22:{\x22conversationType\x22:\x22personal\x22,\x22tenantId\x22:\x2215b67c2a-8e8f-4a5d-8e1d-186e9cf06150\x22,\x22id\x22:\x22a:1bDh2aVy.......j5FUCYYG\x22},\x22recipient\x22:{\x22id\x22:\x2228:d5d5e.....6a6\x22,\x22name\x22:\x22CSE bot\x22},\x22entities\x22:[{\x22locale\x22:\x22ja-JP\x22,\x22country\x22:\x22JP\x22,\x22platform\x22:\x22Mac\x22,\x22type\x22:\x22clientInfo\x22}],\x22channelData\x22:{\x22tenant\x22:{\x22id\x22:\x2215b67c2......06150\x22}},\x22locale\x22:\x22ja-JP\x22}
이런 느낌이 되고 있다. JSON으로 해석하려면 \x22"로 변환 (백 슬래시 이스케이프 해제)해야합니다.

htps //w w. 에스 c. 코/구이데/엔/ぉgs한 sh/쿤 t/후ぃl rp ㎅긴 s. HTML 를 본 느낌, unescape 같은 필터는 (아마) 없다.

Ruby 필터를 사용하여 이스케이프 해제





Ruby 스크립트를 사용해 자유롭게 변환 처리를 쓸 수 있는 필터가 있었으므로, (퍼포먼스적으로 어떨까?라고는 신경이 쓰이면서도) 이것을 사용하기로 했다.

파이썬이라고 .decode("string_escape")

h tps : // s t c ゔ ぇ rf ぉ w. 코 m / 쿠에 s 치온 s / 8639642 / 모든 st와 y-e s ぺ- an d une s 또는 pe st rin gs-

음,, eval인가·····, , 우선 eval %Q{"#{s}"} 를 하는 처리로 일단 해 보는 것에.

변환 처리는 이벤트 API 라고 하는 녀석을 사용해 쓴다. ver 7.3 시점이라면
  • event.get('[nginx][access][response_body]') 에서 [nginx][access][response_body] 에 들어있는 것을 읽고,
  • event.set('[nginx][access][raw_response_body]', raw_response)[nginx][access][raw_response_body]raw_response 의 값을 쓸 수 있습니다

  • 같은 느낌인 것 같다. 굉장히 무리하지만,
          ruby {
            code => "body = event.get('[nginx][access][request_body]'); raw_body = eval %Q{\"#{body}\"}; event.set('raw_request_body', raw_body)"
            remove_field => "[nginx][access][request_body]"
          }
    

    이런 필터를 쓴다.

    Elasticsearch는 시작하기 만하면 괜찮습니다.



    특히 아무것도 하지 않았다.

    완성


    http GET http://localhost:9200/_search 해보면 ...



    이런 식으로, JSON이 퍼스되어 Elasticsearch에 붙잡혀 있었다. 우이

    좋은 웹페이지 즐겨찾기