Observability stores the metrics your services send over OpenTelemetry (OTLP) or Prometheus remote write, next to their logs and traces. Metrics go to the dataset for their environment and are read with PromQL through a Prometheus-compatible API, so Grafana’s Prometheus data source and other Prometheus clients work unchanged.
Send OTLP metrics
Any OTLP metric exporter works: the OpenTelemetry Collector, SDKs and language agents. Set the base endpoint and the ingest key header; the exporter adds the metrics path itself.
OTEL_EXPORTER_OTLP_ENDPOINT=https://obs-ingest.ezghcloud.com
OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer%20ezgh_ik_…"
OTEL_METRICS_EXPORTER=otlp
OTEL_RESOURCE_ATTRIBUTES="service.name=checkout,ezgh.metadata.application=storefront,ezgh.metadata.environment=production"This works with OTEL_EXPORTER_OTLP_PROTOCOL set to grpc, http/protobuf or http/json.
OTEL_EXPORTER_OTLP_HEADERS uses the W3C Baggage format, so the space after Bearer is written
%20.
| Protocol | Endpoint |
|---|---|
| OTLP/gRPC | obs-ingest.ezghcloud.com:443, method opentelemetry.proto.collector.metrics.v1.MetricsService/Export |
| OTLP/HTTP | POST https://obs-ingest.ezghcloud.com/v1/metrics, protobuf or JSON, optionally gzip |
The ingest key must allow obs.metrics.ingest. A key that doesn’t is refused with 403; create
a key that allows it, or a second key:
ezgh ingest-keys create otel-collector --action obs.logs.ingest,obs.traces.ingest,obs.metrics.ingest --project web- All five OTLP point types are stored: gauges, sums, histograms, exponential histograms and summaries, with their exemplars.
- Cumulative and delta temporality are both accepted and stored as sent. A sum or histogram with unspecified temporality is rejected.
- A point with the
no recorded valueflag is a Prometheus staleness marker: the series ends there.
With the Collector
The example uses the current exporter name, otlp_grpc. Collectors released before the rename
call it otlp. A Collector’s prometheus receiver can scrape Prometheus targets into the same
pipeline.
processors:
resource:
attributes:
- { key: ezgh.metadata.environment, value: production, action: insert }
exporters:
otlp_grpc/ezgh:
endpoint: obs-ingest.ezghcloud.com:443
headers: { authorization: "Bearer ${env:EZGH_INGEST_KEY}" }
service:
pipelines:
metrics:
receivers: [otlp]
processors: [resource]
exporters: [otlp_grpc/ezgh]See Send from a Collector for batching, retries and routing to several projects.
Send with Prometheus remote write
A Prometheus server (or any remote-write sender) writes to
POST https://obs-ingest.ezghcloud.com/api/v1/write, with the ingest key as a bearer token.
Remote write 1.0 and 2.0 are both accepted, chosen by the request’s Content-Type.
global:
external_labels:
ezgh_environment: production
ezgh_application: storefront
remote_write:
- url: https://obs-ingest.ezghcloud.com/api/v1/write
protobuf_message: io.prometheus.write.v2.Request
authorization:
credentials_file: /etc/prometheus/ezgh-ingest-key- Environment and application come from the
ezgh_environmentandezgh_applicationlabels, normalized like theezgh.metadata.*resource attributes (a missing or invalid value isunknown). They’re removed from the stored labels and read back asenvironmentandapplication. Set them withexternal_labels, as above, or per target with relabeling. - Use remote write 2.0 (
protobuf_message: io.prometheus.write.v2.Request, Prometheus 3.0 and later) where you can: it carries each series’ type, unit and help. Remote write 1.0 samples are stored with the typeunknown. - Only bearer authorization is accepted;
basic_authandsigv4are refused with401. - Classic histograms and summaries arrive as their
_bucket,_sum,_countand{quantile}series and are stored that way. Native histograms (schema −4 to 8, and custom buckets) are read back as native histograms.
| Answer | Meaning | The sender |
|---|---|---|
204 |
Every sample and histogram was stored | Continues |
400 |
Some or all samples were refused: the body says how many and why. The rest were stored | Drops the batch, without retrying |
401, 403 |
The key is missing, invalid or revoked, or doesn’t allow obs.metrics.ingest |
Drops the batch |
413 |
Over 5 MiB compressed, or 64 MiB decompressed | Drops the batch |
415 |
A Content-Type or Content-Encoding other than remote write’s protobuf and snappy |
Drops the batch |
429, 503 |
Over a rate, or briefly unavailable, with Retry-After |
Retries |
Remote write 2.0 answers also carry X-Prometheus-Remote-Write-Samples-Written,
…-Histograms-Written and …-Exemplars-Written.
Environments and datasets
Each point goes to the dataset for its environment, created automatically the first time an
environment sends data, as for logs and traces. A point without an environment goes to the
unknown dataset. See Service metadata.
Query with PromQL
Each project has a Prometheus HTTP API at
https://obs.ezghcloud.com/v1/organizations/<organization ID>/projects/<project ID>/obs/prometheusezgh projects get <project> -o json shows both IDs. A query reads every dataset (environment)
of the project that the caller may query, so one query can compare environments; select one
with an environment matcher, such as up{environment="production"}.
| Path, under the base URL | Methods | Operation |
|---|---|---|
/api/v1/query |
GET, POST |
Instant query: QueryMetrics, QueryMetricsPost |
/api/v1/query_range |
GET, POST |
Range query: QueryMetricsRange, QueryMetricsRangePost |
/api/v1/series |
GET, POST |
Series matching selectors: ListMetricSeries, ListMetricSeriesPost |
/api/v1/labels |
GET, POST |
Label names: ListMetricLabels, ListMetricLabelsPost |
/api/v1/label/<name>/values |
GET |
One label’s values: ListMetricLabelValues |
/api/v1/metadata |
GET |
Each metric’s type, unit and help: GetMetricMetadata |
/api/v1/query_exemplars |
GET, POST |
Exemplars, with trace and span IDs: QueryMetricExemplars, QueryMetricExemplarsPost |
/api/v1/status/buildinfo |
GET |
The Prometheus version the API is compatible with: GetMetricsBuildInfo |
- Requests and answers are Prometheus’s:
POSTbodies are form-encoded, times are RFC 3339 or Unix seconds, and answers are{"status": "success", "data": …}. Errors are{"status": "error", "errorType": …, "error": …}with an addedcode, the platform’s error code. - Authenticate with
Authorization: Bearerand an API key, a bot’s API key, or an OAuth access token. Ingest keys are refused. - Without
start,labels,label/<name>/values,series,metadataandquery_exemplarscover the last 6 hours, not all time. - The lookback delta is 5 minutes, as in Prometheus. A query’s
lookback_deltacan set 1 second to 1 hour.
Grafana
Add a Prometheus data source with the project’s base URL and a bot’s API key:
-
Create a bot for Grafana and a policy that allows only
obs.metrics.queryin the project, then a key for the bot:ezgh iam bots create grafana ezgh iam policies create metrics-query --document @metrics-query.json ezgh iam policies attach metrics-query --bot grafana ezgh iam api-keys create grafana --bot grafana --expires-in-days 365metrics-query.jsonjson { "statements": [ { "effect": "allow", "actions": ["obs.metrics.query"], "resources": [ "ezgh:*:org_k3f9a0x2m7qp:prj_g7h8i9j0k1l2", "ezgh:*:org_k3f9a0x2m7qp:prj_g7h8i9j0k1l2:*" ] } ] } -
In Grafana, open Connections > Data sources > Add data source and choose Prometheus.
-
Set Prometheus server URL to the project’s base URL.
-
Under HTTP headers, add the header
Authorizationwith the valueBearer ezgh_…(the bot’s key). Leave Authentication as No Authentication. -
Under Advanced settings, keep HTTP method
POST, and set Prometheus type to Prometheus. -
Select Save & test.
Or provision it:
apiVersion: 1
datasources:
- name: EZGH web
type: prometheus
access: proxy
url: https://obs.ezghcloud.com/v1/organizations/<organization ID>/projects/<project ID>/obs/prometheus
jsonData:
httpMethod: POST
prometheusType: Prometheus
httpHeaderName1: Authorization
secureJsonData:
httpHeaderValue1: Bearer ${EZGH_GRAFANA_KEY}One data source reads one project. Grafana alerting works against it like any Prometheus.
From the CLI
ezgh obs metrics query 'sum by (job) (rate(http_server_request_duration_seconds_count[5m]))' --project web
ezgh obs metrics query 'up{environment="production"}' --from 6h --step 5m --project web
ezgh obs metrics label-values __name__ --environment production --project webSee ezgh obs metrics. -o json prints the API’s answer.
Names and labels
OTLP metric names and attributes are translated to Prometheus names when they’re read, the way Prometheus’s own OTLP receiver translates them by default. What was sent is stored unchanged.
| OTLP metric | Unit | Type | PromQL names |
|---|---|---|---|
http.server.request.duration |
s |
Histogram | http_server_request_duration_seconds_bucket (with le), …_sum, …_count |
http.server.request.duration |
s |
Exponential histogram | http_server_request_duration_seconds, a native histogram |
process.cpu.time |
s |
Sum, monotonic | process_cpu_time_seconds_total |
system.memory.usage |
By |
Sum, not monotonic | system_memory_usage_bytes, a gauge |
http.server.active_requests |
{request} |
Sum, not monotonic | http_server_active_requests, a gauge |
rpc.server.duration |
ms |
Summary | rpc_server_duration_milliseconds (with quantile), …_sum, …_count |
-
Characters other than letters, digits and
_become_; the unit becomes a suffix (annotations in braces are dropped), and a monotonic sum gets_total. -
Labels are the point’s attributes, translated the same way (
http.response.status_codeishttp_response_status_code). Two attributes that translate to the same label have their values joined with;. -
Every series also has
environmentandapplication(fromezgh.metadata.*),job(service.namespace/service.name, orservice.name) andinstance(service.instance.id). A sent attribute or remote-write label namedenvironmentorapplicationwith a different value is read asexported_environmentorexported_application, soenvironmentalways names the dataset. -
Other resource attributes are in
target_info: one series per resource, value1, with the resource attributes as labels plusenvironment,application,jobandinstance. Join it to add them:sum by (k8s_namespace_name) ( rate(http_server_request_duration_seconds_count[5m]) * on (environment, job, instance) group_left (k8s_namespace_name) target_info ) -
The instrumentation scope’s name and version aren’t labels. Series that differ only in scope or in a resource attribute other than
jobandinstanceread as one series; the answer then carries a warning that series were merged. EZGH Obs Agent 0.3.0 copies the Kubernetes resource attributes onto each point, so its series havek8s_cluster_name,k8s_node_name,k8s_pod_nameand the other Kubernetes labels. -
Delta sums and histograms are accumulated from the start of the query, so
rate,increaseandhistogram_quantileare exact. A delta counter’s raw value counts from the query’s start. -
Remote-write series keep their names and labels as sent, apart from
ezgh_environmentandezgh_application. -
Generated series. Every dataset that receives traces also has the
traces_spanmetrics_*andtraces_service_graph_*series that Observability derives from its spans. They’re free; see Span metrics.
Limits
A point that breaks a limit is rejected; the rest of the request is stored. OTLP answers say how
many points were rejected and why in partial_success; remote write answers 400.
| Limit | Value |
|---|---|
| Request body | 5 MiB compressed, 64 MiB uncompressed |
| Point size | 64 KiB, with its resource and scope |
| Metric name | 1 KiB |
| Unit | 256 bytes |
| Attributes per map | 256 (point, resource, scope and each exemplar) |
| Explicit histogram buckets | 160 bounds |
| Exponential histogram | Scale −4 to 20 (above 8 is read at scale 8), 160 buckets per range |
| Summary quantiles | 64, each between 0 and 1 |
| Exemplars per point | 16 |
| Point time | Within the dataset’s metrics retention, and at most 1 day ahead |
Counts at or above 263 are rejected. From remote write, gauge native histograms and float histograms with fractional counts are rejected.
Query budgets
Every query reads stored points, so a long range at a short step can be expensive. A query over a
budget is refused with 422 and code query_too_expensive, with budget naming which one,
never cut short. Narrow it: a shorter range, a larger step, or more label matchers.
| Budget | Value | budget |
|---|---|---|
| Samples in memory while evaluating | 5,000,000 | samples |
| Evaluation time | 30 seconds (a query’s timeout can only lower it) |
time |
| Points read | 20,000,000 per request | fetchedSamples |
| Series | 100,000 per request | series |
| Data read | 20 GiB per request | bytes |
| Sample data fetched | 128 MiB per request | memory |
| Answer size | 64 MiB | response |
| Metric names, label values, exemplars | 100,000, 50,000, 10,000 | rows |
| Other limit | Value |
|---|---|
| Points per range query | 11,000 ((end − start) / step); more is 400 |
match[] selectors |
1 to 20 |
| Query text | 64 KiB; a POST body 256 KiB (413) |
| Queries running at once | 20 per organization. A query waits up to 2 seconds for a turn, then is refused with 429 and Retry-After. A query can also get 429 when the service is busy |
| Status | errorType |
code |
When |
|---|---|---|---|
400 |
bad_data |
invalid_request |
A parameter or the PromQL is invalid, or too many points |
401 |
unauthorized |
unauthenticated |
No valid credential |
403 |
forbidden |
access_denied |
The caller may query none of the project’s datasets, or none of those the query’s environment matchers select |
404 |
not_found |
not_found |
The organization or project doesn’t exist, or the caller can’t see it |
422 |
execution or timeout |
query_too_expensive |
Over a budget |
422 |
execution |
query_failed |
The query failed to evaluate, as Prometheus answers it |
429 |
unavailable |
too_many_requests |
Too many queries at once, with Retry-After |
503 |
unavailable |
unavailable |
Briefly unavailable, with Retry-After |
Retention
Each signal of a dataset has its own retention class. Metrics default to 14d-395d, 13 months,
so a year can be compared with the last; logs and traces default to 7d-30d. Every class can be
used for every signal.
| Class | Hot days | Total days |
|---|---|---|
7d-30d |
7 | 30 |
7d-90d |
7 | 90 |
14d-365d |
14 | 365 |
14d-395d |
14 | 395 |
cold-365d |
0 | 365 |
- A dataset’s
retentionholds each signal’s class:{"logs": "7d-30d", "traces": "7d-30d", "metrics": "14d-395d"}.retentionClassis the logs class, which traces follow unless set separately; changing it never changes the metrics class. - Set one signal’s class with
retentionin CreateDataset or UpdateDataset, such as{"retention": {"metrics": "14d-365d"}}. Where this isn’t available yet, the request is refused with409 signal_retention_unavailable. - A new class applies to points that arrive afterward; stored points keep the class they arrived with.
- Points are kept at full resolution for the whole class: there’s no downsampling.
ezgh obs datasets getshows every signal’s class, and ListRetentionClasses returns each signal’s default indefaults.
Active series
GetDataset returns activeSeries, an estimate of the distinct
metric series the dataset received in the previous complete hour:
"activeSeries": { "from": "2026-10-09T13:00:00Z", "to": "2026-10-09T14:00:00Z", "estimate": 41250 }The estimate is within about 1%, and counts by when points were received. It’s null when the
estimate is briefly unavailable. ezgh obs datasets get shows it as Active series. A series
is one combination of metric, attributes, resource and scope. Span metrics
aren’t counted.
Access
| Action | Allows |
|---|---|
obs.metrics.ingest |
Sending metrics, with a project ingest key |
obs.metrics.query |
Querying a dataset’s metrics with PromQL (returns metric data and label values) |
ObsFullAccess includes both. ObsReadOnlyAccess and ReadOnlyAccess include
obs.metrics.query, so read-only users can query metrics. A query reads only the datasets the caller may query, checked on every request.
Trails
Requests to the Prometheus API aren’t recorded in Trails: queries, label and series lookups, metadata and build info only read, and reads of telemetry aren’t recorded. Sending metrics isn’t recorded either.
Pricing
Metrics are metered by ingested bytes and by stored bytes, hot and cold. These meters aren’t billed yet.