Your issue involves three interconnected problems that need systematic resolution. Let me address each area based on your symptoms.
Metric type filtering: Custom metrics require exact filter syntax with proper escaping. Your filter looks correct syntactically, but you need to verify the metric is actually registered. List all metric descriptors first:
GET /v3/projects/{project}/metricDescriptors
filter: metric.type=starts_with("custom.googleapis.com")
This confirms your custom metric exists and shows its exact type string. Common issues: extra spaces, wrong domain prefix, or the metric uses a different project. Once confirmed, your timeSeries query needs aggregation parameters:
filter: metric.type="custom.googleapis.com/app/latency"
interval.endTime: 2024-12-14T16:00:00Z
interval.startTime: 2024-12-14T15:00:00Z
aggregation.alignmentPeriod: 60s
aggregation.perSeriesAligner: ALIGN_MEAN
IAM permissions: Monitoring Viewer alone isn’t sufficient for programmatic access to custom metrics. Your service account needs these specific permissions:
- monitoring.timeSeries.list (read metric data)
- monitoring.metricDescriptors.list (discover available metrics)
- monitoring.metricDescriptors.get (read metric metadata)
Create a custom role or use Monitoring Metric Reader role which includes all three. The console works because it uses your user credentials with broader permissions.
API resource scoping: This is likely your main issue. Custom metrics can have resource labels that must be included in your filter. If your metric has resource.type or resource.labels, you MUST filter on them:
filter: metric.type="custom.googleapis.com/app/latency" AND
resource.type="gce_instance" AND
resource.labels.instance_id="your-instance-id"
The console automatically scopes to visible resources, but the API requires explicit scoping. List your metric descriptor to see required resource labels, then include them in every query. Without proper resource scoping, the API returns empty results even when data exists.
Test with a simplified query first - remove all optional parameters and just query the last hour with required resource filters. Once that works, add aggregation and additional filters incrementally.