概要 #
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で定義を持っておくほうが確実です。
つまずいたときの確認先 #
- クエリ構文そのもの → Logging query language
- どのリソース種別があるか → Monitored resource types
- 監査ログの対応サービス → Google services with audit logs
- gcloudのオプション →
gcloud logging read --help
まとめ #
- 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 でも書ける