Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2d44371
use serde_repr to strictly type SumAggregationTemporality
KennanHunter Jul 30, 2026
3478261
case-insensitive parse out OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PRE…
KennanHunter Jul 30, 2026
fed5e14
return data points according to temporality
KennanHunter Jul 30, 2026
9a46c79
return start_time_unix_nano with otel counters and sum
KennanHunter Jul 30, 2026
c838155
use explicit OpenMetricType enum instead of magic strings
KennanHunter Jul 31, 2026
9132134
flatten and extract helpers to make relationship between OpenMetricType
KennanHunter Jul 31, 2026
d63fad7
Update JSON schema
KennanHunter Jul 31, 2026
5d59018
test OtelTemporalityPreference to make codecov happy
KennanHunter Jul 31, 2026
1ced1c3
otel: default temporality by datadog presence and centralize warning
KennanHunter Aug 1, 2026
3c3d165
add pgdog-jsonschema line to CONTRIBUTING.md
KennanHunter Aug 1, 2026
74dfe77
update 9918e9 to use typed OpenMetricType
KennanHunter Aug 7, 2026
81dc14f
otel: derive effective temporality at read time
KennanHunter Aug 7, 2026
92ddcf3
rename SumAggregationTemporality to AggregationTemporality
KennanHunter Aug 13, 2026
dc639b8
feat(stats): record query latency in a fixed-bucket histogram
alexkarp-umd Aug 13, 2026
b1d7d25
feat(stats): expose query time histogram on the OpenMetrics endpoint
alexkarp-umd Aug 13, 2026
24c8e90
feat(stats): export query time histogram over OTLP
alexkarp-umd Aug 13, 2026
e7c7f02
stats: debug-assert metric type matches its measurements
alexkarp-umd Aug 13, 2026
fbe70ba
perf(stats): share histogram bounds allocation across pools and expor…
alexkarp-umd Aug 13, 2026
d6bdb16
fix(stats): address review feedback on the query time histogram
alexkarp-umd Aug 21, 2026
1cd6d64
fix(stats): refuse a query_time_buckets ladder PgDog can't use
alexkarp-umd Aug 21, 2026
4ace29e
refactor(stats): trim dead code from the query time histogram
alexkarp-umd Aug 21, 2026
a21bb2f
test(stats): cover the latch conflict remedies
alexkarp-umd Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion .schema/pgdog.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@
"query_parser_engine": "pg_query_protobuf",
"query_size_limit": null,
"query_size_limit_action": "warn",
"query_time_buckets": [
0.1,
0.3,
1.0,
3.0,
10.0,
30.0,
100.0,
300.0,
1000.0,
3000.0,
10000.0,
30000.0
],
"query_timeout": 9223372036854775807,
"read_write_split": "include_primary",
"read_write_strategy": "conservative",
Expand Down Expand Up @@ -175,7 +189,8 @@
"endpoint": null,
"headers": {},
"namespace": null,
"push_interval": 0
"push_interval": 10000,
"temporality_preference": "Cumulative"
}
},
"plugins": {
Expand Down Expand Up @@ -1085,6 +1100,28 @@
"$ref": "#/$defs/QuerySizeLimitAction",
"default": "warn"
},
"query_time_buckets": {
"description": "Upper bounds, in milliseconds, of the `query_time_seconds` histogram buckets.\n\nEach bound emits one time series per pool, so prefer a short ladder that\nbrackets the latencies worth alerting on. Values are sorted and\ndeduplicated, and an implicit `+Inf` bucket is always appended. At most\n20 bounds are accepted and every one must be finite and greater than\nzero; a ladder PgDog cannot use is refused at startup rather than\nrepaired, so the exported buckets always match what was configured.\n\n**Note:** This setting cannot be changed at runtime. Restart PgDog after changing it.\n\n**Note:** A malformed `PGDOG_QUERY_TIME_BUCKETS` falls back to the default\nladder instead of being refused, matching every other environment variable.\n\n_Default:_ `[0.1, 0.3, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000, 30000]`\n\nEnv: `PGDOG_QUERY_TIME_BUCKETS` (comma-separated milliseconds)\n\n<https://docs.pgdog.dev/configuration/pgdog.toml/general/#query_time_buckets>",
"type": "array",
"default": [
0.1,
0.3,
1.0,
3.0,
10.0,
30.0,
100.0,
300.0,
1000.0,
3000.0,
10000.0,
30000.0
],
"items": {
"type": "number",
"format": "double"
}
},
"query_timeout": {
"description": "Maximum amount of time to wait for a Postgres query to finish executing.\n\n<https://docs.pgdog.dev/configuration/pgdog.toml/general/#query_timeout>",
"type": "integer",
Expand Down Expand Up @@ -1598,10 +1635,42 @@
"format": "uint64",
"default": 10000,
"minimum": 0
},
"temporality_preference": {
"description": "Describes how the exported metric points should be described.\n\nSee https://opentelemetry.io/docs/specs/otel/metrics/data-model/#metric-points\n\n_Default:_ `Cumulative`, or `Delta` when `datadog_api_key` is set.\n\nEnv: `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`",
"anyOf": [
{
"$ref": "#/$defs/OtelTemporalityPreference"
},
{
"type": "null"
}
],
"default": "Cumulative"
}
},
"additionalProperties": false
},
"OtelTemporalityPreference": {
"description": "Aggregation temporality used when exporting OTLP metric points.",
"oneOf": [
{
"description": "Points report the value accumulated since the exporter started.",
"type": "string",
"const": "Cumulative"
},
{
"description": "Points report the change since the last export.",
"type": "string",
"const": "Delta"
},
{
"description": "Delta for sums, cumulative for histograms; minimizes exporter memory.",
"type": "string",
"const": "LowMemory"
}
]
},
"PassthroughAuth": {
"description": "toggle automatic creation of connection pools given the user name, database and password.\n\nSee [passthrough authentication](https://docs.pgdog.dev/features/authentication/#passthrough-authentication).\n\n<https://docs.pgdog.dev/configuration/pgdog.toml/general/#passthrough_auth>",
"oneOf": [
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ Contributions are welcome. If you see a bug, feel free to submit a PR with a fix
1. Please format your code with `cargo fmt`.
2. If you're feeling generous, `cargo clippy` as well.
3. Please write and include tests. This is production software used in one of the most important areas of the stack.
4. If changes have been made to configuration schemas, run `cargo run -p pgdog-jsonschema`
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions example.pgdog.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ openmetrics_port = 9090
#
# Default: none
openmetrics_namespace = "pgdog_"
# Upper bounds, in milliseconds, of the "query_time_seconds" histogram buckets.
#
# Each bound emits one time series per pool, so prefer a short ladder that
# brackets the latencies worth alerting on. Read once at startup: changing
# this requires a restart.
#
# At most 20 bounds, each finite and greater than zero. A ladder PgDog cannot
# use is refused at startup rather than repaired, so the buckets you scrape are
# always the ones you configured here.
#
# Default: [0.1, 0.3, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000, 30000]
query_time_buckets = [0.1, 0.3, 1.0, 3.0, 10.0, 30.0, 100.0, 300.0, 1000.0, 3000.0, 10000.0, 30000.0]
# Log output format.
#
# Default: text
Expand Down
30 changes: 30 additions & 0 deletions pgdog-config/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::fs::read_to_string;
use std::path::{Path, PathBuf};
use tracing::{error, info, warn};

use crate::otel_temporality::OtelTemporalityPreference;
use crate::sharding::ShardedSchema;
use crate::util::random_string;
use crate::{
Expand Down Expand Up @@ -120,6 +121,7 @@ impl ConfigAndUsers {
self.config.check();
self.users.check(&self.config);
self.validate_server_auth()?;
self.warn_if_data_dog_cumulative();
Ok(())
}

Expand Down Expand Up @@ -149,6 +151,33 @@ impl ConfigAndUsers {
Ok(())
}

fn warn_if_data_dog_cumulative(&self) {
match (
&self.config.otel.datadog_api_key,
&self.config.otel.temporality_preference,
) {
(Some(_datadog_present), Some(OtelTemporalityPreference::Cumulative))
if std::env::var("IGNORE_DATADOG_CUMULATIVE_WARNING")
.ok()
.as_deref()
!= Some("1") =>
{
warn!(
"Sending Cumulative OTLP sums/histograms to Datadog is stateful and lossy: \
all points on a timeseries must reach the same Agent/exporter (constraining \
how you scale collectors), the first point of a new series may be dropped \
(causing gaps on restart), and histogram min/max may be missing or \
approximated. See \
https://docs.datadoghq.com/opentelemetry/guide/otlp_delta_temporality/?tab=python#implications-of-using-cumulative-aggregation-temporality. \
Set IGNORE_DATADOG_CUMULATIVE_WARNING=1 to silence."
);
}
_ => {
// valid
}
}
}

/// Prepared statements are enabled.
pub fn prepared_statements(&self) -> PreparedStatements {
// Disable prepared statements automatically in session mode
Expand Down Expand Up @@ -284,6 +313,7 @@ pub struct Config {
///
/// <https://docs.pgdog.dev/configuration/pgdog.toml/otel/>
#[serde(default)]
#[schemars(default = "Otel::schema_default")]
pub otel: Otel,

/// HashiCorp Vault settings, required for users configured with `server_auth = "vault"`.
Expand Down
103 changes: 103 additions & 0 deletions pgdog-config/src/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,29 @@ pub struct General {
/// <https://docs.pgdog.dev/configuration/pgdog.toml/general/#openmetrics_namespace>
pub openmetrics_namespace: Option<String>,

/// Upper bounds, in milliseconds, of the `query_time_seconds` histogram buckets.
///
/// Each bound emits one time series per pool, so prefer a short ladder that
/// brackets the latencies worth alerting on. Values are sorted and
/// deduplicated, and an implicit `+Inf` bucket is always appended. At most
/// 20 bounds are accepted and every one must be finite and greater than
/// zero; a ladder PgDog cannot use is refused at startup rather than
/// repaired, so the exported buckets always match what was configured.
///
/// **Note:** This setting cannot be changed at runtime. Restart PgDog after changing it.
///
/// **Note:** A malformed `PGDOG_QUERY_TIME_BUCKETS` falls back to the default
/// ladder instead of being refused, matching every other environment variable.
///
/// _Default:_ `[0.1, 0.3, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000, 30000]`
///
/// Env: `PGDOG_QUERY_TIME_BUCKETS` (comma-separated milliseconds)
///
/// <https://docs.pgdog.dev/configuration/pgdog.toml/general/#query_time_buckets>
#[serde(default = "General::query_time_buckets")]
#[schemars(default = "General::schema_default_query_time_buckets")]
pub query_time_buckets: Vec<f64>,

/// Enables support for prepared statements.
///
/// _Default:_ `extended`
Expand Down Expand Up @@ -881,6 +904,7 @@ impl Default for General {
query_size_limit_action: Self::query_size_limit_action(),
openmetrics_port: Self::openmetrics_port(),
openmetrics_namespace: Self::openmetrics_namespace(),
query_time_buckets: Self::query_time_buckets(),
prepared_statements: Self::prepared_statements(),
query_parser_enabled: Self::query_parser_enabled(),
query_parser: QueryParserLevel::default(),
Expand Down Expand Up @@ -954,6 +978,11 @@ impl Default for General {
}

impl General {
/// Default upper bounds of the `query_time_seconds` histogram, in milliseconds.
pub const DEFAULT_QUERY_TIME_BUCKETS: [f64; 12] = [
0.1, 0.3, 1.0, 3.0, 10.0, 30.0, 100.0, 300.0, 1_000.0, 3_000.0, 10_000.0, 30_000.0,
];

fn env_or_default<T: std::str::FromStr>(env_var: &str, default: T) -> T {
env::var(env_var)
.ok()
Expand Down Expand Up @@ -1312,6 +1341,31 @@ impl General {
Self::env_option_string("PGDOG_OPENMETRICS_NAMESPACE")
}

/// Default `query_time_seconds` bucket bounds, in milliseconds.
///
/// Exponential from 100µs to 30s. A malformed env var degrades to the
/// defaults instead of failing startup; it degrades as a whole, so an
/// operator never gets a ladder made of the values that happened to parse.
pub fn query_time_buckets() -> Vec<f64> {
let Some(raw) = Self::env_option_string("PGDOG_QUERY_TIME_BUCKETS") else {
return Self::DEFAULT_QUERY_TIME_BUCKETS.to_vec();
};

raw.split(',')
.map(|value| value.trim().parse::<f64>())
.collect::<Result<Vec<_>, _>>()
.ok()
.filter(|buckets| !buckets.is_empty())
.unwrap_or_else(|| Self::DEFAULT_QUERY_TIME_BUCKETS.to_vec())
}

/// Schema-only default, so the generated schema documents the shipped
/// bounds rather than whatever `PGDOG_QUERY_TIME_BUCKETS` happened to be
/// set to when the schema was generated.
fn schema_default_query_time_buckets() -> Vec<f64> {
Self::DEFAULT_QUERY_TIME_BUCKETS.to_vec()
}

fn default_dns_ttl() -> Option<u64> {
Self::env_option("PGDOG_DNS_TTL")
}
Expand Down Expand Up @@ -1928,6 +1982,55 @@ mod tests {
assert_eq!(General::openmetrics_namespace(), None);
}

#[test]
fn test_query_time_buckets() {
let _guard = remove_env_var("PGDOG_QUERY_TIME_BUCKETS");

let general = General::default();
assert_eq!(
general.query_time_buckets,
General::DEFAULT_QUERY_TIME_BUCKETS.to_vec()
);

let general: General = toml::from_str("query_time_buckets = [1.0, 10.0, 100.0]").unwrap();
assert_eq!(general.query_time_buckets, vec![1.0, 10.0, 100.0]);

// Omitting the setting keeps the defaults.
let general: General = toml::from_str("").unwrap();
assert_eq!(
general.query_time_buckets,
General::DEFAULT_QUERY_TIME_BUCKETS.to_vec()
);
}

#[test]
fn test_env_query_time_buckets() {
let _guard = set_env_var("PGDOG_QUERY_TIME_BUCKETS", "1, 5,25");
assert_eq!(General::query_time_buckets(), vec![1.0, 5.0, 25.0]);

// Nothing parseable falls back to the defaults rather than
// disabling bucketing.
let _guard = set_env_var("PGDOG_QUERY_TIME_BUCKETS", "nonsense");
assert_eq!(
General::query_time_buckets(),
General::DEFAULT_QUERY_TIME_BUCKETS.to_vec()
);

// One bad value discards the whole ladder: keeping the rest would build
// a histogram the operator never asked for.
let _guard = set_env_var("PGDOG_QUERY_TIME_BUCKETS", "1,abc,3");
assert_eq!(
General::query_time_buckets(),
General::DEFAULT_QUERY_TIME_BUCKETS.to_vec()
);

let _guard = remove_env_var("PGDOG_QUERY_TIME_BUCKETS");
assert_eq!(
General::query_time_buckets(),
General::DEFAULT_QUERY_TIME_BUCKETS.to_vec()
);
}

#[test]
fn test_env_invalid_enum_values() {
let _guard = set_env_var("PGDOG_POOLER_MODE", "invalid_mode");
Expand Down
1 change: 1 addition & 0 deletions pgdog-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod general;
pub mod memory;
pub mod networking;
pub mod otel;
pub mod otel_temporality;
pub mod overrides;
pub mod pooling;
pub mod replication;
Expand Down
Loading