Software Workshop

Loki에 저장된 로그 데이터 탐색

curl을 이용해서 Loki에 저장된 로그 데이터를 탐색해보는 방법에 대해서 소개한다.

일반적으로 Loki의 데이터는 Grafana 웹콘솔을 이용해서 시각적으로 범위를 좁혀가면서 조회할 수 있는데, 여러 가지 이유로 웹을 이용한 접근이 어려울 때 데이터 탐색이 상당히 어려워진다.

Loki가 localhost에 설치되었다고 가정하자. 그렇다면 접근 경로는 다음과 같이 결정된다.
http://localhost:3100/loki/api/v1/
외부에서 접근해야 한다면 localhost 대신에 IP나 도메인 이름을 사용하면 된다.

Label 탐색

우선 어떤 데이터가 어떻게 적재되고 있는지 거시적인 관점에서 스키마를 살펴볼 필요가 있다. Prometheus/Grafana 생태계는 스키마 정보에서 label이 중요한데, 어떤 label을 가지고 있는지 살펴본다.

labels와 label API를 이용하도록 한다.

curl -G http://localhost:3100/loki/api/v1/labels

아래 결과는 k8s의 pod에서 발생한 로그들의 공통적으로 가지는 속성들에 해당한다. 그러므로 애플리케이션에 따라 속성들이 달라지고 선택 가능한 label의 이름도 달라진다.

{
  "status": "success",
  "data": [
    "app",
    "component",
    "container",
    "filename",
    "instance",
    "job",
    "namespace",
    "node_name",
    "pod",
    "stream"
  ]
}

Loki가 기본적으로 제공하는 label에는 job, instance, namespace, app, level, severity, pod, container, host 등이 있는데, 이 중에서 가장 큰 단위는 namespace 또는 app, job이 될 것이다. job label 하나만으로 식별하거나 namespace와 app을 합쳐서 식별하는 방법을 권장한다.

해당 label이 가질 수 있는 값을 살펴봐야 필터링을 하거나 aggregation을 할 수 있다.

curl -G http://localhost:3100/loki/api/v1/label/app/values
{
  "status": "success",
  "data": [
    "argocd/argocd-application-controller",
    "argocd/argocd-applicationset-controller",
    "argocd/argocd-repo-server",
    "argocd/argocd-server",
    "blog/wordpress",
    "feedmaker/fm-frontend-app",
    "feedmaker/mysql",
    "feedmaker/nginx",
    "ingress/nginx-ingress-microk8s-controller",
    "knative-serving/3scale-kourier-gateway",
    "knative-serving/knative-serving",
    "kube-system/calico-kube-controllers",
    "kube-system/calico-node",
    "kube-system/dashboard-metrics-scraper",
    "minio-operator/minio-operator",
    "monitoring/grafana",
    "node",
    "observability/grafana",
    "observability/loki",
    "observability/prometheus",
    "observability/tempo",
    "wiki/wiki"
  ]
}

이런 방식으로 어떤 label을 사용할지, 해당 label의 어떤 값을 기준으로 삼을지 정한 다음에 질의를 보내서 데이터를 조회할 수 있다.

로그 데이터 조회

질의를 보내서 조회하는 방법은 query API를 이용하는 것이다. 다음 예제는 app이 nginx ingress controller인 경우로만 제한해서 최근 12시간의 로그 데이터를 조회하는 것이다.

start=$(date -d "-12 hour" +"%Y-%m-%dT%H:%M:%S%:z")
end=$(date +"%Y-%m-%dT%H:%M:%S%:z")
curl -G 'http://localhost:3100/loki/api/v1/query_range' \
  --data-urlencode 'query={app="nginx-ingress-microk8s-controller"}' \
  --data-urlencode 'start='"$start" \
  --data-urlencode 'end='"$end"

조회 결과가 너무 복잡하면, curl 명령 뒤에 | jq 를 덧붙이면 된다.

{
  "status": "success",
  "data": {
    "resultType": "streams",
    "result": [
      {
        "stream": {
          "job": "ingress/nginx-ingress-microk8s-controller",
          "namespace": "ingress",
          "node_name": "yicho-ser7",
          "pod": "nginx-ingress-microk8s-controller-94sr2",
          "stream": "stdout",
          "app": "nginx-ingress-microk8s-controller",
          "container": "nginx-ingress-microk8s",
          "filename": "/var/log/pods/ingress_nginx-ingress-microk8s-controller-94sr2_892a7e48-2e1a-4da8-a7cd-894383350d39/nginx-ingress-microk8s/7.log"
        },
        "values": [
          [
            "1708480002043190387",
            "39.98.44.46 - - [21/Feb/2024:01:46:42 +0000] \"POST /wp-login.php HTTP/1.1\" 503 3160 \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36\" 432 0.072 [blog-wp-wordpress-80] [] 10.1.202.106:8080 3160 0.021 503 08a415b2598aaeaa304c3a2c741df74c"
          ],
          [
            "1708479993394236200",
            "185.191.171.4 - - [21/Feb/2024:01:46:33 +0000] \"GET /?p=192 HTTP/1.1\" 200 16804 \"-\" \"Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)\" 292 0.050 [blog-wp-wordpress-80] [] 10.1.202.106:8080 16804 0.050 200 c21fd7d9fc77ca7ce3ad0191d7f6abd3"
          ],
          [
            "1708479191761829066",
            "219.255.217.250 - - [21/Feb/2024:01:33:11 +0000] \"GET /alt.png HTTP/2.0\" 200 7449 \"https://wiki.terzeron.com/OS_%EC%9D%BC%EB%B0%98_%EC%8B%9C%EC%8A%A4%ED%85%9C/Docker/container%EC%97%90%EC%84%9C_systemctl_%EB%AA%85%EB%A0%B9_%EC%82%AC%EC%9A%A9%EC%8B%9C_Failed_to_get_D-Bus_connection_Operation_not_permitted_%EC%97%90%EB%9F%AC_%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36\" 27 0.003 [wiki-sw-wiki-80] [] 10.1.202.72:3000 7449 0.003 200 81759b2b3f53da618d4cdfbde55f8c9a"
          ]
        ]
      }
    ],
    "stats": {
      "summary": {
        "bytesProcessedPerSecond": 163555569,
        "linesProcessedPerSecond": 437338,
        "totalBytesProcessed": 425962,
        "totalLinesProcessed": 1139,
        "execTime": 0.002604387,
        "queueTime": 3.26e-05,
        "subqueries": 1,
        "totalEntriesReturned": 100
      },
      "querier": {
        "store": {
          "totalChunksRef": 9,
          "totalChunksDownloaded": 9,
          "chunksDownloadTime": 197634,
          "chunk": {
            "headChunkBytes": 0,
            "headChunkLines": 0,
            "decompressedBytes": 147931,
            "decompressedLines": 488,
            "compressedBytes": 193578,
            "totalDuplicates": 0
          }
        }
      },
      "ingester": {
        "totalReached": 1,
        "totalChunksMatched": 1,
        "totalBatches": 1,
        "totalLinesSent": 100,
        "store": {
          "totalChunksRef": 3,
          "totalChunksDownloaded": 3,
          "chunksDownloadTime": 128928,
          "chunk": {
            "headChunkBytes": 190374,
            "headChunkLines": 426,
            "decompressedBytes": 87657,
            "decompressedLines": 225,
            "compressedBytes": 108942,
            "totalDuplicates": 0
          }
        }
      }
    }
  }
}          

참고자료

https://grafana.com/docs/loki/latest/query/