@@ -12,6 +12,7 @@ import (
1212 "slices"
1313
1414 "github.com/crunchydata/postgres-operator/internal/feature"
15+ "github.com/crunchydata/postgres-operator/internal/naming"
1516 "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1617)
1718
@@ -32,11 +33,120 @@ func NewConfigForPgBouncerPod(
3233
3334 config := NewConfig ()
3435
36+ EnablePgBouncerLogging (ctx , cluster , config )
3537 EnablePgBouncerMetrics (ctx , config , sqlQueryUsername )
3638
3739 return config
3840}
3941
42+ // EnablePgBouncerLogging adds necessary configuration to the collector config to collect
43+ // logs from pgBouncer when the OpenTelemetryLogging feature flag is enabled.
44+ func EnablePgBouncerLogging (ctx context.Context ,
45+ inCluster * v1beta1.PostgresCluster ,
46+ outConfig * Config ) {
47+ if feature .Enabled (ctx , feature .OpenTelemetryLogs ) {
48+ directory := naming .PGBouncerLogPath
49+
50+ // Keep track of what log records and files have been processed.
51+ // Use a subdirectory of the logs directory to stay within the same failure domain.
52+ //
53+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/-/extension/storage/filestorage#readme
54+ outConfig .Extensions ["file_storage/pgbouncer_logs" ] = map [string ]any {
55+ "directory" : directory + "/receiver" ,
56+ "create_directory" : true ,
57+ "fsync" : true ,
58+ }
59+
60+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/-/receiver/filelogreceiver#readme
61+ outConfig .Receivers ["filelog/pgbouncer_log" ] = map [string ]any {
62+ // Read the log files and keep track of what has been processed.
63+ "include" : []string {directory + "/*.log" },
64+ "storage" : "file_storage/pgbouncer_logs" ,
65+ }
66+
67+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/-/processor/resourceprocessor#readme
68+ outConfig .Processors ["resource/pgbouncer" ] = map [string ]any {
69+ "attributes" : []map [string ]any {
70+ // Container and Namespace names need no escaping because they are DNS labels.
71+ // Pod names need no escaping because they are DNS subdomains.
72+ //
73+ // https://kubernetes.io/docs/concepts/overview/working-with-objects/names
74+ // https://github.com/open-telemetry/semantic-conventions/blob/v1.29.0/docs/resource/k8s.md
75+ {"action" : "insert" , "key" : "k8s.container.name" , "value" : naming .ContainerPGBouncer },
76+ {"action" : "insert" , "key" : "k8s.namespace.name" , "value" : "${env:K8S_POD_NAMESPACE}" },
77+ {"action" : "insert" , "key" : "k8s.pod.name" , "value" : "${env:K8S_POD_NAME}" },
78+ },
79+ }
80+
81+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/-/processor/transformprocessor#readme
82+ outConfig .Processors ["transform/pgbouncer_logs" ] = map [string ]any {
83+ "log_statements" : []map [string ]any {{
84+ "context" : "log" ,
85+ "statements" : []string {
86+ // Set instrumentation scope
87+ `set(instrumentation_scope.name, "pgbouncer")` ,
88+
89+ // Extract timestamp, pid, log level, and message and store in cache.
90+ `merge_maps(cache, ExtractPatterns(body, ` +
91+ `"^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3} [A-Z]{3}) ` +
92+ `\\[(?<pid>\\d+)\\] (?<log_level>[A-Z]+) (?<msg>.*$)"), "insert")` ,
93+
94+ // https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitytext
95+ `set(severity_text, cache["log_level"])` ,
96+
97+ // Map pgBouncer (libusual) "logging levels" to OpenTelemetry severity levels.
98+ //
99+ // https://github.com/libusual/libusual/blob/master/usual/logging.c
100+ // https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
101+ // https://opentelemetry.io/docs/specs/otel/logs/data-model-appendix/#appendix-b-severitynumber-example-mappings
102+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/-/pkg/ottl/contexts/ottllog#enums
103+ `set(severity_number, SEVERITY_NUMBER_DEBUG) where severity_text == "NOISE" or severity_text == "DEBUG"` ,
104+ `set(severity_number, SEVERITY_NUMBER_INFO) where severity_text == "LOG"` ,
105+ `set(severity_number, SEVERITY_NUMBER_WARN) where severity_text == "WARNING"` ,
106+ `set(severity_number, SEVERITY_NUMBER_ERROR) where severity_text == "ERROR"` ,
107+ `set(severity_number, SEVERITY_NUMBER_FATAL) where severity_text == "FATAL"` ,
108+
109+ // Parse the timestamp.
110+ // The format is neither RFC 3339 nor ISO 8601:
111+ //
112+ // The date and time are separated by a single space U+0020,
113+ // followed by a dot U+002E, milliseconds, another space U+0020,
114+ // then a timezone abbreviation.
115+ //
116+ // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/-/pkg/stanza/docs/types/timestamp.md
117+ `set(time, Time(cache["timestamp"], "%F %T.%L %Z"))` ,
118+
119+ // Keep the unparsed log record in a standard attribute, and replace
120+ // the log record body with the message field.
121+ //
122+ // https://github.com/open-telemetry/semantic-conventions/blob/v1.29.0/docs/general/logs.md
123+ `set(attributes["log.record.original"], body)` ,
124+
125+ // Set pid as attribute
126+ `set(attributes["process.pid"], cache["pid"])` ,
127+
128+ // Set the log message to body.
129+ `set(body, cache["msg"])` ,
130+ },
131+ }},
132+ }
133+
134+ outConfig .Pipelines ["logs/pgbouncer" ] = Pipeline {
135+ Extensions : []ComponentID {"file_storage/pgbouncer_logs" },
136+ Receivers : []ComponentID {"filelog/pgbouncer_log" },
137+ Processors : []ComponentID {
138+ "resource/pgbouncer" ,
139+ "transform/pgbouncer_logs" ,
140+ SubSecondBatchProcessor ,
141+ CompactingProcessor ,
142+ },
143+ Exporters : []ComponentID {DebugExporter },
144+ }
145+ }
146+ }
147+
148+ // EnablePgBouncerMetrics adds necessary configuration to the collector config to scrape
149+ // metrics from pgBouncer when the OpenTelemetryMetrics feature flag is enabled.
40150func EnablePgBouncerMetrics (ctx context.Context , config * Config , sqlQueryUsername string ) {
41151 if feature .Enabled (ctx , feature .OpenTelemetryMetrics ) {
42152 // Add Prometheus exporter
0 commit comments