Skip to content

Commit f228a79

Browse files
authored
add configurable log format and level (#822)
Related to #815 Adds configurable logging output via `general.log_format` and `general.log_level`. - add `log_format` with `text` and `json` modes - add `log_level` using `RUST_LOG` compatible filter syntax - initialize logging from config during startup - update `example.pgdog.toml` and generated JSON schema
1 parent b03b240 commit f228a79

10 files changed

Lines changed: 243 additions & 40 deletions

File tree

.schema/pgdog.schema.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
"load_schema": "auto",
6363
"log_connections": true,
6464
"log_disconnections": true,
65+
"log_format": "text",
66+
"log_level": "info",
6567
"lsn_check_delay": 9223372036854775807,
6668
"lsn_check_interval": 5000,
6769
"lsn_check_timeout": 5000,
@@ -769,6 +771,16 @@
769771
"type": "boolean",
770772
"default": true
771773
},
774+
"log_format": {
775+
"description": "Format to use for PgDog application logs.\n\n_Default:_ `text`\n\nhttps://docs.pgdog.dev/configuration/pgdog.toml/general/#log_format",
776+
"$ref": "#/$defs/LogFormat",
777+
"default": "text"
778+
},
779+
"log_level": {
780+
"description": "Log filter directives using the same syntax as the `RUST_LOG` environment variable.\n\n_Default:_ `info`\n\nhttps://docs.pgdog.dev/configuration/pgdog.toml/general/#log_level",
781+
"type": "string",
782+
"default": "info"
783+
},
772784
"lsn_check_delay": {
773785
"description": "For how long to delay checking for replication delay.\n\nhttps://docs.pgdog.dev/configuration/pgdog.toml/general/#lsn_check_delay",
774786
"type": "integer",
@@ -1090,6 +1102,21 @@
10901102
}
10911103
]
10921104
},
1105+
"LogFormat": {
1106+
"description": "Format to use for PgDog application logs.",
1107+
"oneOf": [
1108+
{
1109+
"description": "Human-readable text logs (default).",
1110+
"type": "string",
1111+
"const": "text"
1112+
},
1113+
{
1114+
"description": "Structured JSON logs suitable for ECS/Datadog ingestion.",
1115+
"type": "string",
1116+
"const": "json"
1117+
}
1118+
]
1119+
},
10931120
"ManualQuery": {
10941121
"description": "Queries with manual routing rules.",
10951122
"type": "object",

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example.pgdog.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ openmetrics_port = 9090
131131
#
132132
# Default: none
133133
openmetrics_namespace = "pgdog_"
134+
# Log output format.
135+
#
136+
# Default: text
137+
#
138+
# Available options:
139+
# - text
140+
# - json
141+
log_format = "text"
142+
# Log filter directives using the same syntax as RUST_LOG.
143+
#
144+
# Default: info
145+
log_level = "info"
134146
# Configure levels of support for prepared statements.
135147
#
136148
# Default: enabled

pgdog-config/src/general.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use schemars::JsonSchema;
22
use serde::{Deserialize, Serialize};
33
use std::env;
4+
use std::fmt;
45
use std::net::Ipv4Addr;
56
use std::path::PathBuf;
7+
use std::str::FromStr;
68
use std::time::Duration;
79

810
use crate::pooling::ConnectionRecovery;
@@ -16,6 +18,38 @@ use super::database::{LoadBalancingStrategy, ReadWriteSplit, ReadWriteStrategy};
1618
use super::networking::TlsVerifyMode;
1719
use super::pooling::{PoolerMode, PreparedStatements};
1820

21+
/// Format to use for PgDog application logs.
22+
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Default, JsonSchema)]
23+
#[serde(rename_all = "snake_case", deny_unknown_fields)]
24+
pub enum LogFormat {
25+
/// Human-readable text logs (default).
26+
#[default]
27+
Text,
28+
/// Structured JSON logs suitable for ECS/Datadog ingestion.
29+
Json,
30+
}
31+
32+
impl fmt::Display for LogFormat {
33+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34+
match self {
35+
Self::Text => f.write_str("text"),
36+
Self::Json => f.write_str("json"),
37+
}
38+
}
39+
}
40+
41+
impl FromStr for LogFormat {
42+
type Err = ();
43+
44+
fn from_str(s: &str) -> Result<Self, Self::Err> {
45+
match s.to_ascii_lowercase().as_str() {
46+
"text" => Ok(Self::Text),
47+
"json" => Ok(Self::Json),
48+
_ => Err(()),
49+
}
50+
}
51+
}
52+
1953
/// General settings are relevant to the operations of the pooler itself, or apply to all database pools.
2054
///
2155
/// https://docs.pgdog.dev/configuration/pgdog.toml/general/
@@ -407,6 +441,22 @@ pub struct General {
407441
#[serde(default)]
408442
pub pub_sub_channel_size: usize,
409443

444+
/// Format to use for PgDog application logs.
445+
///
446+
/// _Default:_ `text`
447+
///
448+
/// https://docs.pgdog.dev/configuration/pgdog.toml/general/#log_format
449+
#[serde(default = "General::log_format")]
450+
pub log_format: LogFormat,
451+
452+
/// Log filter directives using the same syntax as the `RUST_LOG` environment variable.
453+
///
454+
/// _Default:_ `info`
455+
///
456+
/// https://docs.pgdog.dev/configuration/pgdog.toml/general/#log_level
457+
#[serde(default = "General::log_level")]
458+
pub log_level: String,
459+
410460
/// If enabled, log every time a user creates a new connection to PgDog.
411461
///
412462
/// _Default:_ `true`
@@ -642,6 +692,8 @@ impl Default for General {
642692
cross_shard_disabled: Self::cross_shard_disabled(),
643693
dns_ttl: Self::default_dns_ttl(),
644694
pub_sub_channel_size: Self::pub_sub_channel_size(),
695+
log_format: Self::log_format(),
696+
log_level: Self::log_level(),
645697
log_connections: Self::log_connections(),
646698
log_disconnections: Self::log_disconnections(),
647699
two_phase_commit: bool::default(),
@@ -1003,6 +1055,14 @@ impl General {
10031055
Self::env_or_default("PGDOG_QUERY_CACHE_LIMIT", 50_000)
10041056
}
10051057

1058+
pub fn log_format() -> LogFormat {
1059+
Self::env_enum_or_default("PGDOG_LOG_FORMAT")
1060+
}
1061+
1062+
pub fn log_level() -> String {
1063+
env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string())
1064+
}
1065+
10061066
pub fn log_connections() -> bool {
10071067
Self::env_bool_or_default("PGDOG_LOG_CONNECTIONS", true)
10081068
}
@@ -1364,6 +1424,21 @@ mod tests {
13641424
assert_eq!(General::log_disconnections(), true);
13651425
}
13661426

1427+
#[test]
1428+
fn test_env_log_settings() {
1429+
env::set_var("PGDOG_LOG_FORMAT", "json");
1430+
env::set_var("RUST_LOG", "pgdog=debug,info");
1431+
1432+
assert_eq!(General::log_format(), LogFormat::Json);
1433+
assert_eq!(General::log_level(), "pgdog=debug,info");
1434+
1435+
env::remove_var("PGDOG_LOG_FORMAT");
1436+
env::remove_var("RUST_LOG");
1437+
1438+
assert_eq!(General::log_format(), LogFormat::Text);
1439+
assert_eq!(General::log_level(), "info");
1440+
}
1441+
13671442
#[test]
13681443
fn test_env_other_fields() {
13691444
env::set_var("PGDOG_BROADCAST_ADDRESS", "192.168.1.100");

pgdog-config/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub use database::{
2424
Database, EnumeratedDatabase, LoadBalancingStrategy, ReadWriteSplit, ReadWriteStrategy, Role,
2525
};
2626
pub use error::Error;
27-
pub use general::General;
27+
pub use general::{General, LogFormat};
2828
pub use memory::*;
2929
pub use networking::{MultiTenant, Tcp, TlsVerifyMode};
3030
pub use overrides::Overrides;

pgdog/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ tui = ["ratatui"]
2020
pin-project = "1"
2121
tokio = { version = "1", features = ["full"] }
2222
tracing = "0.1"
23-
tracing-subscriber = { version = "0.3", features = ["env-filter", "std"] }
23+
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "std"] }
2424
parking_lot = "0.12"
2525
thiserror = "2"
2626
bytes = "1"

pgdog/src/config/general.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pub use pgdog_config::general::General;
1+
pub use pgdog_config::general::{General, LogFormat};

pgdog/src/config/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub mod users;
1818
pub use core::{Config, ConfigAndUsers};
1919
pub use database::{Database, Role};
2020
pub use error::Error;
21-
pub use general::General;
21+
pub use general::{General, LogFormat};
2222
pub use memory::*;
2323
pub use networking::{MultiTenant, Tcp, TlsVerifyMode};
2424
pub use overrides::Overrides;

pgdog/src/lib.rs

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod tui;
1919
pub mod unique_id;
2020
pub mod util;
2121

22+
use pgdog_config::{General, LogFormat};
2223
use tracing::level_filters::LevelFilter;
2324
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
2425

@@ -41,19 +42,56 @@ static GLOBAL: &stats_alloc::StatsAlloc<System> = &stats_alloc::INSTRUMENTED_SYS
4142
/// Using try_init and ignoring errors to allow
4243
/// for use in tests (setting up multiple times).
4344
pub fn logger() {
44-
let format = fmt::layer()
45-
.with_ansi(std::io::stderr().is_terminal())
46-
.with_writer(std::io::stderr)
47-
.with_file(false);
48-
#[cfg(not(debug_assertions))]
49-
let format = format.with_target(false);
50-
51-
let filter = EnvFilter::builder()
52-
.with_default_directive(LevelFilter::INFO.into())
53-
.from_env_lossy();
54-
55-
let _ = tracing_subscriber::registry()
56-
.with(format)
57-
.with(filter)
58-
.try_init();
45+
init_logger(None);
46+
}
47+
48+
/// Setup the logger using PgDog configuration.
49+
pub fn logger_with_config(general: &General) {
50+
init_logger(Some(general));
51+
}
52+
53+
fn init_logger(general: Option<&General>) {
54+
let filter = match general {
55+
Some(general) => EnvFilter::builder()
56+
.with_default_directive(LevelFilter::INFO.into())
57+
.parse_lossy(general.log_level.as_str()),
58+
None => EnvFilter::builder()
59+
.with_default_directive(LevelFilter::INFO.into())
60+
.from_env_lossy(),
61+
};
62+
63+
match general
64+
.map(|general| general.log_format)
65+
.unwrap_or_default()
66+
{
67+
LogFormat::Text => {
68+
let format = fmt::layer()
69+
.with_ansi(std::io::stderr().is_terminal())
70+
.with_writer(std::io::stderr)
71+
.with_file(false);
72+
#[cfg(not(debug_assertions))]
73+
let format = format.with_target(false);
74+
75+
let _ = tracing_subscriber::registry()
76+
.with(format)
77+
.with(filter)
78+
.try_init();
79+
}
80+
LogFormat::Json => {
81+
let format = fmt::layer()
82+
.json()
83+
.with_ansi(false)
84+
.with_writer(std::io::stderr)
85+
.with_file(false)
86+
.with_current_span(false)
87+
.with_span_list(false);
88+
#[cfg(not(debug_assertions))]
89+
let format = format.with_target(false);
90+
91+
let _ = tracing_subscriber::registry()
92+
.with(format)
93+
.with(filter)
94+
.try_init();
95+
}
96+
}
5997
}

0 commit comments

Comments
 (0)