GCP charges $0.50 per GB of log ingestion. You get 50 GB free per project. After that, every health check, every debug statement, every verbose container log costs real money. Most teams don't notice until the bill arrives and Cloud Logging is the #2 line item. Exclusion filters, log sinks, and retention policies in Terraform fix this in under 30 minutes.
Here's what makes Cloud Logging so expensive: every GCP service generates logs by default, and they all route to the _Default sink. Load balancer health checks hitting /healthz 100 times per minute? Logged. Kubernetes container stdout with DEBUG-level noise? Logged. VPC flow logs for internal traffic? Logged. You pay $0.50 for every GB of that.
One company (Harness) found that just applying exclusion filters and routing logs to cheaper storage cut their logging bill in half, saving over $140K annually.
Let's do the same with Terraform.
📊 Cloud Logging Pricing Cheat Sheet
| Component | Cost | Notes |
|---|---|---|
| Log ingestion | $0.50/GB | The main cost driver |
| Free tier | 50 GB/month/project | Disappears fast at scale |
| Retention (default) | Free | 30 days included with ingestion |
| Retention (extended) | $0.01/GB/month | For logs kept > 30 days |
| _Required bucket | Free | Admin audit logs, can't modify |
| Exclusion filters | Free | Dropped logs = $0 ingestion |
| Log sinks to GCS | GCS pricing only | ~$0.020/GB vs $0.50/GB |
The math that hurts:
| Daily Ingestion | Monthly Volume | Billable (minus 50GB free) | Monthly Cost |
|---|---|---|---|
| 5 GB/day | 150 GB | 100 GB | $50 |
| 20 GB/day | 600 GB | 550 GB | $275 |
| 50 GB/day | 1,500 GB | 1,450 GB | $725 |
| 150 GB/day | 4,500 GB | 4,450 GB | $2,225 |
Most teams generating 50+ GB/day can cut 40-60% with the filters below. That's $290-$435/month saved just from the 50 GB/day example.
🔧 Step 1: Exclusion Filters (Biggest Win, Zero Cost)
Exclusion filters drop logs before ingestion. You never pay for excluded logs. These are the 5 filters that eliminate the most common noise:
# Filter 1: Health check logs (often the #1 volume source)
resource "google_logging_project_exclusion" "health_checks" {
name = "exclude-health-check-logs"
project = var.project_id
description = "Exclude load balancer health check noise"
filter = <<-EOT
resource.type = "http_load_balancer"
AND (httpRequest.requestUrl = "/health"
OR httpRequest.requestUrl = "/healthz"
OR httpRequest.requestUrl = "/readiness"
OR httpRequest.requestUrl = "/livez")
EOT
}
# Filter 2: Debug-level logs in non-dev environments
resource "google_logging_project_exclusion" "debug_logs" {
name = "exclude-debug-logs"
project = var.project_id
description = "Exclude DEBUG severity in staging/prod"
filter = "severity = \"DEBUG\""
}
# Filter 3: GKE system container noise
resource "google_logging_project_exclusion" "gke_system" {
name = "exclude-gke-system-noise"
project = var.project_id
description = "Exclude noisy kube-system and istio logs"
filter = <<-EOT
resource.type = "k8s_container"
AND (resource.labels.namespace_name = "kube-system"
OR resource.labels.namespace_name = "istio-system"
OR resource.labels.namespace_name = "gke-managed-system")
AND severity <= "INFO"
EOT
}
# Filter 4: VPC flow logs for internal traffic
resource "google_logging_project_exclusion" "internal_vpc_flows" {
name = "exclude-internal-vpc-flows"
project = var.project_id
description = "Exclude VPC flow logs for internal-only traffic"
filter = <<-EOT
resource.type = "gce_subnetwork"
AND jsonPayload.connection.src_ip =~ "^10\\."
AND jsonPayload.connection.dest_ip =~ "^10\\."
EOT
}
# Filter 5: Cloud SQL routine logs
resource "google_logging_project_exclusion" "cloudsql_routine" {
name = "exclude-cloudsql-routine"
project = var.project_id
description = "Exclude Cloud SQL routine checkpoint and stats logs"
filter = <<-EOT
resource.type = "cloudsql_database"
AND (textPayload =~ "checkpoint starting"
OR textPayload =~ "checkpoint complete"
OR textPayload =~ "automatic analyze")
EOT
}
⚠️ Critical: Excluded logs are gone forever. You cannot recover them. Always test your filter in the Logs Explorer first by pasting the filter string into the query box. Verify it only matches what you intend before deploying.
Typical savings from these 5 filters:
| Filter | Typical Volume Reduction | Why |
|---|---|---|
| Health checks | 15-30% | LBs check every 5-10 seconds per backend |
| Debug logs | 10-25% | Apps default to DEBUG in many frameworks |
| GKE system noise | 10-20% | kube-system is extremely chatty |
| Internal VPC flows | 5-15% | High-traffic internal services |
| Cloud SQL routine | 3-5% | Constant checkpoint/analyze messages |
Combined: 40-60% reduction in ingestion volume.
📦 Step 2: Route Logs to Cheaper Storage with Sinks
Some logs you need for compliance or debugging, but don't need in Cloud Logging's expensive $0.50/GB storage. Route them to Cloud Storage (GCS) at $0.020/GB instead:
# Create a GCS bucket for archived logs
resource "google_storage_bucket" "log_archive" {
name = "${var.project_id}-log-archive"
location = "US"
# Use lifecycle rules from previous post!
lifecycle_rule {
action {
type = "SetStorageClass"
storage_class = "NEARLINE"
}
condition {
age = 30
matches_storage_class = ["STANDARD"]
}
}
lifecycle_rule {
action {
type = "SetStorageClass"
storage_class = "COLDLINE"
}
condition {
age = 90
matches_storage_class = ["NEARLINE"]
}
}
lifecycle_rule {
action {
type = "Delete"
}
condition {
age = 365
}
}
labels = local.common_labels
}
# Sink: Route data access audit logs to GCS
resource "google_logging_project_sink" "audit_to_gcs" {
name = "audit-logs-to-gcs"
project = var.project_id
destination = "storage.googleapis.com/${google_storage_bucket.log_archive.name}"
filter = "logName:\"cloudaudit.googleapis.com/data_access\""
unique_writer_identity = true
}
# Grant the sink's service account write access to the bucket
resource "google_storage_bucket_iam_member" "log_writer" {
bucket = google_storage_bucket.log_archive.name
role = "roles/storage.objectCreator"
member = google_logging_project_sink.audit_to_gcs.writer_identity
}
# Exclude audit logs from _Default sink (avoid double-paying)
resource "google_logging_project_exclusion" "audit_from_default" {
name = "exclude-audit-from-default"
project = var.project_id
description = "Audit logs go to GCS, skip Cloud Logging"
filter = "logName:\"cloudaudit.googleapis.com/data_access\""
}
⚠️ Gotcha: If you create a sink WITHOUT adding an exclusion on the _Default sink, you pay twice - once for Cloud Logging ingestion and once for the sink destination. Always pair sinks with exclusions to avoid double-paying.
Cost comparison for 100 GB/month of audit logs:
| Destination | Storage Cost | Ingestion Cost | Total |
|---|---|---|---|
| Cloud Logging (default) | Included | $50/month | $50/month |
| GCS Standard | $2/month | $0 | $2/month |
| GCS Nearline (after 30d) | $1/month | $0 | $1/month |
That's a 96% savings on audit log storage while keeping them accessible for compliance.
⏱️ Step 3: Custom Retention Policies
Not all logs need 30 days of retention. Debug logs from last week? Useless. Reduce retention to save on storage:
# Custom log bucket with 7-day retention for debug/dev logs
resource "google_logging_project_bucket_config" "short_retention" {
project = var.project_id
location = "global"
bucket_id = "short-retention-logs"
retention_days = 7
description = "Short retention for debug and development logs"
}
# Sink to route dev namespace logs to the short-retention bucket
resource "google_logging_project_sink" "dev_to_short" {
name = "dev-logs-short-retention"
project = var.project_id
destination = "logging.googleapis.com/projects/${var.project_id}/locations/global/buckets/short-retention-logs"
filter = <<-EOT
resource.type = "k8s_container"
AND resource.labels.namespace_name =~ "^dev-"
EOT
unique_writer_identity = true
}
⚠️ Warning: Reducing retention below the current value deletes existing logs that fall outside the new window. There is no undo. Plan this change during a maintenance window and communicate with your team first.
🏢 Multi-Project Exclusion Module
Deploy the same exclusion filters across every project in your org:
variable "project_ids" {
type = list(string)
description = "List of project IDs to apply exclusion filters"
}
variable "exclusion_filters" {
type = map(object({
description = string
filter = string
}))
default = {
health-checks = {
description = "Exclude LB health check logs"
filter = "resource.type = \"http_load_balancer\" AND (httpRequest.requestUrl = \"/health\" OR httpRequest.requestUrl = \"/healthz\")"
}
debug-logs = {
description = "Exclude DEBUG severity"
filter = "severity = \"DEBUG\""
}
gke-system = {
description = "Exclude kube-system INFO and below"
filter = "resource.type = \"k8s_container\" AND resource.labels.namespace_name = \"kube-system\" AND severity <= \"INFO\""
}
}
}
# Deploy all exclusions to all projects
resource "google_logging_project_exclusion" "org_wide" {
for_each = {
for pair in setproduct(var.project_ids, keys(var.exclusion_filters)) :
"${pair[0]}-${pair[1]}" => {
project = pair[0]
filter_key = pair[1]
filter_conf = var.exclusion_filters[pair[1]]
}
}
name = each.value.filter_key
project = each.value.project
description = each.value.filter_conf.description
filter = each.value.filter_conf.filter
}
New project? Add it to project_ids. New filter? Add it to exclusion_filters. terraform apply. Done across every project instantly. ✅
💡 Quick Reference: What to Do First
| Action | Effort | Savings |
|---|---|---|
| Add health check exclusion filter | 5 min | 15-30% of ingestion |
| Add debug log exclusion | 5 min | 10-25% of ingestion |
| Add GKE system namespace exclusion | 5 min | 10-20% of ingestion |
| Route audit logs to GCS with sink | 15 min | 96% on audit storage |
| Set 7-day retention for dev logs | 10 min | Reduced retention cost |
| Deploy org-wide exclusion module | 20 min | Consistent across all projects |
Start with the health check exclusion. It's 5 minutes of Terraform and typically eliminates the single largest source of log waste. 🎯
📊 TL;DR
Cloud Logging ingestion = $0.50/GB (adds up FAST)
Free tier = 50 GB/month/project (not enough)
Exclusion filters = drop logs before ingestion = $0 cost
Health check logs = usually the #1 volume source, exclude them
Debug logs in prod = pure waste, exclude them
Sinks to GCS = $0.020/GB vs $0.50/GB (96% cheaper)
Sink + no exclusion = you pay TWICE (don't forget the exclusion!)
_Required bucket = free, can't modify (admin audit logs)
Retention < 30 days = no impact on bill (already included)
Test filters first = excluded logs are gone forever
Bottom line: Cloud Logging charges $0.50 for every GB you don't explicitly exclude. Five exclusion filters deployed in Terraform can cut your logging bill by 40-60%. That's thousands per year at scale for 30 minutes of work. 📉
Open your GCP billing console right now. Filter by Cloud Logging. If it's in your top 5 services by cost, come back here and deploy those exclusion filters. Your CFO will think you're a genius. 😀
Found this helpful? Follow for more GCP cost optimization with Terraform! 💬
Top comments (0)