|
| 1 | +--- |
| 2 | +layout: docs |
| 3 | +menu: |
| 4 | + docsplatform_{{.version}}: |
| 5 | + identifier: cluster-management-telemetry-data-check |
| 6 | + name: Verifying Telemetry Data |
| 7 | + parent: cluster-management |
| 8 | + weight: 100 |
| 9 | +menu_name: docsplatform_{{.version}} |
| 10 | +section_menu_id: guides |
| 11 | +--- |
| 12 | + |
| 13 | +# Verifying Telemetry Data (Logs & Metrics) |
| 14 | + |
| 15 | +This guide shows how to confirm your logs and metrics are actually being stored — not just |
| 16 | +collected — in the monitoring stack: **ClickHouse** for logs, **Thanos + S3** for metrics. |
| 17 | +See [OpenTelemetry Monitoring](otel-monitoring.md) for how the stack is set up. |
| 18 | + |
| 19 | +## Basics |
| 20 | + |
| 21 | +- Logs and metrics are stored in the **same S3 bucket**, under different prefixes: |
| 22 | + - Logs → `<bucket>/logs/` |
| 23 | + - Metrics → `<bucket>/metrics/` |
| 24 | +- Bucket, endpoint, and prefixes are defined in your `TelemetryStack` resource: |
| 25 | + ```bash |
| 26 | + kubectl get telemetrystack -n monitoring -o yaml |
| 27 | + ``` |
| 28 | + Look under `spec.logs.clickhouse.s3` and `spec.metrics.thanos.s3`. |
| 29 | +- ClickHouse database name is your tenant ID, not a fixed name like `otel` or `default`. |
| 30 | + Find it with `SHOW DATABASES` (see below) — the table inside is `otel_logs`. |
| 31 | +- All checks below run from inside the cluster via `kubectl exec`, so no port-forwarding |
| 32 | + or extra credentials are required except for the optional S3 listing step. |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## 1. Check logs are landing in ClickHouse |
| 37 | + |
| 38 | +Find the ClickHouse pod: |
| 39 | + |
| 40 | +```bash |
| 41 | +kubectl get pods -n monitoring -l app.kubernetes.io/name=clickhouse |
| 42 | +``` |
| 43 | + |
| 44 | +List databases and confirm the `otel_logs` table exists: |
| 45 | + |
| 46 | +```bash |
| 47 | +kubectl exec -n monitoring <clickhouse-pod> -- clickhouse-client --query "SHOW DATABASES" |
| 48 | +kubectl exec -n monitoring <clickhouse-pod> -- clickhouse-client --query "SHOW TABLES FROM <your-tenant-db>" |
| 49 | +``` |
| 50 | + |
| 51 | +Check row count and freshness: |
| 52 | + |
| 53 | +```bash |
| 54 | +kubectl exec -n monitoring <clickhouse-pod> -- clickhouse-client --query \ |
| 55 | + "SELECT count(), min(Timestamp), max(Timestamp), now() FROM <your-tenant-db>.otel_logs" |
| 56 | + |
| 57 | +kubectl exec -n monitoring <clickhouse-pod> -- clickhouse-client --query \ |
| 58 | + "SELECT count() FROM <your-tenant-db>.otel_logs WHERE Timestamp > now() - INTERVAL 5 MINUTE" |
| 59 | +``` |
| 60 | + |
| 61 | +**Healthy result:** `max(Timestamp)` is close to `now()`, and the 5-minute count is greater |
| 62 | +than zero. If both queries return recent data, logs are flowing end-to-end. |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## 2. Check metrics are landing in Thanos / S3 |
| 67 | + |
| 68 | +Find the Thanos query pod: |
| 69 | + |
| 70 | +```bash |
| 71 | +kubectl get pods -n monitoring -l app.kubernetes.io/name=thanos-query |
| 72 | +``` |
| 73 | + |
| 74 | +Ask it which StoreAPIs it's talking to and what time range each one covers: |
| 75 | + |
| 76 | +```bash |
| 77 | +kubectl exec -n monitoring <thanos-query-pod> -- \ |
| 78 | + wget -qO- --header="THANOS-TENANT: <your-tenant>" "http://localhost:9090/api/v1/stores" |
| 79 | +``` |
| 80 | + |
| 81 | +Look at the `minTime`/`maxTime` fields (Unix epoch **milliseconds**) for each store: |
| 82 | + |
| 83 | +```bash |
| 84 | +date -u -d @<maxTime-in-seconds> # drop the last 3 digits first to convert ms -> s |
| 85 | +``` |
| 86 | + |
| 87 | +**Healthy result:** `maxTime` should be within the last few minutes for the live/receive |
| 88 | +store, and within your compaction interval for the long-term S3-backed store. |
| 89 | + |
| 90 | +For a second confirmation, check the ingester's own TSDB head timestamp: |
| 91 | + |
| 92 | +```bash |
| 93 | +kubectl get pods -n monitoring -l app.kubernetes.io/name=thanos-receive-ingester |
| 94 | +kubectl exec -n monitoring <thanos-receive-ingester-pod> -- wget -qO- http://localhost:10902/metrics \ |
| 95 | + | grep prometheus_tsdb_head_max_time |
| 96 | +``` |
| 97 | + |
| 98 | +If this timestamp is stale (not advancing over successive checks), metrics are not being |
| 99 | +ingested. Metrics are shipped from each connected cluster by the OpenTelemetry stack |
| 100 | +(`appscode-otel-stack`). See the [Troubleshooting](#troubleshooting-metrics-stale-but-logs-healthy) |
| 101 | +section below for how to trace that pipeline. |
| 102 | + |
| 103 | +--- |
| 104 | + |
| 105 | +## 3. Check the S3 bucket directly (optional) |
| 106 | + |
| 107 | +Read the bucket, endpoint, and credentials from the `TelemetryStack` spec first — the same |
| 108 | +values back both the ClickHouse and Thanos configs: |
| 109 | + |
| 110 | +```bash |
| 111 | +kubectl get telemetrystack -n monitoring -o jsonpath='{.spec.metrics.thanos.s3}' |
| 112 | +``` |
| 113 | + |
| 114 | +Which client you use depends on where the bucket actually lives. The backend is |
| 115 | +S3-compatible in every case, so pick the matching variant below. |
| 116 | + |
| 117 | +### a. MinIO (in-cluster or S3-compatible with a custom endpoint) |
| 118 | + |
| 119 | +Use the MinIO client (`mc`) with the endpoint and access credentials from the spec. |
| 120 | +The `--insecure` flag is only needed when the endpoint serves a self-signed certificate: |
| 121 | + |
| 122 | +```bash |
| 123 | +kubectl run mc-check -n monitoring --rm -it --restart=Never --image=minio/mc:latest -- sh |
| 124 | +mc alias set myminio <endpoint> <accessKey> <secretKey> --insecure |
| 125 | +mc ls myminio/<bucket>/logs/ --insecure --recursive | tail |
| 126 | +mc ls myminio/<bucket>/metrics/ --insecure --recursive | tail |
| 127 | +``` |
| 128 | + |
| 129 | +### b. Real AWS S3 |
| 130 | + |
| 131 | +When the bucket is a genuine AWS S3 bucket, there is no custom endpoint — just pass the |
| 132 | +AWS access key, secret, and region. Drop `--endpoint-url` entirely so the CLI talks to |
| 133 | +the regional AWS endpoint: |
| 134 | + |
| 135 | +```bash |
| 136 | +kubectl run aws-check -n monitoring --rm -it --restart=Never --image=amazon/aws-cli:latest \ |
| 137 | + --env AWS_ACCESS_KEY_ID=<accessKey> \ |
| 138 | + --env AWS_SECRET_ACCESS_KEY=<secretKey> \ |
| 139 | + --env AWS_DEFAULT_REGION=<region> \ |
| 140 | + -- s3 ls s3://<bucket>/logs/ --recursive | tail |
| 141 | +# repeat for the metrics prefix |
| 142 | +kubectl run aws-check -n monitoring --rm -it --restart=Never --image=amazon/aws-cli:latest \ |
| 143 | + --env AWS_ACCESS_KEY_ID=<accessKey> \ |
| 144 | + --env AWS_SECRET_ACCESS_KEY=<secretKey> \ |
| 145 | + --env AWS_DEFAULT_REGION=<region> \ |
| 146 | + -- s3 ls s3://<bucket>/metrics/ --recursive | tail |
| 147 | +``` |
| 148 | + |
| 149 | +### c. s3grid (S3-compatible service with a custom endpoint) |
| 150 | + |
| 151 | +s3grid is S3-compatible but reached through its own endpoint, so use the AWS CLI with |
| 152 | +`--endpoint-url` pointing at the s3grid endpoint from the spec: |
| 153 | + |
| 154 | +```bash |
| 155 | +kubectl run s3grid-check -n monitoring --rm -it --restart=Never --image=amazon/aws-cli:latest \ |
| 156 | + --env AWS_ACCESS_KEY_ID=<accessKey> \ |
| 157 | + --env AWS_SECRET_ACCESS_KEY=<secretKey> \ |
| 158 | + --env AWS_DEFAULT_REGION=<region> \ |
| 159 | + -- s3 ls s3://<bucket>/logs/ --endpoint-url <s3grid-endpoint> --recursive | tail |
| 160 | +# repeat for the metrics prefix |
| 161 | +kubectl run s3grid-check -n monitoring --rm -it --restart=Never --image=amazon/aws-cli:latest \ |
| 162 | + --env AWS_ACCESS_KEY_ID=<accessKey> \ |
| 163 | + --env AWS_SECRET_ACCESS_KEY=<secretKey> \ |
| 164 | + --env AWS_DEFAULT_REGION=<region> \ |
| 165 | + -- s3 ls s3://<bucket>/metrics/ --endpoint-url <s3grid-endpoint> --recursive | tail |
| 166 | +``` |
| 167 | + |
| 168 | +> The same `mc`-based flow from (a) works against s3grid too — `mc alias set` accepts any |
| 169 | +> S3-compatible endpoint. Use whichever client you already have handy. |
| 170 | +
|
| 171 | +**Healthy result (any variant):** recent objects under both prefixes, with timestamps |
| 172 | +matching your retention/compaction schedule. |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Summary |
| 177 | + |
| 178 | +| Pipeline | Where to look | Healthy signal | |
| 179 | +|---|---|---| |
| 180 | +| Logs | ClickHouse `<tenant-db>.otel_logs` | `max(Timestamp)` ≈ now, non-zero rows in last 5 min | |
| 181 | +| Metrics | Thanos Query `/api/v1/stores`, receive ingester `/metrics` | `maxTime`/head timestamp advancing, close to now | |
| 182 | +| S3 | `<bucket>/logs/` and `<bucket>/metrics/` prefixes | Recent objects under both prefixes | |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +## Troubleshooting: metrics stale but logs healthy |
| 187 | + |
| 188 | +There is no Prometheus doing remote-write in this setup. Telemetry is shipped from each |
| 189 | +connected cluster by the **`appscode-otel-stack`** chart (built on `opentelemetry-kube-stack`), |
| 190 | +which runs two OpenTelemetry collectors in the `monitoring` namespace of the *connected* |
| 191 | +cluster: |
| 192 | + |
| 193 | +- **Daemonset (agent) collector**: runs on every node. Its `prometheus` receiver scrapes |
| 194 | + targets assigned by the **Target Allocator** (which discovers `ServiceMonitor`/`PodMonitor` |
| 195 | + CRs), and its `filelog` receiver collects container logs. Everything is forwarded over |
| 196 | + OTLP to the gateway collector. |
| 197 | +- **Gateway collector** (deployment): receives OTLP and exports to the monitoring cluster |
| 198 | + through `prom-label-proxy`: |
| 199 | + - **Metrics** → `prometheusremotewrite` exporter → `https://<ingest-endpoint>:10001/api/v1/receive` |
| 200 | + → Thanos receive. The `THANOS-TENANT` header is injected per batch by the |
| 201 | + `headers_setter` extension. |
| 202 | + - **Logs** → `clickhouse` exporter → the same `prom-label-proxy` endpoint → ClickHouse |
| 203 | + (`otel_logs` table). |
| 204 | + |
| 205 | +The `<ingest-endpoint>` depends on how the platform is deployed: |
| 206 | + |
| 207 | +- **DNS mode**: the exporters point at the monitoring cluster's domain name. |
| 208 | +- **IP mode**: the exporters use the fixed hostname `prom-label-proxy.monitoring.svc.cluster.local`, |
| 209 | + which is mapped to the monitoring cluster's IP via a `hostAliases` entry on the gateway |
| 210 | + collector pod (required for TLS certificate verification against a bare IP). |
| 211 | + |
| 212 | +Confirm the endpoint actually in use from the gateway collector's config: |
| 213 | + |
| 214 | +```bash |
| 215 | +kubectl get opentelemetrycollectors -n monitoring -o yaml | grep -B2 -A2 'endpoint:' |
| 216 | +``` |
| 217 | + |
| 218 | +Logs and metrics share the daemon → gateway OTLP hop and the mTLS client cert |
| 219 | +(`otel-client-cert`), but split into different exporters at the gateway. So if logs are |
| 220 | +healthy and metrics are stale, connectivity and certs are fine; the problem is specific |
| 221 | +to the metrics path: either **scraping** (daemon collector / Target Allocator) or the |
| 222 | +**remote-write export** (gateway → Thanos). Run these checks **on the connected cluster**: |
| 223 | + |
| 224 | +### a. Collector pods are running |
| 225 | + |
| 226 | +```bash |
| 227 | +kubectl get pods -n monitoring -l otel-collector-type=daemonset |
| 228 | +kubectl get pods -n monitoring -l otel-collector-type=deployment |
| 229 | +``` |
| 230 | + |
| 231 | +The daemonset collector must have one Running pod per node; a node without an agent pod |
| 232 | +silently drops that node's scrape targets (allocation is per-node). |
| 233 | + |
| 234 | +### b. Gateway is exporting metrics without errors |
| 235 | + |
| 236 | +Check the gateway collector's own telemetry for sent vs. failed metric points. The |
| 237 | +collector image is minimal (no shell or `wget` inside), so `kubectl exec` will not work; |
| 238 | +port-forward its internal telemetry port (8888) and query it from your machine instead: |
| 239 | + |
| 240 | +```bash |
| 241 | +kubectl port-forward -n monitoring <gateway-collector-pod> 8888:8888 & |
| 242 | +sleep 2 # give the port-forward a moment to establish |
| 243 | +curl -s http://localhost:8888/metrics \ |
| 244 | + | grep -E 'otelcol_exporter_(sent|send_failed|enqueue_failed).*metric_points' |
| 245 | +``` |
| 246 | + |
| 247 | +These counters are cumulative since the pod started, so a single reading proves nothing: |
| 248 | +a large `send_failed` value may just be leftover from a past incident (for example a |
| 249 | +monitoring-cluster restart) while everything is healthy now. Run the `curl` again after |
| 250 | +30 seconds or so and compare the two readings; only the counters that are still |
| 251 | +*advancing* matter. There are three possible outcomes: |
| 252 | + |
| 253 | +1. **`sent_metric_points{exporter="prometheusremotewrite"}` is increasing and |
| 254 | + `send_failed`/`enqueue_failed` stay flat.** The gateway is exporting successfully, so |
| 255 | + this cluster's pipeline is healthy. The problem is downstream (prom-label-proxy or |
| 256 | + Thanos receive on the monitoring cluster) or a tenant mismatch; continue with step (d). |
| 257 | +2. **`send_failed_metric_points` is climbing.** Exports are failing. Read the gateway logs |
| 258 | + for the actual `prometheusremotewrite` error (HTTP status codes, TLS failures) against |
| 259 | + the `prom-label-proxy` endpoint: |
| 260 | + |
| 261 | + ```bash |
| 262 | + kubectl logs -n monitoring <gateway-collector-pod> --tail=100 | grep -iE "error|prometheusremotewrite" |
| 263 | + ``` |
| 264 | + |
| 265 | + TLS or certificate errors point at the `otel-client-cert` secret (check it exists and |
| 266 | + is not expired). Connection refused or timeout points at a wrong or unreachable ingest |
| 267 | + endpoint. HTTP 4xx/5xx responses point at the prom-label-proxy / Thanos receive side |
| 268 | + on the monitoring cluster. |
| 269 | +3. **All counters are flat or zero.** The gateway has nothing to export, which means the |
| 270 | + daemon collectors are not scraping anything. Continue with step (c). |
| 271 | + |
| 272 | +A growing `enqueue_failed_metric_points` means the exporter's sending queue is |
| 273 | +overflowing because sends are too slow or failing, and those points are dropped; treat |
| 274 | +it the same as outcome 2. |
| 275 | + |
| 276 | +Note the exporter retries for a bounded time (`max_elapsed_time: 120s`) and then drops the |
| 277 | +batch, so persistent export errors mean permanent data loss, not delayed delivery. |
| 278 | + |
| 279 | +When finished, stop the background port-forward with `kill %1`. |
| 280 | + |
| 281 | +### c. Scrape targets exist and are being allocated |
| 282 | + |
| 283 | +If the gateway shows nothing to export (`sent` flat, no errors), the daemon collectors |
| 284 | +aren't scraping anything. Check that the Target Allocator is running and that |
| 285 | +`ServiceMonitor`/`PodMonitor` resources exist for your workloads: |
| 286 | + |
| 287 | +```bash |
| 288 | +kubectl get pods -n monitoring -l app.kubernetes.io/component=opentelemetry-targetallocator |
| 289 | +kubectl get servicemonitors,podmonitors -A |
| 290 | +``` |
| 291 | + |
| 292 | +The Target Allocator watches these CRs and distributes their scrape targets across the |
| 293 | +daemonset collectors. No matching monitors (or an unhealthy allocator) means the |
| 294 | +`prometheus` receiver has nothing to scrape: no data flows and no errors are logged |
| 295 | +anywhere, which is exactly the "silently stale" symptom. |
| 296 | + |
| 297 | +### d. Data flowing under the wrong tenant |
| 298 | + |
| 299 | +Metrics may be arriving in Thanos but under a different tenant than the one you're |
| 300 | +querying. The gateway derives the tenant from resource attributes: it defaults to |
| 301 | +`default`, and is set to the source namespace name only for namespaces labeled |
| 302 | +`ace.appscode.com/client-org: "true"`; batches with no tenant attribute fall back to |
| 303 | +`anonymous`. Re-run the store/query checks from section 2 with `THANOS-TENANT: default` |
| 304 | +(and `anonymous`) before concluding data is missing. |
| 305 | + |
| 306 | +After fixing whichever stage was broken, re-run the `prometheus_tsdb_head_max_time` check |
| 307 | +from section 2 over a couple of minutes to confirm the timestamp starts advancing. |
0 commit comments