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