Skip to content

Logging Query Language & Debug Patterns

Tại sao LQL là kỹ năng không thể thiếu

Logs Explorer là công cụ bạn dùng mỗi ngày khi có incident. Trong những moment đó, tốc độ tìm ra root cause phụ thuộc trực tiếp vào khả năng viết query nhanh và chính xác.

Logging Query Language (LQL) không phải SQL, không phải regex, không phải Prometheus PromQL — nó có syntax riêng với semantics đặc thù. Hiểu đúng cách indexing hoạt động, tại sao một query nhanh hơn query khác, và cách dùng các hàm đặc biệt như sample()source() là sự khác biệt giữa tìm được nguyên nhân trong 5 phút hay 50 phút.


Internal Model: Cách LQL Evaluate Queries

Index và non-indexed fields

Cloud Logging index một số fields nhất định để tăng tốc query. Khi bạn filter theo indexed field, query chạy nhanh hơn và tốn ít resource hơn.

Fields được index (fast lookup):

  • resource.type — quan trọng nhất, luôn filter theo field này khi có thể
  • resource.labels.* — tất cả resource label keys
  • logName — tên log (ví dụ: cloudaudit.googleapis.com/activity)
  • severity — log severity level
  • timestamp — luôn được index (range scan)
  • insertId — unique entry ID

Fields KHÔNG được index (full scan):

  • textPayload — plain text payload
  • jsonPayload.* (hầu hết subfields) — JSON fields thường không được index
  • labels.* — custom labels không được index trừ một số trường hợp

Hệ quả thực tế: Query resource.type="k8s_container" AND jsonPayload.level="ERROR" sẽ chạy nhanh hơn đáng kể so với jsonPayload.level="ERROR" không có resource filter, vì phần resource.type sử dụng index để thu hẹp tập entry cần scan.


Cú Pháp Cơ Bản LQL

Field paths và toán tử

# Equality
resource.type = "gce_instance"
severity = "ERROR"

# Inequality
severity != "DEBUG"
httpRequest.status != 200

# Comparison (numeric)
httpRequest.status >= 500
httpRequest.status < 600

# Contains (substring match)
jsonPayload.message : "connection refused"

# Regex match
jsonPayload.message =~ "timeout|TIMEOUT|Timeout"

# Prefix match
logName =~ "projects/my-project/logs/.*"

Toán tử : vs =: Đây là điểm gây nhầm lẫn nhiều nhất.

  • field = "value" — exact equality match
  • field : "value"contains match (field chứa chuỗi con "value")

Với string fields như jsonPayload.message:

  • jsonPayload.message = "connection refused" — chỉ match khi message chính xác là "connection refused"
  • jsonPayload.message : "connection refused" — match khi message chứa "connection refused"

Trong thực tế, : được dùng nhiều hơn cho text search. = dùng cho categorical values.

Boolean logic

# AND (implicit — bởi newline hoặc space)
resource.type = "k8s_container"
severity = "ERROR"

# Explicit AND
resource.type = "k8s_container" AND severity = "ERROR"

# OR
severity = "ERROR" OR severity = "CRITICAL"

# NOT
NOT severity = "DEBUG"

# Grouping
(severity = "ERROR" OR severity = "CRITICAL") AND resource.type = "k8s_container"

Thứ tự ưu tiên: NOT > AND > OR. Luôn dùng ngoặc khi kết hợp AND và OR để tránh nhầm lẫn.

Kiểm tra existence

# Field tồn tại (có giá trị khác null)
httpRequest.requestUrl IS NOT NULL

# Field không tồn tại
NOT httpRequest.requestUrl IS NOT NULL

# Tương đương với cách ngắn hơn
httpRequest.requestUrl:""  # Match mọi entry có httpRequest.requestUrl

Hàm Đặc Biệt Trong LQL

log_id() — filter theo log name

# Chỉ audit activity logs
log_id("cloudaudit.googleapis.com/activity")

# Container logs từ Kubernetes
log_id("stdout") OR log_id("stderr")

# GKE system logs
log_id("events")

log_id(name) tương đương với logName = ".../<name>" nhưng ngắn hơn và không cần biết full path.

source() — filter theo resource hierarchy

# Tất cả logs từ folder Engineering và children
source("folders/123456789")

# Tất cả logs từ org
source("organizations/987654321")

# Logs từ project cụ thể
source("projects/my-project")

source() đặc biệt hữu ích khi query từ aggregated log bucket hoặc từ Log Analytics trên centralized bucket.

sample() — random sampling

# Giữ 10% entries ngẫu nhiên (deterministic dựa trên insertId)
sample(insertId, 0.1)

# Kết hợp với filter — lấy 5% error logs
severity = "ERROR" AND sample(insertId, 0.05)

sample() dùng hash của field được chỉ định để chọn ngẫu nhiên. Vì dùng hash, cùng entry sẽ luôn có cùng kết quả — nếu entry A được giữ với sample(insertId, 0.1), nó luôn được giữ với sampling rate đó.

Hữu ích khi muốn reduce cost của exclusion filter mà vẫn giữ statistical sample.

# Tìm entries có text "OOM" ở bất kỳ đâu
SEARCH("OOM")

# Tìm entries có "database" hoặc "connection"
SEARCH("database") OR SEARCH("connection")

SEARCH() là full-text search qua tất cả string fields. Chậm hơn filter theo field cụ thể nhưng hữu ích khi không biết log ở field nào.


Kỹ Thuật Correlate Logs Across Services

Trace ID correlation

Cách hiệu quả nhất để correlate logs từ nhiều services là qua trace ID. Khi một request đi qua API Gateway → Service A → Service B → Database, tất cả components có thể share cùng một trace ID.

Với W3C Trace Context (được GCP hỗ trợ natively), log entries có field trace chứa trace ID dạng:

projects/my-project/traces/abc123def456...

Query để tìm tất cả logs của một request:

trace = "projects/my-project/traces/abc123def456789012345678901234ab"

Trong Logs Explorer, click vào trace ID link sẽ tự động filter tất cả log entries liên quan.

Nếu application không propagate trace context: Có thể dùng custom request ID. Log entry cần chứa jsonPayload.request_id = "REQ-12345" và filter:

jsonPayload.request_id = "REQ-12345"

Nhưng điều này chỉ work nếu tất cả services đều log request ID theo cùng field name.

Timestamp-based correlation

Khi không có trace context, timestamp range là fallback:

# Tìm tất cả logs quanh thời điểm incident
timestamp >= "2024-03-15T14:00:00Z"
timestamp <= "2024-03-15T14:05:00Z"
severity >= "WARNING"

Kết hợp với source() để lấy từ nhiều projects:

source("folders/engineering-folder-id")
timestamp >= "2024-03-15T14:00:00Z"
timestamp <= "2024-03-15T14:05:00Z"
severity >= "ERROR"

Correlation qua resource labels

GKE logs có resource labels đồng nhất. Nếu bạn biết Pod name, có thể correlate tất cả logs từ Pod đó:

resource.type = "k8s_container"
resource.labels.pod_name = "payment-service-7f9b8d4-xkp2q"

Hoặc correlation theo namespace để xem toàn bộ namespace trong một khoảng thời gian:

resource.type = "k8s_container"
resource.labels.namespace_name = "payment"
timestamp >= "2024-03-15T14:00:00Z"
timestamp <= "2024-03-15T14:05:00Z"

Debug Patterns Thực Tế

Pattern 1: Tìm root cause của error spike

Tình huống: Alert báo 5xx rate tăng đột ngột lúc 14:00.

Bước 1: Tìm errors theo resource type

severity >= "ERROR"
timestamp >= "2024-03-15T13:58:00Z"
timestamp <= "2024-03-15T14:05:00Z"

Bước 2: Xác định resource type có nhiều errors nhất (dùng Group By trong Logs Explorer hoặc Log Analytics)

Bước 3: Drill down vào service cụ thể

resource.type = "k8s_container"
resource.labels.container_name = "payment-api"
severity >= "ERROR"
timestamp >= "2024-03-15T13:58:00Z"

Bước 4: Tìm trace ID của request thất bại, trace sang các services khác

Pattern 2: Debug GKE Pod không start được

# Tìm events liên quan đến pod
resource.type = "k8s_pod"
resource.labels.pod_name =~ "my-app-.*"
resource.labels.namespace_name = "production"

# Hoặc xem events từ kubelet
resource.type = "k8s_node"
jsonPayload.MESSAGE =~ "my-app"

Kết hợp với cluster events:

resource.type = "k8s_cluster"
jsonPayload.reason = "Failed"
jsonPayload.involvedObject.name =~ "my-app-.*"

Pattern 3: Audit trail cho security investigation

Khi cần biết ai đã làm gì trong một project:

# Tất cả admin actions của một user
log_id("cloudaudit.googleapis.com/activity")
protoPayload.authenticationInfo.principalEmail = "suspicious-user@company.com"
timestamp >= "2024-03-01T00:00:00Z"
# Tất cả IAM changes
log_id("cloudaudit.googleapis.com/activity")
protoPayload.methodName =~ "setIamPolicy|modifyPolicy"
# Secret Manager access (cần Data Access Audit Logs bật)
log_id("cloudaudit.googleapis.com/data_access")
resource.type = "secretmanager.googleapis.com/Secret"
protoPayload.authenticationInfo.principalEmail != "service-account@project.iam.gserviceaccount.com"

Pattern 4: Tìm memory leak hoặc OOM kill

# OOMKill events từ Kubernetes
resource.type = "k8s_pod"
jsonPayload.reason = "OOMKilling"

# Node pressure eviction
resource.type = "k8s_node"
jsonPayload.message =~ "Evicted|evict"

# Container restart do OOM
resource.type = "k8s_container"
jsonPayload.message =~ "OOM|out of memory"

Pattern 5: Database slow queries

# Cloud SQL slow query log
resource.type = "cloudsql_database"
log_id("cloudsql.googleapis.com/mysql-slow.log")
jsonPayload.query_time > 1.0  # queries > 1 giây

# Kết hợp với audit để biết user
log_id("cloudsql.googleapis.com/mysql-slow.log") OR log_id("cloudaudit.googleapis.com/data_access")
resource.type = "cloudsql_database"
resource.labels.database_id = "my-project:my-instance"
timestamp >= "2024-03-15T14:00:00Z"

Log Analytics: SQL cho Complex Queries

Khi LQL không đủ mạnh (cần GROUP BY, aggregation, JOIN), Log Analytics cung cấp SQL interface:

sql
-- Top 10 endpoints với error rate cao nhất trong 24 giờ
SELECT
  httpRequest.requestUrl,
  COUNT(*) as total_requests,
  COUNTIF(httpRequest.status >= 500) as error_count,
  ROUND(COUNTIF(httpRequest.status >= 500) * 100.0 / COUNT(*), 2) as error_rate_pct
FROM
  `my-project._Default._AllLogs`
WHERE
  timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
  AND resource.type = "http_load_balancer"
  AND httpRequest.requestUrl IS NOT NULL
GROUP BY
  httpRequest.requestUrl
HAVING
  total_requests > 100  -- Loại bỏ endpoints ít traffic
ORDER BY
  error_rate_pct DESC
LIMIT 10
sql
-- Phân tích lỗi theo giờ để tìm pattern
SELECT
  EXTRACT(HOUR FROM timestamp AT TIME ZONE "Asia/Ho_Chi_Minh") as hour_vn,
  severity,
  COUNT(*) as count
FROM
  `my-project._Default._AllLogs`
WHERE
  DATE(timestamp) = CURRENT_DATE()
  AND resource.type = "k8s_container"
  AND resource.labels.namespace_name = "production"
GROUP BY
  hour_vn, severity
ORDER BY
  hour_vn, severity

Tips Tối Ưu Query Performance

1. Luôn filter theo timestamp range trước

Timestamp là indexed field. Filter theo time range thu hẹp đáng kể số entries cần scan. Không bao giờ query mà không có time range khi đang troubleshoot production.

2. Filter theo resource.type ngay sau timestamp

# Tốt
timestamp >= "2024-03-15T14:00:00Z"
resource.type = "k8s_container"
severity = "ERROR"

# Kém hơn (thiếu resource.type filter)
timestamp >= "2024-03-15T14:00:00Z"
severity = "ERROR"
jsonPayload.component = "payment-service"

3. Tránh leading wildcard trong regex

# Chậm: leading wildcard
jsonPayload.message =~ ".*connection refused.*"

# Nhanh hơn: contains match
jsonPayload.message : "connection refused"

4. Giới hạn time window trong Log Analytics

Log Analytics query được tính phí theo volume scanned. Với 100 GB logs/ngày và không có time filter, một query có thể scan TB data và tốn tiền.

Luôn dùng:

sql
WHERE timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)

hoặc cho Log Analytics bucket:

sql
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)

References