GCP Observability

GCP Cloud Monitoring and Operations: Observability Built In

At 03:40 your phone buzzes: checkout latency is climbing, a few customers are seeing 500s, and nobody pushed anything. On most platforms this is the start of a long night spent SSHing into VMs, grepping log files, and guessing which of forty microservices is the culprit. On Google Cloud it should be a ten-minute incident — because the metrics, logs, traces and profiles you need were already collected, correlated by project, service and revision, and waiting in one console. The difference between those two nights is not luck. It is whether you set up Cloud Monitoring and the Operations Suite (the product family Google still half-jokingly calls “Stackdriver”) before the incident, not during it.

This article is the build-it-properly guide. The Operations Suite is not one product — it is Cloud Monitoring (metrics, dashboards, uptime checks, alerting, SLOs), Cloud Logging (structured logs, queries, sinks, log-based metrics), Cloud Trace (distributed latency traces), Cloud Profiler (continuous CPU and heap profiling), and Error Reporting (aggregated exceptions). They share one telemetry pipeline, one IAM surface and one console, which is exactly why GCP observability is powerful and exactly why it can quietly cost you a fortune if you ingest everything blindly. The hard part was never collecting data — GCP collects most of it for free, automatically. The hard part is turning that firehose into a handful of dashboards you trust, alerts that fire only when a human must act, and SLOs that tell you whether you are actually meeting the promise you made to users.

By the end you will be able to wire the Ops Agent onto a VM, write a metric threshold and a burn-rate SLO alert that does not page at 3 a.m. for nothing, query metrics in both MQL and PromQL, build an uptime check with a matching alert, turn a noisy log line into a billable-event counter with a log-based metric, route logs to BigQuery with a sink, and read a trace waterfall to find the one slow dependency. Every step has real gcloud, real Terraform, and real config — and a stack of reference tables you can keep open mid-incident.

What problem this solves

Distributed systems fail in distributed ways. A single user request fans out across a load balancer, three or four Cloud Run services, a Cloud SQL primary, a Pub/Sub topic and a downstream API — and when it gets slow or errors, the failure is rarely where the symptom shows up. Without centralized observability you are reduced to the worst debugging loop there is: notice the symptom (usually because a customer told you), guess a service, SSH in, tail -f a log, guess again. Mean-time-to-resolution measured in hours, and a team that is permanently reactive.

What breaks without this setup is not the platform — it is your ability to see the platform. Three failure patterns recur. First, you learn about outages from customers, because nothing was watching the public endpoint or the error rate. Second, you cannot localize a problem, because metrics, logs and traces live in different tools (or no tool) and nothing correlates them, so a “the app is slow” report turns into a four-engineer guessing session. Third — the subtle one — you drown in your own noise: alerts fire constantly for things nobody acts on, the on-call engineer starts ignoring the pager, and the one alert that mattered gets lost in the flood. That last failure is more dangerous than having no alerts at all.

Who hits this: every team running anything non-trivial on GCP. It bites hardest on teams that “turned on logging” and got a surprise five-figure bill, teams running many services with no shared dashboard, and teams whose alerting is a pile of static thresholds (CPU > 80%) that page on transient spikes and stay silent through real user-facing outages. The fix is not “collect more” — GCP already collects plenty. The fix is to be deliberate: define what “healthy” means as an SLO, alert on the error budget burn rate, sample and exclude the logs you will never read, and correlate the three signals so any incident collapses to one place to look.

To frame the whole field before the deep dive, here is what each pillar of the Operations Suite collects, who emits it, and the first place you look when a specific kind of incident starts:

Pillar What it collects How data arrives First incident it answers
Cloud Monitoring Time-series metrics, dashboards, uptime checks, alert policies, SLOs Auto from GCP services; Ops Agent; custom/OTLP; Managed Prometheus “Is anything past a threshold or burning budget?”
Cloud Logging Structured/text log entries, audit logs, query, sinks, log-based metrics Auto from GCP services; Ops Agent; client libs; gcloud logging write “What did the service actually say when it failed?”
Cloud Trace Distributed latency spans across services Auto on some products; OpenTelemetry/SDK instrumentation “Which hop in this slow request is the slow one?”
Cloud Profiler Continuous CPU and heap (and more) profiles Profiler agent/library in the app “Which function is burning the CPU?”
Error Reporting Deduplicated, grouped exceptions and stack traces Parsed automatically from logs; reporting API “What’s the top new exception and is it spiking?”

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should be comfortable with the GCP basics: a project is the billing and IAM boundary for resources, and most observability data is scoped to a project. You should know how to run gcloud (in Cloud Shell or locally), read JSON/YAML output, and grant an IAM role. Familiarity with HTTP status codes, basic Linux process concepts, and the idea of a time-series metric (a named number sampled over time, tagged with labels) will make everything here land faster. You do not need prior Stackdriver experience.

This sits in the Observability & Operations track and is downstream of the platform fundamentals. It assumes you understand the GCP Resource Hierarchy (because metrics scopes and log sinks operate across that hierarchy) and Google Cloud IAM (because every agent, sink and alert needs the right role). It pairs naturally with whatever you are running: Cloud Run and GKE both feed this pipeline automatically, and when something is denied rather than slow, the GCP Permission-Denied decision tree is the companion playbook. The CLI muscle memory comes from the gcloud CLI Quickstart.

A quick map of which IAM role lets a person (or service account) do which observability job, so you grant least privilege and not Owner:

Task Predefined role Role ID Notes
View dashboards, metrics, alerts Monitoring Viewer roles/monitoring.viewer Read-only; good default for most engineers
Create alerts, dashboards, uptime checks, SLOs Monitoring Editor roles/monitoring.editor The day-to-day “I build monitoring” role
Manage everything in Monitoring incl. metrics scope Monitoring Admin roles/monitoring.admin Add/remove projects from a scope
Write custom metrics from an app/agent Monitoring Metric Writer roles/monitoring.metricWriter Grant to the VM/Cloud Run service account
Read logs Logs Viewer roles/logging.viewer Excludes data-access audit logs
Read all logs incl. private/data-access Private Logs Viewer roles/logging.privateLogViewer Sensitive; restrict tightly
Write log entries Logs Writer roles/logging.logWriter Grant to agents/workloads emitting logs
Create sinks, buckets, log-based metrics Logging Admin roles/logging.admin Configures the logging plumbing
Write trace spans Cloud Trace Agent roles/cloudtrace.agent Grant to the instrumented workload’s SA

Core concepts

Six mental models make every later task obvious. Get these and the console stops feeling like a maze.

The metric, not the chart, is the unit. Cloud Monitoring stores time series: a stream of timestamped values for one metric type (e.g. compute.googleapis.com/instance/cpu/utilization) attached to one monitored resource (e.g. a specific gce_instance), further split by labels (zone, instance_id, your own labels). A chart is just a query over those time series. When you “can’t find a metric,” you are almost always missing the right resource type or a label filter — not a missing metric.

Everything has a scope, and scopes can span projects. A metrics scope (formerly “Workspace”) is the lens through which Monitoring sees metrics. A scope has a scoping project and can include other projects, so one pane of glass can watch a whole fleet. Logs, by contrast, default to per-project, but you can aggregate them upward with a sink that points at a log bucket in another project. Knowing whether you are looking at one project or many is the first question in any “why don’t I see it” moment.

GCP collects most of this for free, automatically. Google-managed services emit system metrics and logs with zero setup: Compute Engine CPU and network, Cloud Run request count and latencies, Cloud SQL connections, load-balancer 5xx rates, and so on. Cloud Trace is automatic on Cloud Run, App Engine and via the load balancer for a sample of requests. You only install an agent when you need inside-the-guest signals — process memory, disk, application log files — that the hypervisor cannot see.

Logs and metrics are two shapes of the same truth, and you can convert between them. A log-based metric turns “count of log lines matching this filter” into a time series you can chart and alert on (a counter metric), or extracts a number from each matching line into a distribution (a distribution metric). This is how you alert on something that is only ever expressed in a log message — a specific stack trace, a “payment declined” event, a slow-query warning.

Alerting has two philosophies, and the modern one is burn rate. The old way: static thresholds (error_rate > 1% for 5 minutes). It pages on transient spikes and stays silent through slow bleeds. The modern way: define an SLO (e.g. 99.9% of requests succeed over 28 days), which implies an error budget (0.1% may fail), and alert on the burn rate — how fast you are spending that budget. A fast burn over a short window pages immediately; a slow burn over a long window opens a ticket. You alert on user pain, not on a number that may not matter.

The three signals correlate by labels, so collect them with consistent labels. A metric spike, the log lines from that window, and the slow trace are all tagged with the same project_id, service/revision (Cloud Run), cluster/namespace/pod (GKE) or instance_id (GCE). Consistent resource labels are what let you jump from “latency chart is red” to “here are the exact logs and the exact slow trace” in two clicks. Inconsistent or missing labels are why some teams’ “observability” is three disconnected tools.

The vocabulary in one table

Before the deep sections, pin down every moving part. The glossary repeats these for lookup; this is the mental model side by side:

Term One-line definition Where it lives Why it matters
Metric type The named kind of measurement Monitoring metric descriptor What you select to chart/alert
Monitored resource The thing being measured (VM, service, bucket) Attached to each time series Scopes a metric to one entity
Label Key/value tag on a time series On resource or metric Filter and group-by dimension
Metrics scope The set of projects Monitoring can see Scoping project Multi-project single pane
Ops Agent Unified telemetry agent for VMs On the guest OS Ships host metrics + app logs
Uptime check Periodic probe of an endpoint Monitoring, global pollers External “is it up” signal
Alerting policy Condition(s) + notification channels Monitoring Turns a breach into a page
Notification channel Where an alert is delivered Monitoring Email/Slack/PagerDuty/etc.
SLO Target reliability over a window Monitoring (on a service) Defines “good”; implies budget
Error budget The allowed amount of failure Derived from the SLO What burn-rate alerts watch
Log-based metric A metric derived from logs Logging → Monitoring Alert on things only in logs
Sink A route exporting logs to a destination Logging Long-term storage / analytics
Log bucket Where Logging stores entries Logging Retention + cost live here
MQL Monitoring Query Language Metrics Explorer / API Powerful ratio/window queries
PromQL Prometheus query language Managed Service for Prometheus Standard for k8s/Prometheus users

The metric model — read it once, query forever

Every Cloud Monitoring time series is identified by three things together: a metric type, a monitored resource type, and a set of labels on each. Add two more attributes — metric kind and value type — and you can read any metric descriptor and write a correct query the first time. This is the single most useful thing to internalize, because most “I can’t find / can’t aggregate the metric” problems are a misunderstanding of one of these five fields.

Attribute What it answers Example value Why it matters
Metric type What is being measured run.googleapis.com/request_count The series name you select
Monitored resource type What entity it belongs to cloud_run_revision Determines which labels exist
Resource labels Which specific entity service_name, revision_name, location Filter/group to one thing
Metric labels Sub-dimensions of the metric response_code_class="5xx" Slice without separate metrics
Metric kind How values accumulate GAUGE, DELTA, CUMULATIVE Dictates align/rate handling
Value type The data shape INT64, DOUBLE, DISTRIBUTION Distributions → percentiles

Metric kind trips people up, so be explicit:

Metric kind Meaning Typical example How you usually query it
GAUGE A value sampled at a point in time CPU utilization, memory used Align with mean/max over a window
DELTA Change over the sample interval Request count per minute Sum/rate to get per-second or totals
CUMULATIVE A running total since a start point Bytes sent (monotonic counter) rate/align rate to get per-second

A DISTRIBUTION value type (used by latency metrics like run.googleapis.com/request_latencies) holds a histogram per point, which is how Monitoring gives you p50/p95/p99 without you pre-aggregating. You pick the percentile at query time with an aligner, not at collection time.

List what is actually available in your project rather than guessing metric names:

# List metric descriptors for Cloud Run (filter to the service you care about)
gcloud monitoring metrics-descriptors list \
  --filter='metric.type=starts_with("run.googleapis.com/")' \
  --format='value(type)'

# Inspect one descriptor in full — kind, value type, labels
gcloud monitoring metrics-descriptors describe \
  run.googleapis.com/request_latencies \
  --format=yaml

Custom metrics and OpenTelemetry (OTLP)

When the built-in metrics don’t cover a business signal — orders per minute, queue depth, cache hit ratio — you write a custom metric under the custom.googleapis.com/ (or workload.googleapis.com/ via the Ops Agent/OTLP) namespace. You can write directly with the Monitoring API/client libraries, or — increasingly the recommended path — emit OpenTelemetry and export via OTLP to Google Cloud. The same OTLP pipeline can carry metrics, traces and logs, which is why new instrumentation should start there rather than three separate vendor SDKs.

Way to get metrics in Best for Namespace Notes
Auto (GCP services) Anything Google-managed *.googleapis.com/ Free, zero setup
Ops Agent VM host + app metrics agent.googleapis.com/, workload.googleapis.com/ One agent, config-driven
Custom metric API / client lib Bespoke business metrics custom.googleapis.com/ You manage descriptors + writes
OpenTelemetry → OTLP exporter New, portable instrumentation workload.googleapis.com/ One pipeline for metrics/traces/logs
Managed Service for Prometheus Existing Prometheus/k8s exporters Prometheus model Query with PromQL; keep your exporters

The Ops Agent — getting inside-the-guest signals

The hypervisor sees a VM’s CPU, disk and network from the outside, but it cannot see memory used by your process, disk usage inside the filesystem, or the contents of /var/log/myapp/app.log. For that you install an agent on the guest. Google now ships one agent — the Ops Agent — that collects both metrics and logs, replacing the two older “legacy Monitoring agent” and “legacy Logging agent” daemons. New deployments should use the Ops Agent; the legacy agents are deprecated.

Capability Ops Agent Legacy Monitoring agent Legacy Logging agent
Metrics Yes Yes No
Logs Yes No Yes
One daemon for both Yes No (two agents) No (two agents)
Config model Single YAML, receivers/processors/pipelines collectd-based fluentd-based
Built on OpenTelemetry + Fluent Bit collectd fluentd
Status Recommended Deprecated Deprecated

Install it (Debian/Ubuntu/RHEL/SUSE/Windows are supported via the same script). The instance’s service account needs roles/monitoring.metricWriter and roles/logging.logWriter:

# Install the Ops Agent on a running VM (run on the instance)
curl -sSO https://dl.google.com/cloudagents/add-google-cloud-ops-agent.sh
sudo bash add-google-cloud-ops-agent.sh --also-install

# Confirm it's running
sudo systemctl status google-cloud-ops-agent"*"

Fleet-wide, install and keep agents current with the Ops Agent policy via the VM Manager (or bake it into your image). The agent’s behavior is one YAML file at /etc/google-cloud-ops-agent/config.yaml. Here is a config that tails an application log and parses it as JSON, plus enables host metrics:

# /etc/google-cloud-ops-agent/config.yaml
logging:
  receivers:
    myapp:
      type: files
      include_paths: [/var/log/myapp/app.log]
  processors:
    parse_json:
      type: parse_json          # turn JSON log lines into structured fields
  service:
    pipelines:
      myapp_pipeline:
        receivers: [myapp]
        processors: [parse_json]
metrics:
  receivers:
    hostmetrics:
      type: hostmetrics
      collection_interval: 60s
  service:
    pipelines:
      host_pipeline:
        receivers: [hostmetrics]

After editing the config, restart the agent: sudo systemctl restart google-cloud-ops-agent. The host metrics then appear under agent.googleapis.com/ (e.g. agent.googleapis.com/memory/percent_used), and your parsed log shows up in Logs Explorer with each JSON field as a queryable column.

Common Ops Agent metric Metric type What it tells you
Memory used % agent.googleapis.com/memory/percent_used Real in-guest memory pressure
Disk used % agent.googleapis.com/disk/percent_used Filesystem fill (the thing that pages you)
Swap used agent.googleapis.com/swap/percent_used Memory exhaustion warning sign
CPU load agent.googleapis.com/cpu/load_1m Saturation beyond raw utilization
Process count agent.googleapis.com/processes/count_by_state Zombie/stuck process detection

Querying metrics: MQL vs PromQL

Cloud Monitoring speaks two query languages, and which you reach for depends on where you came from. MQL (Monitoring Query Language) is Google’s native, table-pipeline language — extremely good at ratios, joins between metrics, and time-window math. PromQL arrives via Google Cloud Managed Service for Prometheus, so if your team already writes Prometheus queries and runs exporters, you keep that muscle memory and your existing alerting rules. Both run in Metrics Explorer; both can back an alerting policy.

Dimension MQL PromQL (Managed Prometheus)
Origin Google-native Prometheus ecosystem
Best for Ratios, metric joins, window functions k8s workloads, existing exporters/rules
Data model GCP time series (resource + metric labels) Prometheus samples (labels)
Where it runs Metrics Explorer, alerting, API Metrics Explorer, PromQL API, Grafana
Reuse existing rules No Yes — bring your .rules.yaml
Learning curve from scratch Moderate Moderate (familiar if you know Prom)

A worked example: the 5xx error ratio for a Cloud Run service. In MQL you fetch the metric, split by response-code class, and divide the bad over the total:

# MQL — fraction of Cloud Run requests that are 5xx, per service, over 5m windows
fetch cloud_run_revision
| metric 'run.googleapis.com/request_count'
| filter (resource.service_name == 'checkout')
| align rate(5m)
| group_by [metric.response_code_class],
    [value_count: sum(value.request_count)]
| ratio  # 5xx slice / total — paired with a divide; commonly expressed as:

In practice the canonical MQL error-ratio pattern uses an explicit outer_join/div of the 5xx series over the all-codes series. The point for this article: MQL can compute the ratio itself, which is exactly what a good alert condition wants — alert on “fraction of requests failing,” not “absolute count of failures,” because absolute counts scale with traffic and produce false alarms.

The same intent in PromQL, against Managed Service for Prometheus, reads the way any Prometheus user expects:

# PromQL — 5xx ratio for the checkout service over 5 minutes
sum(rate(run_googleapis_com:request_count{service_name="checkout", response_code_class="5xx"}[5m]))
/
sum(rate(run_googleapis_com:request_count{service_name="checkout"}[5m]))

Rule of thumb: if your org already standardized on Prometheus and Grafana, use Managed Service for Prometheus and PromQL so you keep one language across clouds; if you are GCP-native and want the richest ratio/window math without running exporters, use MQL.

Uptime checks — the external “is it up” signal

Internal metrics tell you the service thinks it’s healthy. An uptime check tells you whether the outside world can actually reach it. Cloud Monitoring runs uptime checks from multiple global locations on an interval, asserting on status code and (optionally) response content. Pair every uptime check with an alerting policy so a failure pages you — an uptime check with no alert is a dashboard nobody watches.

Field Options / values Notes
Target type URL (public), GCE instance, Cloud Run, App Engine, LB, Elastic IP Public HTTP(S) is the common case
Protocol HTTP, HTTPS, TCP TCP for non-HTTP ports
Check frequency 1, 5, 10, 15 minutes 1 min = fastest detection, more checks
Regions Global, USA, Europe, Asia-Pacific, etc. Multiple regions reduce false positives
Timeout Up to 60 s Tune to your slow-but-ok latency
Content matching Contains / not-contains / regex Catch “200 OK but wrong page”
Auth / headers Basic auth, custom headers Probe authenticated endpoints
SSL cert expiry Alert N days before expiry Catches forgotten cert renewals
Private checks Via PSC / internal targets Probe internal-only services

Create a public uptime check and confirm it:

# Create an HTTPS uptime check that asserts the home page returns 200 and contains a marker
gcloud monitoring uptime create "checkout-home" \
  --resource-type=uptime-url \
  --resource-labels=host=shop.example.com,project_id=$PROJECT_ID \
  --path="/health" \
  --port=443 \
  --protocol=https \
  --period=1 \
  --timeout=10 \
  --matcher-content="ok" --matcher-type=contains-string

# List existing checks
gcloud monitoring uptime list-configs --format='table(displayName, httpCheck.path)'

In Terraform, the same check plus a content matcher:

resource "google_monitoring_uptime_check_config" "checkout_home" {
  display_name = "checkout-home"
  timeout      = "10s"
  period       = "60s"            # 1-minute checks

  http_check {
    path         = "/health"
    port         = 443
    use_ssl      = true
    validate_ssl = true           # also fail if the cert is invalid
  }

  monitored_resource {
    type = "uptime_url"
    labels = {
      project_id = var.project_id
      host       = "shop.example.com"
    }
  }

  content_matchers {
    content = "ok"
    matcher = "CONTAINS_STRING"
  }
}

A subtle but important point: a single global uptime check can flap if one polling region has a transient network issue. The check is reported failed only when a configured number of regions see the failure, which is why multi-region checks are far less noisy than single-region ones. Alert on the uptime check’s check_passed metric being false across regions, not on a single probe.

Alerting policies — turning a breach into a page

An alerting policy is one or more conditions plus the notification channels that get told when those conditions are met. The art is entirely in the condition: the right metric, the right comparison, and — crucially — the right duration (how long the breach must persist before it counts). A threshold with no duration pages on every one-minute spike; a threshold with a five-minute duration pages only on a sustained problem.

Condition knob What it controls Typical setting Failure mode if wrong
Metric / filter Which time series The specific resource + label filter Wrong series → never fires / always fires
Aligner Per-series time bucketing rate, mean, percentile Counter without rate → nonsense values
Reducer (group-by) Combine across series sum/mean across instances Per-instance noise vs fleet view
Comparison The threshold test > 0.01 (1% errors) Absolute count → traffic-dependent noise
Duration How long before it triggers 5–10 minutes 0 min → pages on every transient spike
Retest / auto-close When the incident closes Auto-close after N minutes healthy Stuck-open incidents
Notification channels Where it goes PagerDuty for sev1, Slack for sev3 Page fatigue or missed alerts
Severity / labels Routing + docs Severity + a runbook link On-call wastes time without a runbook

Set up a notification channel first (you cannot route an alert without one), then create a policy. A Slack or PagerDuty channel is usually wired in the console once; here is email via CLI and the policy from a YAML/JSON spec:

# Create an email notification channel
gcloud beta monitoring channels create \
  --display-name="oncall-email" \
  --type=email \
  --channel-labels=email_address=oncall@example.com

# Grab its ID for the policy
CHANNEL=$(gcloud beta monitoring channels list \
  --filter='displayName="oncall-email"' --format='value(name)')

A metric-threshold policy that fires when Cloud Run 5xx ratio stays above 2% for 5 minutes, defined in Terraform (the most maintainable way to manage many policies):

resource "google_monitoring_alert_policy" "run_5xx" {
  display_name = "checkout 5xx > 2% (5m)"
  combiner     = "OR"

  conditions {
    display_name = "5xx ratio high"
    condition_threshold {
      filter = <<-EOT
        resource.type = "cloud_run_revision"
        AND resource.labels.service_name = "checkout"
        AND metric.type = "run.googleapis.com/request_count"
        AND metric.labels.response_code_class = "5xx"
      EOT
      comparison      = "COMPARISON_GT"
      threshold_value = 0.02
      duration        = "300s"        # must persist 5 minutes
      aggregations {
        alignment_period   = "60s"
        per_series_aligner = "ALIGN_RATE"
      }
      trigger { count = 1 }
    }
  }

  notification_channels = [var.oncall_channel_id]
  alert_strategy {
    auto_close = "1800s"              # close 30 min after recovery
  }
  documentation {
    content   = "Checkout is returning 5xx. Runbook: https://wiki/runbooks/checkout-5xx"
    mime_type = "text/markdown"
  }
}
Condition type Use when Example
Metric threshold A value crosses a line for a duration CPU > 85% for 10 min
Metric absence A metric stops arriving No heartbeat metric for 5 min → silent failure
Forecast (predictive) A trend will cross a line soon Disk will fill within 4 hours
Rate of change A value moves too fast Error rate doubled in 2 min
Log match (log alert) A specific log line appears A “FATAL” entry → page immediately
MQL / PromQL condition Anything expressible as a query A computed ratio or join
SLO burn rate Error budget burning too fast The next section — the recommended one

SLOs and burn-rate alerting — pages that mean something

This is where good observability separates from box-checking. A static threshold answers “is this number high?” An SLO answers the question that actually matters: “are we keeping the promise we made to users?” You define a Service-Level Indicator (SLI) — a measurable proxy for happiness, like the fraction of requests that succeed under 300 ms — and an SLO target on it, like 99.9% over a rolling 28 days. The math then hands you an error budget: with a 99.9% target, 0.1% of requests are allowed to fail. Reliability is not “never fail”; it is “fail less than the budget.”

Term Definition Example
SLI The measured signal % of requests with status 2xx and latency < 300 ms
SLO The target for the SLI over a window 99.9% over 28 rolling days
Error budget 100% − SLO, the permitted failure 0.1% of requests over 28 days
Burn rate How fast you’re spending the budget 1× = on pace to exactly exhaust it; 10× = 10× too fast
Compliance period The window the SLO is measured over Rolling 28 days (or calendar month)

The reason burn-rate alerting beats thresholds: a 99.9%/28-day budget tolerates roughly 40 minutes of total downtime per 28 days. If you burn at , you’ll use exactly the budget by the end of the window — fine. If you burn at 14.4×, you’ll exhaust a month’s budget in 2 hours — that is an emergency and should page now. The trick is to alert on a fast-burn (high rate over a short window → page) and a slow-burn (lower rate over a long window → ticket), so you catch both the sudden outage and the slow bleed without paging on every blip.

Alert tier Burn rate Short window Long window Action
Fast burn (page) ~14.4× 1 hour 5 minutes (confirmation) Page on-call now
Medium burn (page) ~6× 6 hours 30 minutes Page during the day
Slow burn (ticket) ~3× 24 hours 2 hours File a ticket; fix this sprint
Slow burn (ticket) ~1× 3 days 6 hours Investigate; budget trending bad

Create an availability SLO and a burn-rate alert with gcloud. First the SLO on a service (here a Cloud Run service registered in Monitoring):

# Create a request-based availability SLO: 99.9% good over a 28-day rolling window
gcloud monitoring slos create \
  --service=checkout \
  --slo-id=availability-999 \
  --display-name="Checkout availability 99.9%/28d" \
  --goal=0.999 \
  --rolling-period=28d \
  --request-based-good-total-ratio \
  --good-service-filter='metric.type="run.googleapis.com/request_count" metric.labels.response_code_class!="5xx"' \
  --total-service-filter='metric.type="run.googleapis.com/request_count"'

Then a fast-burn alerting policy on that SLO’s burn rate (the console’s “Create SLO alert” wizard generates exactly this — fast and slow windows wired together):

resource "google_monitoring_alert_policy" "slo_fast_burn" {
  display_name = "Checkout SLO fast burn (14.4x / 1h)"
  combiner     = "AND"   # both the long and short window must agree

  conditions {
    display_name = "1h burn > 14.4x"
    condition_threshold {
      filter          = "select_slo_burn_rate(\"${google_monitoring_slo.availability.name}\", \"3600s\")"
      comparison      = "COMPARISON_GT"
      threshold_value = 14.4
      duration        = "0s"
      aggregations { alignment_period = "300s"; per_series_aligner = "ALIGN_MEAN" }
    }
  }
  conditions {
    display_name = "5m burn > 14.4x (fast confirmation)"
    condition_threshold {
      filter          = "select_slo_burn_rate(\"${google_monitoring_slo.availability.name}\", \"300s\")"
      comparison      = "COMPARISON_GT"
      threshold_value = 14.4
      duration        = "0s"
      aggregations { alignment_period = "60s"; per_series_aligner = "ALIGN_MEAN" }
    }
  }
  notification_channels = [var.oncall_channel_id]
}

The combination — a long window to establish the trend and a short window to confirm it is still happening — is what stops a burn-rate alert from firing on a single bad minute and from staying stuck open after recovery. This is the single most valuable alerting pattern in the whole platform; if you build only one alert per service, build this.

Cloud Logging — queries, sinks, and log-based metrics

Cloud Logging ingests log entries from GCP services, the Ops Agent, and your application (via client libraries or gcloud logging write), stores them in log buckets, and lets you query them with the Logging query language. Two operational facts dominate everything you do here: logs cost money to ingest and retain, and logs can become metrics.

Querying and the log entry shape

Every entry has a resource (the monitored resource that emitted it), a severity, a timestamp, a logName, and a payload (textPayload for plain text, jsonPayload for structured). Structured JSON logs are vastly more useful — you can filter on fields. Query in Logs Explorer or via CLI:

# All ERROR+ entries from the checkout Cloud Run service in the last hour
gcloud logging read \
  'resource.type="cloud_run_revision"
   AND resource.labels.service_name="checkout"
   AND severity>=ERROR
   AND timestamp>="2026-06-23T00:00:00Z"' \
  --limit=50 --format=json

# Write a structured test log entry
gcloud logging write my-test-log \
  '{"event":"payment_declined","amount":4999,"user":"u123"}' \
  --payload-type=json --severity=WARNING
Log type Where it comes from Default retention Cost note
Platform logs GCP services (LB, Cloud SQL, etc.) 30 days (_Default bucket) Ingestion billed beyond free tier
User/app logs Your code, Ops Agent 30 days (_Default) The volume you most control
Admin Activity audit Who changed what (always on) 400 days (_Required) Free; cannot disable or shorten
Data Access audit Who read/queried data Off by default (except BigQuery) Can be huge; enable selectively
System Event audit GCP-initiated admin actions 400 days (_Required) Free
Access Transparency Google staff access to your data 400 days Available on eligible support tiers

Sinks — routing logs to where they live longest and cheapest

A sink matches entries with a filter and routes them to a destination: another log bucket, BigQuery (for SQL analytics), Cloud Storage (cheap long-term archive), or Pub/Sub (stream to a SIEM or external system). Two built-in sinks (_Required, _Default) already exist; you add your own for analytics and retention.

Sink destination Best for Query model Cost shape
Log bucket Searchable retention in Logging Logging query language Storage by volume + retention days
BigQuery SQL analytics, dashboards, joins SQL Storage + query (bytes scanned)
Cloud Storage Cheap long-term/compliance archive None (re-ingest to query) Cheapest per GB; object storage
Pub/Sub Stream to SIEM/Splunk/3rd party Consumer-defined Per-message; real-time fan-out

Create a sink that ships security-relevant logs to BigQuery for long-term analysis, then grant the sink’s writer identity permission to write to the dataset (the step people forget — the sink is created disabled-in-effect until its service account can write):

# Route all admin-activity audit logs to a BigQuery dataset
gcloud logging sinks create audit-to-bq \
  bigquery.googleapis.com/projects/$PROJECT_ID/datasets/security_logs \
  --log-filter='logName:"cloudaudit.googleapis.com%2Factivity"'

# The command prints a writerIdentity service account — grant it BigQuery write
SINK_SA=$(gcloud logging sinks describe audit-to-bq --format='value(writerIdentity)')
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="$SINK_SA" --role="roles/bigquery.dataEditor"
resource "google_logging_project_sink" "audit_to_bq" {
  name        = "audit-to-bq"
  destination = "bigquery.googleapis.com/projects/${var.project_id}/datasets/security_logs"
  filter      = "logName:\"cloudaudit.googleapis.com%2Factivity\""
  unique_writer_identity = true
}

resource "google_project_iam_member" "sink_writer" {
  project = var.project_id
  role    = "roles/bigquery.dataEditor"
  member  = google_logging_project_sink.audit_to_bq.writer_identity
}

Controlling log cost — exclusions and retention

The fastest way to a surprise bill is ingesting verbose logs nobody reads (health-check 200s, debug chatter, load-balancer success lines). You cut cost two ways: an exclusion filter on a sink (drop matching entries before they’re stored/billed in that bucket) and shorter retention on buckets you don’t need for 30+ days. Audit _Required logs cannot be excluded or shortened — that’s the floor.

Cost lever What it does Where Caution
Exclusion filter Stop storing matching entries On a sink/bucket Excluded logs are gone — don’t drop audit
Bucket retention Days entries are kept Per log bucket Longer = more storage cost
Sample then exclude Keep 1%, drop 99% of noisy success Exclusion with sample() Keep enough to debug
Route to GCS not Logging Archive cheaply, skip Logging storage Sink to Cloud Storage Not searchable without re-ingest
Disable Data Access logs Avoid huge read-audit volume Audit config You lose “who read what”
# Stop billing/storing load-balancer 200s in the default bucket (keep a 1% sample)
gcloud logging sinks update _Default \
  --add-exclusion=name=drop-lb-200s,filter='resource.type="http_load_balancer" AND httpRequest.status=200 AND NOT sample(insertId, 0.01)'

# Shorten a custom bucket's retention to 7 days
gcloud logging buckets update my-debug-bucket --location=global --retention-days=7

Log-based metrics — alert on what only logs know

Some signals exist only as log lines: a specific stack trace, a “circuit breaker open” message, a “payment declined” event. A log-based metric turns “count of entries matching a filter” into a chartable, alertable time series. There are two kinds: a counter (how many matching entries) and a distribution (extract a numeric field from each match into a histogram, giving you percentiles of, say, a logged latency).

Log-based metric type What it produces Use for Example filter/value
Counter Count of matching entries over time Rate of a specific event jsonPayload.event="payment_declined"
Distribution Histogram from an extracted number Percentiles of a logged value Value = jsonPayload.latency_ms
Boolean (via counter) Presence/absence of a condition “Did the FATAL line appear?” severity=CRITICAL
With labels The above, split by a field Per-tenant/per-region counts Label = jsonPayload.region
# Counter: number of payment-declined events, labeled by reason
gcloud logging metrics create payment_declined \
  --description="Count of declined payments" \
  --log-filter='resource.type="cloud_run_revision" AND jsonPayload.event="payment_declined"'

# Then alert on it like any metric:
#   logging.googleapis.com/user/payment_declined  (rate > threshold)
resource "google_logging_metric" "payment_declined" {
  name   = "payment_declined"
  filter = "resource.type=\"cloud_run_revision\" AND jsonPayload.event=\"payment_declined\""
  metric_descriptor {
    metric_kind = "DELTA"
    value_type  = "INT64"
    labels { key = "reason"; value_type = "STRING" }
  }
  label_extractors = {
    "reason" = "EXTRACT(jsonPayload.reason)"
  }
}

The counter then lives at logging.googleapis.com/user/payment_declined and is alertable exactly like a platform metric — which is how you page when declines spike, even though “declined” was never a metric, only a log line.

Cloud Trace and Cloud Profiler — latency and CPU at the function level

When a request is slow, a metric tells you that it’s slow; Cloud Trace tells you where. A trace is a tree of spans, each span a timed unit of work (an HTTP handler, a DB call, an RPC to another service), tagged with the same labels as your metrics and logs. Read the waterfall and the long bar is your culprit — the slow dependency, the N+1 query, the lock contention.

Aspect Cloud Trace Cloud Profiler
Answers “Which hop in this request is slow?” “Which function is burning CPU/memory?”
Data Distributed spans / latency waterfalls Statistical CPU/heap/contention profiles
Auto on Cloud Run, App Engine, LB (sampled) No — add the profiler agent/library
Instrumentation OpenTelemetry / OTLP, or auto Profiler library in the app
Overhead Sampled, low Continuous, low (statistical sampling)
Cost driver Spans ingested Effectively free for the profiling itself

Trace is automatic for a sampled fraction of requests on Cloud Run, App Engine and through the external load balancer. For full control and for non-auto services, instrument with OpenTelemetry and export via OTLP — the same pipeline you use for metrics and logs. The service account needs roles/cloudtrace.agent.

Cloud Profiler runs a tiny statistical profiler in your application that continuously samples CPU (and heap, contention, threads depending on language) and ships profiles to the console, where a flame graph shows which functions dominate. It is low overhead by design and effectively free — there is no good reason not to run it on a production service whose CPU bill you’d like to cut. Languages supported include Go, Java, Python and Node.js.

Signal Tool Typical “aha”
Slow endpoint, unknown cause Trace waterfall One downstream call is 800 ms of a 900 ms request
High CPU bill, unknown cause Profiler flame graph JSON serialization is 40% of CPU
Errors clustered by type Error Reporting One new exception after a deploy, spiking
Latency p99 rising over days Monitoring + Trace Memory leak → GC pauses, confirmed in Profiler

Error Reporting sits on top of Logging: it parses stack traces out of your error logs automatically, deduplicates them into groups, counts occurrences, and tells you which exception is new or spiking and which release introduced it. You don’t configure much — log your errors with stack traces and Error Reporting groups them for you, with a link straight to the offending logs and (if instrumented) traces.

Architecture at a glance

The Operations Suite is one pipeline with many sources and many readers. On the left, telemetry is produced: Google-managed services (Cloud Run, GKE, Compute Engine, Cloud SQL, load balancers) emit system metrics and logs automatically; VMs run the Ops Agent to add in-guest metrics and application log files; applications emit custom metrics, traces and structured logs via client libraries or OpenTelemetry/OTLP. All of it flows into the center: Cloud Monitoring stores time-series metrics (scoped through a metrics scope that can span projects), Cloud Logging stores log entries in log buckets, and Cloud Trace stores spans. On the right, humans and systems consume it: dashboards and Metrics Explorer (MQL/PromQL) for charts, uptime checks and alerting policies that evaluate conditions and SLO burn rate and fan out to notification channels (email, Slack, PagerDuty), log-based metrics that loop log matches back into the metrics store, and sinks that route logs onward to BigQuery, Cloud Storage and Pub/Sub. Follow any incident left-to-right: a source emits, the store correlates by shared labels, and a reader (a dashboard, an alert, a query) turns it into action.

Cloud Monitoring, Logging and Trace collect metrics, logs and traces from Google-managed services, the Ops Agent on VMs, and instrumented apps; the data lands in the Monitoring metrics store, Logging buckets and Trace, then feeds dashboards, uptime checks, alerting policies, SLOs, log-based metrics and sinks to BigQuery, Cloud Storage and Pub/Sub.

The alert path deserves its own walk-through, because it is where the value is realized. A metric (or a computed SLO burn rate, or a log-based metric) is continuously evaluated against an alerting policy’s condition. When the condition holds for its configured duration, the policy opens an incident and pushes it to every attached notification channel — the on-call engineer’s pager, a Slack channel, a ticketing webhook. The incident carries the documentation/runbook link you attached, so the responder lands with context instead of a bare “something is wrong.” When the metric recovers and stays healthy past the auto-close window, the incident closes itself. The diagram traces a metric breach from time series → condition → incident → notification → human, which is the entire reason you collected anything in the first place.

A metric or SLO burn rate crosses an alerting policy's condition for its configured duration, the policy opens an incident, and the incident fans out to notification channels (email, Slack, PagerDuty) with a runbook link, then auto-closes when the signal recovers.

Real-world scenario

Northwind Retail ran a checkout platform on GCP: an external HTTPS load balancer in front of three Cloud Run services (web, checkout, inventory), a Cloud SQL for PostgreSQL primary, and a Pub/Sub topic feeding an order-processing worker on GKE. They had “monitoring” in the sense that the GCP console showed graphs, but no dashboards they trusted, no SLOs, and exactly one alert: CPU > 90% on the Cloud SQL instance, which had fired so often during nightly batch jobs that everyone muted it. Customers were their alerting system.

The breaking incident: a Friday-evening deploy of checkout shipped a change that created a new database connection per request instead of using the pool. At low traffic it was invisible. As evening peak built, connections to Cloud SQL climbed, the primary hit its connection limit, new requests started failing with 500s, and p99 latency on checkout went from 240 ms to 9 seconds. The first anyone heard of it was a customer tweet 35 minutes in. Two engineers spent 50 minutes guessing — was it the load balancer? Pub/Sub backlog? GKE? — before someone opened the Cloud SQL connections metric and saw the cliff.

The post-incident rebuild is the lesson. They defined an SLO for checkout: 99.9% of requests succeed (non-5xx) under 500 ms over 28 days, which gave them an error budget of roughly 40 minutes/28 days and, more importantly, a fast-burn alert (14.4× over 1 hour, confirmed at 5 minutes) wired to PagerDuty. They added an uptime check from three regions on /health of the public endpoint, alerting on multi-region failure. They added a metric-threshold alert on Cloud SQL database/postgresql/num_backends at 80% of the connection limit — the leading indicator of exactly the failure they’d just had. They built one incident dashboard for checkout: request rate, 5xx ratio, p50/p95/p99 latency (from the request_latencies distribution), Cloud SQL connections, and Pub/Sub oldest_unacked_message_age side by side. They turned on Cloud Profiler on the worker and Cloud Trace (already automatic on Cloud Run) so a slow request shows its slow span. Finally they cut the noise: an exclusion dropping load-balancer 200s (which had been a third of their log bill) while keeping a 1% sample, and a sink shipping admin-activity audit logs to BigQuery for the security team.

The next regression — a slow N+1 query introduced three weeks later — paged the on-call engineer at the 6-minute mark via the fast-burn alert, while the budget was barely touched. The dashboard showed checkout latency climbing with flat error rate, a trace showed 14 sequential identical queries in one request, and it was rolled back in 11 minutes — before a single customer noticed. Same class of bug, two incidents: one a 90-minute public outage discovered by Twitter, the other an 11-minute non-event discovered by a burn-rate alert. The platform didn’t change. The observability did.

Advantages and disadvantages

Advantages Disadvantages
Integrated — metrics, logs, traces, profiles in one console, correlated by shared labels Cost can balloon on high log/trace ingest if you don’t exclude and sample
Most data is collected automatically and free from GCP services Per-project default for logs means multi-project visibility needs deliberate scopes/sinks
SLO + burn-rate alerting is first-class — page on user pain, not noise The metric model (kind/value-type/labels) has a learning curve; easy to mis-query at first
Two query languages (MQL and PromQL) — keep Prometheus skills or go GCP-native MQL is GCP-only; portability favors PromQL/OpenTelemetry, which need more setup
Ops Agent unifies host metrics + app logs in one daemon and one YAML Custom dashboards and good alerts take real design effort up front
Profiler is effectively free and continuously running Some advanced trace/Prometheus features carry ingestion charges to watch
Terraform/gcloud make all of it reproducible as code Audit Data Access logs can be enormous; enabling them carelessly is a bill and a privacy surface

When does the integration matter most? On multi-service architectures where a request crosses Cloud Run, Cloud SQL and Pub/Sub — the shared-label correlation collapses a four-tool investigation into one. When does the cost risk bite hardest? On chatty services that log every request at INFO and never set an exclusion; that is the most common surprise-bill story on GCP, and it is entirely preventable.

Hands-on lab

This lab is free-tier-friendly and takes about 20 minutes. You will deploy a tiny Cloud Run service, watch its metrics and logs flow in automatically, add an uptime check, create a log-based metric and an alert, and tear it all down. Replace PROJECT_ID and region as needed.

Step 1 — Set your project and enable the APIs.

export PROJECT_ID="$(gcloud config get-value project)"
export REGION="asia-south1"   # Mumbai; pick yours
gcloud services enable run.googleapis.com monitoring.googleapis.com \
  logging.googleapis.com cloudtrace.googleapis.com

Expected: each API reports Operation finished successfully (or is already enabled).

Step 2 — Deploy a sample service that logs structured JSON. Use Google’s hello container; it emits request logs automatically.

gcloud run deploy obs-demo \
  --image=us-docker.pkg.dev/cloudrun/container/hello \
  --region=$REGION --allow-unauthenticated

Expected: a Service URL like https://obs-demo-xxxx-el.a.run.app. Save it: export URL=$(gcloud run services describe obs-demo --region=$REGION --format='value(status.url)').

Step 3 — Generate traffic and confirm metrics arrive.

for i in $(seq 1 30); do curl -s -o /dev/null "$URL"; done
# Metrics take a minute or two; then read the request count
gcloud monitoring time-series list \
  --filter='metric.type="run.googleapis.com/request_count" AND resource.labels.service_name="obs-demo"' \
  --format='value(points[0].value.int64Value)' 2>/dev/null | head

Expected: after 1–2 minutes, non-zero request counts. (Metric data is not instantaneous.)

Step 4 — Read the logs that arrived automatically.

gcloud logging read \
  'resource.type="cloud_run_revision" AND resource.labels.service_name="obs-demo"' \
  --limit=5 --format='value(timestamp, httpRequest.requestMethod, httpRequest.status)'

Expected: recent rows showing GET 200. No agent was installed — Cloud Run logs are automatic.

Step 5 — Create an uptime check on the service URL.

HOST=$(echo "$URL" | sed 's#https://##')
gcloud monitoring uptime create "obs-demo-uptime" \
  --resource-type=uptime-url \
  --resource-labels=host=$HOST,project_id=$PROJECT_ID \
  --path="/" --port=443 --protocol=https --period=1 --timeout=10
gcloud monitoring uptime list-configs --format='table(displayName)'

Expected: the check appears in the list. Within minutes it begins reporting check_passed.

Step 6 — Create a log-based metric for 4xx/5xx responses, and an email channel + alert.

# Counter of non-2xx responses
gcloud logging metrics create obs_demo_errors \
  --description="Non-2xx responses to obs-demo" \
  --log-filter='resource.type="cloud_run_revision" AND resource.labels.service_name="obs-demo" AND httpRequest.status>=400'

# Email channel
gcloud beta monitoring channels create --display-name="lab-email" \
  --type=email --channel-labels=email_address=$(gcloud config get-value account)

Expected: the metric logging.googleapis.com/user/obs_demo_errors now exists; the channel is created. Build the threshold alert on that metric in the console (Alerting → Create policy → select the user metric) or via a Terraform policy as shown earlier.

Step 7 — Validate. Hit a non-existent path a few times to generate 404s, wait ~2 minutes, and confirm the log-based metric increments:

for i in $(seq 1 10); do curl -s -o /dev/null "$URL/nope-$i"; done
gcloud logging read \
  'resource.type="cloud_run_revision" AND resource.labels.service_name="obs-demo" AND httpRequest.status=404' \
  --limit=3 --format='value(timestamp, httpRequest.status)'

Expected: 404 rows appear, and the obs_demo_errors metric (in Metrics Explorer) shows a bump.

Step 8 — Teardown. Remove everything so you incur nothing:

gcloud run services delete obs-demo --region=$REGION --quiet
gcloud logging metrics delete obs_demo_errors --quiet
# Delete the uptime check and channel by their IDs:
gcloud monitoring uptime delete \
  "$(gcloud monitoring uptime list-configs --filter='displayName=obs-demo-uptime' --format='value(name)')" --quiet
gcloud beta monitoring channels delete \
  "$(gcloud beta monitoring channels list --filter='displayName=lab-email' --format='value(name)')" --quiet

Expected: each resource confirms deletion. Cloud Run scales to zero anyway, but deleting is tidy.

Common mistakes & troubleshooting

The differentiator. Each row is a real failure mode: symptom → root cause → how to confirm → fix.

# Symptom Root cause How to confirm Fix
1 “My VM’s memory/disk metric isn’t in Monitoring” No agent installed; hypervisor can’t see in-guest systemctl status google-cloud-ops-agent shows not-installed Install the Ops Agent; grant SA monitoring.metricWriter
2 Agent installed but no metrics/logs Instance SA lacks metricWriter/logWriter Check SA roles; agent log /var/log/google-cloud-ops-agent/ shows 403 Grant the two writer roles to the instance SA
3 Log-based counter is always zero Filter doesn’t match (wrong field/label) Run the same filter in Logs Explorer — zero results Fix the filter; structured jsonPayload.x vs textPayload
4 Alert never fires though metric is high Counter metric without ALIGN_RATE; or wrong filter Preview the condition in the policy editor — flat/empty line Use ALIGN_RATE for counters; verify the series in Metrics Explorer
5 Alert fires constantly (page fatigue) Duration 0 / threshold on absolute count Incidents open and close every minute Add a 5–10 min duration; alert on a ratio, not a raw count
6 Sink created but no data in BigQuery Sink writer SA lacks bigquery.dataEditor on the dataset gcloud logging sinks describe → check writerIdentity binding Grant the writer identity write access to the destination
7 Uptime check flaps red/green Single region transient; timeout too low Check shows alternating regions; latency near timeout Use multiple regions; raise timeout; alert on multi-region fail
8 “No metrics from another project” in my dashboard That project isn’t in the metrics scope Monitoring → Settings → Metrics scope list Add the project to the scoping project’s metrics scope
9 SLO shows 100% but users complain SLI measures the wrong thing (e.g. LB 200 ≠ correct response) Compare SLI definition to actual user journey Redefine the SLI to a real good-request signal (status + latency)
10 Log bill spiked Verbose success logs (health checks, LB 200s) ingested Logging → Logs storage shows top contributors Add an exclusion (sample 1%); route archive to GCS
11 Trace shows no spans for my service Not auto-instrumented and no OTLP exporter Trace list empty for the service Add OpenTelemetry/OTLP; grant SA cloudtrace.agent
12 Alert opened but nobody was notified Policy has no notification channel attached Policy detail shows zero channels Attach a channel; test it via “Send test notification”
13 Custom metric write returns 403 SA lacks monitoring.metricWriter API error PERMISSION_DENIED on timeSeries.create Grant monitoring.metricWriter to the writer SA
14 Metric exists but percentiles look wrong Treating a DISTRIBUTION as a GAUGE Descriptor valueType: DISTRIBUTION Use a percentile aligner (e.g. ALIGN_PERCENTILE_99)
15 Two services’ metrics collide on a dashboard Filtering on metric type but not resource labels Series multiply unexpectedly Add resource.labels.service_name= to the filter

A few diagnostic shortcuts for the most common of these:

If you see… It’s probably… Do this
A flat/empty preview in the alert editor Wrong aligner or filter Build the query in Metrics Explorer first, then paste
403 in the agent log Missing writer IAM role on the instance SA Add monitoring.metricWriter + logging.logWriter
Sink “succeeded” but empty destination Writer identity not granted on the destination Bind the printed writerIdentity with the destination’s editor role
SLO green during a real outage SLI proxy is too lenient Tie the SLI to status code AND latency of the real path
Surprise Logging bill Unexcluded high-volume success logs Exclude with sample(); shorten non-critical bucket retention

Best practices

Security notes

Cost & sizing

GCP observability is “free until it isn’t.” Most metrics are free (Google system metrics and a generous custom-metric allowance), uptime checks and dashboards cost nothing meaningful, and Profiler is free. The two line items that actually move the bill are log ingestion/storage and, to a lesser degree, trace/Managed-Prometheus ingestion. The discipline is to ingest deliberately.

Cost driver What’s free / cheap What costs How to control
System metrics All Google-service metrics Nothing to do
Custom metrics A monthly allowance per project Beyond the allowance, per metric-month Don’t explode label cardinality
Log ingestion A monthly free allotment per project; audit _Required free Per GiB beyond the free allotment Exclude + sample noisy success logs
Log storage/retention 30 days in _Default included Longer retention, extra buckets Shorten retention; archive to GCS
Uptime checks Generous free allowance Very high check volume Sensible frequency (1–5 min)
Trace ingestion A monthly free span allowance Spans beyond the allowance Sample; don’t trace 100% of high-QPS
Managed Prometheus Per sample ingested Drop unused series; scrape sensibly
Profiler Effectively free Run it everywhere

Rough planning numbers (always confirm against the current GCP pricing page — these move): log ingestion is billed per GiB after a free allotment per project per month, so a service logging every request at INFO can quietly push tens of GiB/month and a real bill; excluding load-balancer 200s alone often cuts a third of that. For an Indian startup running, say, a handful of Cloud Run services, a disciplined setup (system metrics free, a few custom metrics, logs sampled and excluded, audit logs to BigQuery) commonly lands the observability bill in the low thousands of rupees per month — and an undisciplined one (everything logged, nothing excluded, long retention) can be 10× that for no extra insight. The cheapest observability improvement you can make is almost always an exclusion filter, not more dashboards.

Right-sizing move When Effect
Exclude + 1%-sample success logs Always, day one Biggest single log-cost cut
Shorten retention on debug/non-audit buckets Logs you don’t need 30 days Lower storage cost
Archive compliance logs to Cloud Storage Long-retention, rarely queried Cheapest per GB
Cap custom-metric label cardinality Per-user/per-request labels creeping in Avoids metric-month explosion
Sample traces on high-QPS services >1000 req/s services Keeps span ingest bounded
Move multi-project view to one metrics scope Many projects One pane, no extra ingest cost

Interview & exam questions

  1. What is the difference between Cloud Monitoring and Cloud Logging? Monitoring stores and queries time-series metrics (and runs uptime checks, alerts, SLOs); Logging stores and queries log entries and routes them via sinks. They share a console and IAM, and you can convert logs to metrics with log-based metrics. Maps to PCA and Cloud DevOps Engineer.

  2. What does a metrics scope do, and why does it matter? It defines the set of projects Cloud Monitoring can see through one scoping project, enabling a single pane of glass across a multi-project fleet. Without adding a project to the scope, its metrics won’t appear in that scope’s dashboards.

  3. When do you need the Ops Agent versus relying on automatic collection? GCP collects system metrics and logs from managed services automatically; you install the Ops Agent on VMs to get in-guest signals the hypervisor can’t see — process memory, filesystem usage, application log files.

  4. Explain an SLO, an error budget, and burn rate. An SLO is a reliability target on an SLI over a window (e.g. 99.9%/28d). The error budget is the allowed failure (0.1%). Burn rate is how fast you’re spending that budget; you alert when a fast-burn (high rate, short window) indicates you’ll exhaust the budget quickly.

  5. Why alert on a ratio rather than an absolute error count? An absolute count (e.g. “40 errors”) scales with traffic and produces false alarms as you grow or quiet false-negatives at night; a ratio (“2% of requests fail”) is traffic-independent and reflects user pain consistently.

  6. What’s the difference between a counter and a distribution log-based metric? A counter counts matching log entries (rate of an event); a distribution extracts a numeric field from each match into a histogram, giving you percentiles of a logged value (e.g. p99 of a logged latency).

  7. How do you export logs for long-term analysis, and what’s the gotcha? Create a sink to BigQuery/Cloud Storage/Pub/Sub; the gotcha is you must grant the sink’s writer identity permission on the destination, or the sink silently writes nothing.

  8. MQL or PromQL — how do you choose? PromQL (via Managed Service for Prometheus) if your team already uses Prometheus/Grafana and exporters and wants portability; MQL if you’re GCP-native and want rich ratio/join/window math without running exporters. Both back alerts.

  9. Why might an SLO read 100% while users are unhappy? The SLI measures the wrong thing — e.g. counting load-balancer 200s as “good” when the body is an error page, or ignoring latency. Fix by tying the SLI to status and latency of the real user journey.

  10. How do you cut a runaway Cloud Logging bill? Add exclusion filters (drop and/or 1%-sample high-volume success logs like LB 200s), shorten retention on non-audit buckets, and archive compliance logs to Cloud Storage. Audit _Required logs are free and can’t be reduced.

  11. What’s the fastest way to find which function is burning CPU on a service? Run Cloud Profiler (continuous, low-overhead, effectively free) and read the flame graph; it pinpoints the dominant functions without code changes beyond adding the library.

  12. A request is slow but error-free — which tool localizes the slow hop? Cloud Trace — read the span waterfall; the longest span (a slow downstream call, an N+1 query) is the culprit. Metrics tell you it’s slow; traces tell you where.

Quick check

  1. Your VM shows CPU in Monitoring but not memory-used-percent. What’s missing, and which two IAM roles fix the agent path?
  2. You want an alert that fires when 99.9%-availability is being burned 14.4× too fast over an hour — what is this alert pattern called, and why two windows?
  3. A log-based counter you created reads zero even though the events are happening. What are the two most likely causes?
  4. You created a sink to BigQuery and see no rows. What did you forget?
  5. Which signal — metric, log, or trace — do you reach for first when a request is slow but returns 200, and why?

Answers

  1. The Ops Agent isn’t installed (the hypervisor can’t see in-guest memory). The instance service account needs roles/monitoring.metricWriter and roles/logging.logWriter.
  2. Fast-burn SLO alerting. You use a long window (1 hour) to establish the burn trend and a short window (5 minutes) to confirm it’s still happening, so you neither fire on a single bad minute nor stay stuck open after recovery.
  3. The filter doesn’t match (wrong field — jsonPayload.x vs textPayload, or a typo), or you’re charting a counter without ALIGN_RATE so it looks flat. Test the exact filter in Logs Explorer first.
  4. You forgot to grant the sink’s writer identity (writerIdentity) write permission on the destination dataset (roles/bigquery.dataEditor). The sink writes nothing until then.
  5. Cloud Trace. A 200 means no error to find in logs and the metric only says “slow”; the trace waterfall shows exactly which span (downstream call, query) consumed the time.

Glossary

Next steps

GCPCloud MonitoringCloud LoggingCloud TraceSLOMQLPromQLObservability
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading