↓メインコンテンツへスキップ
  1. Blogs/

よく使うCloud Loggingクエリまとめ(絞り込みから「出ない」ときの確認まで)

5 分
GoogleCloud CloudLogging CLI
0222-nnn
著者
0222-nnn
猫が好き
目次

概要
#

Cloud Loggingは、書き方さえ合っていれば目的のログにすぐ着きます。困るのは0件が返ったときです。ログが無いのか、クエリが違うのか、そもそも収集していないのか、返ってきた空行からは区別がつきません。

しかも綴りを間違えてもエラーになりません。 resource.type="k8s_containers" と複数形で書けば、静かに0件が返ります。

自分が検証で実際に打ったクエリを、実行結果とセットでまとめます。忘れたときに引けることと、0件の理由をたどれることを目的にします。

Google Cloudのコンソールかgcloudを触ったことがある人向けです。Cloud Loggingをこれから使い始める人の入門ではありません。ログ設計そのものや、Cloud Logging以外への転送は扱いません。

やりたいことから Logs Explorer に貼れるクエリだけを引きたいときは、チートシートを使います。

検証環境
#

  • Google Cloud SDK 562.0.0

出力例は、このブログのTerraform/Google Cloudシリーズで作った検証環境のログを実際に読んだ結果です。

リソースはすべて削除済みですが、ログは残っています。保持期間は保存先のバケットで決まります。この環境では _Default が30日、_Required が400日でした。

$ gcloud logging buckets list --location=global \
    --format='table(name.basename():label=BUCKET_ID,retentionDays,description)'
BUCKET_ID  RETENTION_DAYS  DESCRIPTION
_Default   30              Default bucket
_Required  400             Audit bucket

リソースを消しても、しばらくは調べ直せます。検証の片付けを終えたあとに「あれはどうだったか」となったとき、この30日が効きます。

まず何があるかを見る
#

いきなり絞り込むより、何が入っているかを数えるほうが早いことが多いです。

gcloud logging read "" --freshness=30d --limit=2000 --format="value(resource.type)" | sort | uniq -c | sort -rn

これは直近30日の最新2000件に含まれる種別です。 一覧に無い種別のログが存在しないとは言えません。期間全体を棚卸しするなら、日時範囲を固定して取得上限を外します。

   1232 k8s_cluster
    352 k8s_node
    343 gce_instance
     26 k8s_container
      8 gce_firewall_rule
      6 gce_subnetwork
      6 gce_project
      4 project
      4 gce_instance_group_manager
      4 audited_resource
      2 gke_cluster
      2 gce_reserved_address
      ...

k8s_clusterとgke_cluster、k8s_nodeとgke_nodepoolのように似た名前が併存します。当てずっぽうで書くより、この一覧から選びます。

ログ名(logNameのログID部分)も同じように棚卸しできます。

gcloud logging logs list --format='value(.)'

value(name) だと空になります。返るのは projects/PROJECT_ID/logs/<ログID> のフルパスで、ログIDは URL エンコードされています。

この環境では42件ありました。以下は結果から projects/PROJECT_ID/logs/ を除き、ログIDをデコードして並べたものです。

cloudaudit.googleapis.com/activity
cloudaudit.googleapis.com/data_access
cloudaudit.googleapis.com/system_event
container.googleapis.com/cluster-autoscaler-visibility
kubelet
kube-proxy
nginx_access
nginx_error
ops-agent-fluent-bit
serialconsole.googleapis.com/serial_port_1_output
stdout
stderr
syslog

ログIDは送り方で変わります。GKE でコンテナの標準出力・標準エラーを集めたものはstdout/stderrに入ります。Ops Agent で集めたものは、config.yamlで付けたレシーバIDがそのままログIDになります(nginx_accessなど)。

アプリケーションのログがどのIDに入るかは、送信方法で決まります。

絞り込みの基本
#

4つの軸
#

軸 例
どこから resource.type="k8s_container"
どのログに logName="projects/YOUR_PROJECT_ID/logs/stdout"
どの深刻度 severity>=ERROR
いつ timestamp>="2026-09-08T09:00:00Z"

logNameはフルパスで書きます。ログIDだけを指定したいならLOG_ID()が使えます。

gcloud logging read 'LOG_ID("nginx_access")' --freshness=30d --limit=2 --format="value(logName)"
projects/YOUR_PROJECT_ID/logs/nginx_access
projects/YOUR_PROJECT_ID/logs/nginx_access

演算子
#

書き方 意味
= 完全一致
: 部分一致
=~ 正規表現に一致
!~ 正規表現に一致しない
>= > <= < 大小比較(severityとtimestamp)
AND OR NOT 論理演算

部分一致と正規表現を比べます。

gcloud logging read 'logName:"nginx"' --freshness=30d --limit=2 --format="value(logName)"
gcloud logging read 'logName=~"nginx_(access|error)$"' --freshness=30d --limit=2 --format="value(logName)"

どちらもnginx_accessに当たります。ただし検索条件は違い、速度は測っていません。 単純な部分一致なら:、パターンを書くなら=~を使います。

severity
#

大小比較が効きます。

gcloud logging read 'severity>=ERROR' --freshness=30d --limit=3 \
  --format="table(timestamp.date('%Y-%m-%d %H:%M:%S'),severity,resource.type)"
TIMESTAMP            SEVERITY  TYPE
2026-09-08 09:25:29  ERROR     gce_firewall_rule
2026-09-08 09:25:29  ERROR     gce_firewall_rule
2026-09-08 09:25:29  ERROR     gce_firewall_rule

この環境で実際に出た深刻度はDEFAULT/INFO/NOTICE/WARNING/ERRORで、CRITICALは0件でした。

深刻度は9段階定義されています。severity>=ERRORはCRITICAL以上も含むので、severity>=CRITICALに狭めても件数は増えません。特定の段階だけを見たいならseverity=ERRORと等価で書きます。

構造化ログを掘る
#

ペイロードは3種類あり、どれに入るかで書き方が変わります。

ペイロード 入るもの クエリ例
textPayload 素のテキスト textPayload:"error"
jsonPayload JSON オブジェクト jsonPayload.status.scaleUp="true"
protoPayload 型付きのオブジェクト。監査ログがこれを使う protoPayload.methodName:"delete"

判別は収集エージェント側で行われます。GKEでどう振り分けられるかは別記事で測りました。

labels は2か所ある
#

resource.labelsと、エントリ直下のlabelsは別物です。

gcloud logging read 'resource.type="k8s_container"' --freshness=30d --limit=1 --format=json
resource.labels:
  cluster_name    = tf-adv-gke-ipmasq
  container_name  =
  location        = asia-northeast1-a
  namespace_name  =
  pod_name        =
  project_id      = YOUR_PROJECT_ID

labels(エントリ側):
  compute.googleapis.com/instance_group_manager/name = gke-...-grp
  compute.googleapis.com/instance_group_manager/zone = asia-northeast1-a
  instance_name = gke-...-gbn3

resource.labelsはリソースを特定する軸で、labelsはエントリごとの付加情報です。labels."k8s-pod/app"のようにキーにスラッシュが入ることがあるので、その場合は引用符で囲みます。

なお上の例ではcontainer_nameなどが空です。同じresource.typeでも、書き手によって埋まるラベルが違います。絞り込む前に、まず1件を--format=jsonで開いて何が入っているかを見ます。

監査ログ
#

4種類
#

種類 ログID 何が残るか
Admin Activity cloudaudit.googleapis.com/activity リソースの作成・変更・削除
Data Access cloudaudit.googleapis.com/data_access データの読み書き
System Event cloudaudit.googleapis.com/system_event Google側が行った操作
Policy Denied cloudaudit.googleapis.com/policy ポリシーによる拒否

この環境での件数です。

activity      500件以上
data_access   500件以上
system_event   68件
policy          0件

誰が何をしたかを見る
#

gcloud logging read 'logName=~"cloudaudit.googleapis.com%2Factivity"' --freshness=30d --limit=3 \
  --format="table(protoPayload.authenticationInfo.principalEmail, protoPayload.methodName)"
PRINCIPAL_EMAIL       METHOD_NAME
you@example.com       v1.compute.networks.delete
you@example.com       v1.compute.networks.delete
you@example.com       v1.compute.subnetworks.delete

principalEmail を出さないと「誰が」が分かりません。serviceName はサービス名で、実行した主体ではありません。

エスケープするのはログIDの中の/だけです。projects/YOUR_PROJECT_ID/logs/ の区切りはそのままにします。

projects/YOUR_PROJECT_ID/logs/cloudaudit.googleapis.com%2Factivity   # 置き換えるのはログID内部の / だけ

LOG_ID()を使うならエンコードは要りません(LOG_ID("cloudaudit.googleapis.com/activity"))。

サービス別に数えると、どのAPIをどれだけ呼んだかが見えます。

gcloud logging read 'logName=~"cloudaudit.googleapis.com%2Factivity"' --freshness=30d --limit=400 \
  --format="value(protoPayload.serviceName)" | sort | uniq -c | sort -rn
    360 k8s.io
     31 compute.googleapis.com
      4 networkconnectivity.googleapis.com
      3 container.googleapis.com
      1 iam.googleapis.com
      1 cloudresourcemanager.googleapis.com

Data Access は既定で無効 —— だが0件とは限らない
#

「BigQueryを除き、Data Access監査ログは既定で無効」とされています。実際、この環境で読み書きしたサービスは0件でした。

secretmanager.googleapis.com     0 件
cloudkms.googleapis.com          0 件
storage.googleapis.com           0 件

プロジェクトの IAM ポリシーではauditConfigsが空でした。ただし組織やフォルダから継承する設定は、この出力では分かりません。

$ gcloud projects get-iam-policy YOUR_PROJECT_ID --format="value(auditConfigs)"
(空)

それでもdata_accessには500件以上ありました。 取得した直近400件は、すべて1つのサービスでした。

$ gcloud logging read 'logName=~"data_access"' --freshness=30d --limit=400 \
    --format="value(protoPayload.serviceName)" | sort | uniq -c
    400 oslogin.googleapis.com
methodName:   google.cloud.oslogin.dataplane.OsLoginDataPlaneService.ListLoginProfiles
resourceName: projects/YOUR_PROJECT_ID/zones/asia-northeast1-a/instances/...-bastion
severity:     INFO

踏み台インスタンスを対象としたListLoginProfilesです。SSH接続との対応は確かめていません。 数えるなら、接続しない時間帯と、時刻を記録して複数回接続した時間帯を比べます。

oslogin.googleapis.comは監査ログ対応サービスの一覧に載っていません。 OS Loginには専用の監査ログ資料がありますが、このListLoginProfilesが既定で記録される条件までは書かれていません。出ること自体は実測できましたが、理由はたどれませんでした。

実務上の意味はこうです。「Data Accessは無効だから空のはず」と思って探すのをやめない。 有効化していないサービスの分は確かに空ですが、data_accessそのものは空とは限りません。

_Required と _Default
#

保持期間が2種類あったのは、シンクが2本あるからです。

$ gcloud logging sinks list --format="table(name,destination.basename(),filter)"
NAME       DESTINATION  FILTER
_Required  _Required    LOG_ID("cloudaudit.googleapis.com/activity") OR ... OR LOG_ID("cloudaudit.googleapis.com/system_event") OR ...
_Default   _Default     NOT LOG_ID("cloudaudit.googleapis.com/activity") AND ... AND NOT LOG_ID("cloudaudit.googleapis.com/system_event") AND ...

_Requiredが400日で拾うのはAdmin Activity・System Event・Access Transparencyです。_Defaultはそれ以外を30日で拾います。

data_accessは_Requiredの条件に入らないので30日で消えます。 監査ログだから400日、ではありません。

gcloud logging read のオプション
#

オプション 効果 既定
--freshness 何日前まで遡るか 1d
--limit 取得件数 上限なし
--order 並び順 desc(新しい順)
--format 出力形式 YAML
--project 対象プロジェクト gcloudの設定値

--freshnessの既定が1日なのが、いちばんの落とし穴です。

$ gcloud logging read 'resource.type="gce_instance"' --limit=2 --format="value(timestamp)"
(0件)

$ gcloud logging read 'resource.type="gce_instance"' --freshness=30d --limit=2 --format="value(timestamp)"
2026-09-08T09:26:08.294261736Z
2026-09-08T09:25:03.006720Z

同じクエリで、片方は0件、片方は出ます。 2日前のログを探していて何も返らないとき、まずこれを疑います。

--order=ascで古い順になります。時系列を追うときに使います。

ただし --freshness は降順のときだけ効きます。

$ gcloud logging read --help | grep -A2 freshness
--freshness=FRESHNESS; default="1d"
   Return entries that are not older than this value. Works only with DESC
   ordering and filters without a timestamp.

昇順で期間を絞るなら、timestamp をフィルタに書きます。

$ gcloud logging read 'resource.type="gce_instance" AND timestamp>="2026-08-09T00:00:00Z"' \
    --limit=2 --order=asc --format="value(timestamp)"
2026-08-09T08:38:20.601147Z
2026-08-09T08:38:31.957772Z

上の出力は8月9日のもので、--freshness=30d では本来入らない古さです。--order=asc を付けた時点で --freshness が無視されていました。

–format の使い分け
#

指定 用途
--format=json 中身を全部見る。何が入っているか分からないときは最初にこれ
--format="table(...)" 見出し付きで揃える
--format="value(...)" タブ区切り。sortやuniqに渡す
--format="csv(...)" 表計算に貼る
$ gcloud logging read 'resource.type="gce_firewall_rule"' --freshness=30d --limit=2 --format="csv(timestamp,severity)"
timestamp,severity
2026-09-08T09:25:52.627894Z,NOTICE
2026-09-08T09:25:52.071821Z,NOTICE

時刻の扱い
#

timestampはUTCです。 JSTで考えていると9時間ずれます。

範囲で絞るときはUTCで書きます。

gcloud logging read 'timestamp>="2026-09-08T09:00:00Z" AND timestamp<"2026-09-08T09:30:00Z"' \
  --limit=2 --format="value(timestamp,resource.type)"
2026-09-08T09:29:48.052996Z	gce_subnetwork
2026-09-08T09:27:16.805069Z	gce_subnetwork

表示だけJSTにしたいなら、--format側で変換します。

gcloud logging read 'resource.type="gce_firewall_rule"' --freshness=30d --limit=2 \
  --format="table(timestamp.date('%Y-%m-%d %H:%M:%S %Z', tz='Asia/Tokyo'), severity)"
TIMESTAMP                SEVERITY
2026-09-08 18:25:52 JST  NOTICE
2026-09-08 18:25:52 JST  NOTICE

絞り込みには+09:00付きのJSTも書けます。timestamp>="2026-09-08T18:00:00+09:00"はtimestamp>="2026-09-08T09:00:00Z"と同じです。出力の末尾のZがUTCを表すことだけ覚えておけば混乱しません。

0件だったときに疑う順
#

上から順に確かめます。手元だけで確かめられるものから並べています。 頻度を数えたわけではありません。

1. --freshness が短くないか
#

既定は1日です。まず--freshness=30dを付けて再実行します。

2. 綴りが合っているか
#

間違えてもエラーになりません。

$ gcloud logging read 'resource.type="k8s_containers"' --freshness=30d --limit=1 --format="value(timestamp)"
(0件)

複数形にしただけです。実在する名前は、同じ期間から取り出して確かめます。

gcloud logging read "" --freshness=30d --limit=2000 --format="value(resource.type)" | sort -u

--freshnessを揃えないと、探しているログが一覧にも出ません。

ただし列挙型のフィールドは検証されます。

$ gcloud logging read 'severity = "ERRO"' --freshness=30d --limit=2
ERROR: (gcloud.logging.read) INVALID_ARGUMENT: The value 'erro' is of incorrect type.

severityは落ちて、resource.typeは落ちない。落ちてくれるほうが親切です。

3. 演算子を書き忘れていないか
#

これも静かに通ります。

$ gcloud logging read 'resource.type="k8s_container"' --freshness=30d --limit=100 --format="value(resource.type)" | wc -l
26

$ gcloud logging read 'resource.type "k8s_container"' --freshness=30d --limit=100 --format="value(resource.type)" | wc -l
1

=を落としただけで、別のクエリとして成立してしまいます。 0件ではなく「妙に少ない」という形で出るので、いっそう気づきにくいです。

4. そもそも収集しているか
#

クエリの問題ではないことがあります。

  • GCEのOS・アプリケーションのログは、収集と送信を自分で設定する。 この検証では Ops Agent を使った(検証)。クライアントライブラリや OpenTelemetry Collector という手もある
  • Data Access監査ログは、BigQueryを除いて既定で無効です
  • GKEでは、対象のワークロードやコンポーネントのログ収集が有効になっているかを見ます

5. 保持期間を過ぎていないか
#

_Defaultは30日です。data_accessもここに入ります。

6. 除外されていないか
#

シンクに除外フィルタを設定していると、そのシンクの宛先には転送されません。

捨てられるのは受信後です。除外はLogging APIがエントリを受け取ったあとに適用され、シンクごとに独立して評価されます。

つまり、あるシンクで除外されても、別のシンクが同じエントリを保存していることがあります。 探しているバケットに紐づくシンクを見ます。

gcloud logging sinks describe _Default --format="value(exclusions)"

除外の追加・削除はgcloud logging sinks update側にあります(--add-exclusion / --remove-exclusions)。gcloud logging exclusionsというコマンドはありません。

検証でよく使った形
#

このシリーズで実際に打ったものです。

# コンテナのログを Pod 単位で
gcloud logging read 'resource.type="k8s_container"
  AND resource.labels.cluster_name="CLUSTER_NAME"
  AND resource.labels.pod_name="POD_NAME"' --freshness=1h --limit=50

# GKE オートスケーラが「なぜ増やさなかったか」
gcloud logging read 'LOG_ID("container.googleapis.com/cluster-autoscaler-visibility")' \
  --freshness=1h --limit=5 --format=json

# 誰がリソースを消したか
gcloud logging read 'logName=~"cloudaudit.googleapis.com%2Factivity"
  AND protoPayload.methodName=~"delete"' --freshness=30d --limit=20 \
  --format="table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.methodName,protoPayload.resourceName)"

# Ops Agent で集めた nginx のアクセスログ
gcloud logging read 'LOG_ID("nginx_access")' --freshness=1h --limit=20 \
  --format="value(timestamp,jsonPayload.message)"

# 直近のエラーだけ、時系列で(昇順では freshness が効かないので timestamp で絞る)
gcloud logging read "severity>=ERROR AND timestamp>=\"$(date -u -d '-6 hours' +%Y-%m-%dT%H:%M:%SZ)\"" \
  --limit=50 --order=asc \
  --format="table(timestamp.date('%H:%M:%S', tz='Asia/Tokyo'),resource.type,textPayload)"

ログベースの指標
#

同じクエリを繰り返し数えたいなら、ログベースの指標にするとMetrics Explorerやアラートから使えます。

gcloud logging metrics list --format="value(name)"

この環境ではユーザー定義の指標は0件でした。作るならgcloud logging metrics createです。

指標はプロジェクトに属します。 VMやクラスタを作り直しても消えません。ただし別プロジェクトで同じ構成を再現するなら、Terraformで定義を持っておくほうが確実です。

つまずいたときの確認先
#

まとめ
#

  • 0件はエラーではない。 綴り違いも演算子の書き忘れも、静かに通る
  • --freshnessの既定は1日。 0件だったらまずここを疑う
  • まず数える。 resource.typeとlogNameを棚卸ししてから絞り込む
  • 1件を--format=jsonで開く。 同じ種別でも、埋まっているラベルは書き手によって違う
  • logNameでエスケープするのはログIDの中の/だけ。 パスの区切りは変えない。LOG_ID()ならエンコード不要
  • Data Accessは「既定で無効」だが「空」ではない。 この環境ではosloginが書いていた
  • data_accessの保持は30日。 400日残るのはAdmin ActivityとSystem Event
  • 出力のZはUTC。 絞り込みはオフセット付きで JST でも書ける

参考資料
#

関連記事

GKEでPodの退避を起こしたら、監視に必要なメトリクスがCloud Monitoringに来なかった
8 分
Terraform GoogleCloud GKE Kubernetes CloudLogging CloudMonitoring
terraform applyが成功してもVMの中は設定されていなかった話
4 分
Terraform GoogleCloud Ansible ComputeEngine CloudLogging
標準出力に1行JSONを書くだけなら、GKEではFluentdサイドカーが要らなかった
2 分
Terraform GoogleCloud GKE CloudLogging Fluentd
ドメイン認証がFAILEDになった9分後にAUTHORIZEDへ戻り、証明書はACTIVEになった
4 分
Terraform GoogleCloud LoadBalancing CertificateManager CloudDNS
TerraformでGoogle Cloudの予算アラートを作り、Pub/Sub通知の中身まで確かめた
4 分
Terraform GoogleCloud CloudBilling PubSub
GKEのdefault_compute_class_enabledを有効にしても何も起きなかったので調べた
3 分
Terraform GoogleCloud GKE