TigerData
Monitor TigerData (TimescaleDB) hypertables, the columnstore, and background jobs and policies.
Your TimescaleDB databases show up alongside the rest of your telemetry, on dashboards that are already built. You can see how your hypertables are growing and how much the columnstore saves, and keep watch on the background jobs that compress, retain, and aggregate your data - with alerts when one fails, crashes, or falls behind, or when a server stops reporting. Works for self-hosted TimescaleDB and Tiger Cloud.
What this integration does
It provisions dashboards and alert rules. It does not collect anything itself - Grafana Alloy gathers what TimescaleDB knows about its hypertables, columnstore, and background jobs by running a few queries against each database where TimescaleDB is installed. This works for self-hosted TimescaleDB 2.17 and later and for Tiger Cloud services. For the PostgreSQL server itself - connections, cache, locks - install the PostgreSQL integration as well. Until Alloy is collecting, the dashboards are empty.
Navigate to Integrations from the left-hand sidebar, then select TigerData.
Permission tiers
TigerData runs Read-only, the default. It requires a database user Alloy can log in as - it needs no privileges - and an OpsPilot API key.
Its capabilities are Dashboards and Alerts.
Grafana's own docs cover the exporter in full: see prometheus.exporter.postgres for every option, and install Alloy anywhere that can reach the database.
1. Create a monitoring user
Connect as an administrator - on Tiger Cloud, tsdbadmin - and create a user for Alloy:
It needs no other privilege: TimescaleDB's information views and size functions are readable by any user who can connect, and show sizes and job status, never data.
2. Save the queries
Save this as timescaledb-queries.yaml beside Alloy's config file:
timescaledb_extension:
query: |
SELECT current_database() AS datname, extversion AS version, 1 AS info
FROM pg_extension WHERE extname = 'timescaledb'
metrics:
- datname: {usage: LABEL, description: "Database"}
- version: {usage: LABEL, description: "TimescaleDB version"}
- info: {usage: GAUGE, description: "TimescaleDB is installed in this database"}
timescaledb_hypertable:
cache_seconds: 300
query: |
SELECT current_database() AS datname, h.hypertable_schema AS schema, h.hypertable_name AS hypertable,
h.num_chunks AS chunks,
h.compression_enabled::int AS columnstore_enabled,
hypertable_size(format('%I.%I', h.hypertable_schema, h.hypertable_name)::regclass) AS size_bytes,
coalesce(cs.number_compressed_chunks, 0) AS columnstore_chunks,
coalesce(cs.before_compression_total_bytes, 0) AS columnstore_before_bytes,
coalesce(cs.after_compression_total_bytes, 0) AS columnstore_after_bytes
FROM timescaledb_information.hypertables h
LEFT JOIN LATERAL hypertable_compression_stats(format('%I.%I', h.hypertable_schema, h.hypertable_name)::regclass) cs ON true
metrics:
- datname: {usage: LABEL, description: "Database"}
- schema: {usage: LABEL, description: "Hypertable schema"}
- hypertable: {usage: LABEL, description: "Hypertable"}
- chunks: {usage: GAUGE, description: "Chunks"}
- columnstore_enabled: {usage: GAUGE, description: "1 if the columnstore (compression) is enabled"}
- size_bytes: {usage: GAUGE, description: "Total size, tables, indexes and TOAST"}
- columnstore_chunks: {usage: GAUGE, description: "Chunks in the columnstore (compressed)"}
- columnstore_before_bytes: {usage: GAUGE, description: "Size of the columnstore chunks before conversion"}
- columnstore_after_bytes: {usage: GAUGE, description: "Size of the columnstore chunks after conversion"}
timescaledb_job:
query: |
SELECT current_database() AS datname, j.job_id::text AS job_id, j.proc_name AS proc,
coalesce(ca.view_name, j.hypertable_name, '') AS relation,
j.scheduled::int AS scheduled,
extract(epoch FROM j.schedule_interval) AS schedule_interval_seconds,
coalesce((s.last_run_status = 'Failed')::int, 0) AS last_run_failed,
coalesce((s.job_status = 'Running')::int, 0) AS running,
s.total_runs AS runs_total, s.total_failures AS failures_total,
coalesce(st.consecutive_failures, 0) AS consecutive_failures,
coalesce(st.total_crashes, 0) AS crashes_total,
coalesce(st.consecutive_crashes, 0) AS consecutive_crashes,
CASE WHEN s.last_successful_finish > '-infinity' THEN extract(epoch FROM s.last_successful_finish) END AS last_success_timestamp_seconds
FROM timescaledb_information.jobs j
JOIN timescaledb_information.job_stats s USING (job_id)
LEFT JOIN _timescaledb_internal.bgw_job_stat st USING (job_id)
LEFT JOIN timescaledb_information.continuous_aggregates ca
ON ca.materialization_hypertable_schema = j.hypertable_schema AND ca.materialization_hypertable_name = j.hypertable_name
metrics:
- datname: {usage: LABEL, description: "Database"}
- job_id: {usage: LABEL, description: "Job id"}
- proc: {usage: LABEL, description: "The job's procedure: a policy, or a user action"}
- relation: {usage: LABEL, description: "The hypertable or continuous aggregate the job works on"}
- scheduled: {usage: GAUGE, description: "1 if the job is scheduled, 0 if paused"}
- schedule_interval_seconds: {usage: GAUGE, description: "How often the job runs"}
- last_run_failed: {usage: GAUGE, description: "1 if the job's last run failed"}
- running: {usage: GAUGE, description: "1 while the job is running"}
- runs_total: {usage: COUNTER, description: "Runs"}
- failures_total: {usage: COUNTER, description: "Failed runs"}
- consecutive_failures: {usage: GAUGE, description: "Failed runs since the last success"}
- crashes_total: {usage: COUNTER, description: "Runs whose background worker crashed"}
- consecutive_crashes: {usage: GAUGE, description: "Crashed runs since the last success"}
- last_success_timestamp_seconds: {usage: GAUGE, description: "When the job last finished successfully"}
Sizes are re-read every five minutes rather than every scrape, since they walk every chunk. Alloy reads this file only when it starts.
3. Configure Alloy
Alloy's config file is in a different place on each system:
| OS | Config file | After a change |
|---|---|---|
| Linux | /etc/alloy/config.alloy |
sudo systemctl restart alloy |
| Windows | C:\Program Files\GrafanaLabs\Alloy\config.alloy |
restart the Alloy service in services.msc |
| macOS | $(brew --prefix)/etc/alloy/config.alloy |
brew services restart grafana/grafana/alloy |
Put the monitoring user's password in a file beside the config, readable only by Alloy, written without a trailing newline, and add:
local.file "timescaledb_password" {
filename = "/etc/alloy/timescaledb-password"
is_secret = true
}
prometheus.exporter.postgres "timescaledb" {
data_source_names = ["postgresql://opspilot_monitor:" + local.file.timescaledb_password.content + "@db.example.com:5432/tsdb?sslmode=require"]
disable_default_metrics = true
disable_settings_metrics = true
custom_queries_config_path = "/etc/alloy/timescaledb-queries.yaml"
autodiscovery {
enabled = true
database_allowlist = ["tsdb"]
}
}
discovery.relabel "timescaledb" {
targets = prometheus.exporter.postgres.timescaledb.targets
rule {
target_label = "job"
replacement = "integrations/timescaledb"
}
}
prometheus.scrape "timescaledb" {
targets = discovery.relabel.timescaledb.output
scrape_interval = "60s"
forward_to = [prometheus.remote_write.opspilot.receiver]
}
A few things worth understanding:
- List every database that has TimescaleDB in
database_allowlist, and connect to one of them. The queries run in each; in a database without TimescaleDB they fail on every scrape. On Tiger Cloud the database istsdband the host and port are the service's, from the Console. - URL-encode the password if it contains
@,:,/,?,#, or%- it is part of a URL. - Keep the relabel rule. Alloy gives every PostgreSQL exporter the same
job, so without it these series would land beside the PostgreSQL integration's, and the dashboards select on thejobit sets. - For more than one server, repeat the exporter for each under a name of its own (letters, digits, and underscores), and list their targets together:
targets = array.concat(prometheus.exporter.postgres.one.targets, prometheus.exporter.postgres.two.targets).
4. Send data to OpsPilot
You need an OpsPilot API key, from Data and Licenses > API Keys. Put it in a file the same way, as opspilot-api-key beside the config file, and add:
local.file "opspilot_api_key" {
// Windows: "C:/Program Files/GrafanaLabs/Alloy/opspilot-api-key"
filename = "/etc/alloy/opspilot-api-key"
}
prometheus.remote_write "opspilot" {
endpoint {
url = "https://api.opspilot.com/v1/metrics"
headers = {
"authorization" = string.trim_space(local.file.opspilot_api_key.content),
}
}
}
Using environment variables instead
Environment variables work in place of files if you already manage Alloy's settings that way: write sys.env("TIMESCALEDB_PASSWORD") in place of local.file.timescaledb_password.content, and sys.env("OPSPILOT_API_KEY") for the key. Alloy reads its environment from /etc/default/alloy (Debian, Ubuntu), /etc/sysconfig/alloy (RHEL, SUSE), $(brew --prefix)/etc/alloy/config.env (macOS), or on Windows the Environment value of the registry key HKEY_LOCAL_MACHINE\SOFTWARE\GrafanaLabs\Alloy.
5. Confirm the data arrived
In OpsPilot, open Explore, select the Metrics data source, and run:
You should get one series per database, carrying its TimescaleDB version. If nothing comes back, run pg_up{job="integrations/timescaledb"}: 0 means Alloy cannot connect or log in, and the Alloy log for prometheus.exporter.postgres says why; no result at all means Alloy is not sending.
Once data is flowing, the Dashboard data column on Your installation links straight to the provisioned dashboards.
Need more help?
Contact support in the chat bubble and let us know how we can assist.