curl을 이용해서 Loki에 저장된 로그 데이터를 탐색해보는 방법에 대해서 소개한다.
일반적으로 Loki의 데이터는 Grafana 웹콘솔을 이용해서 시각적으로 범위를 좁혀가면서 조회할 수 있는데, 여러 가지 이유로 웹을 이용한 접근이 어려울 때 데이터 탐색이 상당히 어려워진다.
Loki가 localhost에 설치되었다고 가정하자. 그렇다면 접근 경로는 다음과 같이 결정된다.
http://localhost:3100/loki/api/v1/
외부에서 접근해야 한다면 localhost 대신에 IP나 도메인 이름을 사용하면 된다.
우선 어떤 데이터가 어떻게 적재되고 있는지 거시적인 관점에서 스키마를 살펴볼 필요가 있다. 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
}
}
}
}
}
}