@@ -19,6 +19,7 @@ import (
1919 "github.com/pkg/errors"
2020
2121 "github.com/crunchydata/postgres-operator/internal/collector"
22+ "github.com/crunchydata/postgres-operator/internal/feature"
2223 "github.com/crunchydata/postgres-operator/internal/initialize"
2324 "github.com/crunchydata/postgres-operator/internal/naming"
2425 "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
@@ -32,7 +33,7 @@ func (r *PGAdminReconciler) reconcilePGAdminConfigMap(
3233 ctx context.Context , pgadmin * v1beta1.PGAdmin ,
3334 clusters map [string ][]* v1beta1.PostgresCluster ,
3435) (* corev1.ConfigMap , error ) {
35- configmap , err := configmap (pgadmin , clusters )
36+ configmap , err := configmap (ctx , pgadmin , clusters )
3637 if err != nil {
3738 return configmap , err
3839 }
@@ -50,7 +51,7 @@ func (r *PGAdminReconciler) reconcilePGAdminConfigMap(
5051}
5152
5253// configmap returns a v1.ConfigMap for pgAdmin.
53- func configmap (pgadmin * v1beta1.PGAdmin ,
54+ func configmap (ctx context. Context , pgadmin * v1beta1.PGAdmin ,
5455 clusters map [string ][]* v1beta1.PostgresCluster ,
5556) (* corev1.ConfigMap , error ) {
5657 configmap := & corev1.ConfigMap {ObjectMeta : naming .StandalonePGAdmin (pgadmin )}
@@ -63,26 +64,29 @@ func configmap(pgadmin *v1beta1.PGAdmin,
6364
6465 // TODO(tjmoore4): Populate configuration details.
6566 initialize .Map (& configmap .Data )
66- configSettings , err := generateConfig (pgadmin )
67+ pgadminConfigSettings , err := generateConfig (ctx , pgadmin )
6768 if err == nil {
68- configmap .Data [settingsConfigMapKey ] = configSettings
69+ configmap .Data [settingsConfigMapKey ] = pgadminConfigSettings
6970 }
7071
7172 clusterSettings , err := generateClusterConfig (clusters )
7273 if err == nil {
7374 configmap .Data [settingsClusterMapKey ] = clusterSettings
7475 }
7576
76- gunicornSettings , err := generateGunicornConfig (pgadmin )
77+ gunicornSettings , gunicornLoggingSettings , err := generateGunicornConfig (ctx , pgadmin )
7778 if err == nil {
7879 configmap .Data [gunicornConfigKey ] = gunicornSettings
80+ configmap .Data [gunicornLoggingConfigKey ] = gunicornLoggingSettings
7981 }
8082
8183 return configmap , err
8284}
8385
84- // generateConfig generates the config settings for the pgAdmin
85- func generateConfig (pgadmin * v1beta1.PGAdmin ) (string , error ) {
86+ // generateConfigs generates the config settings for the pgAdmin and gunicorn
87+ func generateConfig (ctx context.Context , pgadmin * v1beta1.PGAdmin ) (
88+ string , error ,
89+ ) {
8690 settings := map [string ]any {
8791 // Bind to all IPv4 addresses by default. "0.0.0.0" here represents INADDR_ANY.
8892 // - https://flask.palletsprojects.com/en/2.2.x/api/#flask.Flask.run
@@ -102,6 +106,48 @@ func generateConfig(pgadmin *v1beta1.PGAdmin) (string, error) {
102106 settings ["UPGRADE_CHECK_ENABLED" ] = false
103107 settings ["UPGRADE_CHECK_URL" ] = ""
104108 settings ["UPGRADE_CHECK_KEY" ] = ""
109+ settings ["DATA_DIR" ] = dataMountPath
110+ settings ["LOG_FILE" ] = LogFileAbsolutePath
111+
112+ // If OTel logs feature gate is enabled, we want to change the pgAdmin/gunicorn logging
113+ if feature .Enabled (ctx , feature .OpenTelemetryLogs ) && pgadmin .Spec .Instrumentation != nil {
114+
115+ var (
116+ maxBackupRetentionNumber = 1
117+ // One day in minutes for pgadmin rotation
118+ pgAdminRetentionPeriod = 24 * 60
119+ )
120+
121+ // If the user has set a retention period, we will use those values for log rotation,
122+ // which is otherwise managed by python.
123+ if pgadmin .Spec .Instrumentation .Logs != nil &&
124+ pgadmin .Spec .Instrumentation .Logs .RetentionPeriod != nil {
125+
126+ retentionNumber , period := collector .ParseDurationForLogrotate (pgadmin .Spec .Instrumentation .Logs .RetentionPeriod .AsDuration ())
127+ // `LOG_ROTATION_MAX_LOG_FILES`` in pgadmin refers to the already rotated logs.
128+ // `backupCount` for gunicorn is similar.
129+ // Our retention unit is for total number of log files, so subtract 1 to account
130+ // for the currently-used log file.
131+ maxBackupRetentionNumber = retentionNumber - 1
132+ if period == "hourly" {
133+ // If the period is hourly, set the pgadmin
134+ // and gunicorn retention periods to hourly.
135+ pgAdminRetentionPeriod = 60
136+ }
137+ }
138+
139+ settings ["LOG_ROTATION_AGE" ] = pgAdminRetentionPeriod
140+ settings ["LOG_ROTATION_MAX_LOG_FILES" ] = maxBackupRetentionNumber
141+ settings ["JSON_LOGGER" ] = true
142+ settings ["CONSOLE_LOG_LEVEL" ] = "WARNING"
143+ settings ["FILE_LOG_LEVEL" ] = "INFO"
144+ settings ["FILE_LOG_FORMAT_JSON" ] = map [string ]string {
145+ "time" : "created" ,
146+ "name" : "name" ,
147+ "level" : "levelname" ,
148+ "message" : "message" ,
149+ }
150+ }
105151
106152 // To avoid spurious reconciles, the following value must not change when
107153 // the spec does not change. [json.Encoder] and [json.Marshal] do this by
@@ -185,7 +231,9 @@ func generateClusterConfig(
185231
186232// generateGunicornConfig generates the config settings for the gunicorn server
187233// - https://docs.gunicorn.org/en/latest/settings.html
188- func generateGunicornConfig (pgadmin * v1beta1.PGAdmin ) (string , error ) {
234+ func generateGunicornConfig (ctx context.Context , pgadmin * v1beta1.PGAdmin ) (
235+ string , string , error ,
236+ ) {
189237 settings := map [string ]any {
190238 // Bind to all IPv4 addresses and set 25 threads by default.
191239 // - https://docs.gunicorn.org/en/latest/settings.html#bind
@@ -213,5 +261,105 @@ func generateGunicornConfig(pgadmin *v1beta1.PGAdmin) (string, error) {
213261 encoder .SetIndent ("" , " " )
214262 err := encoder .Encode (settings )
215263
216- return buffer .String (), err
264+ // Gunicorn logging dict settings
265+ logSettings := map [string ]any {}
266+
267+ // If OTel logs feature gate is enabled, we want to change the pgAdmin/gunicorn logging
268+ if feature .Enabled (ctx , feature .OpenTelemetryLogs ) &&
269+ pgadmin .Spec .Instrumentation != nil {
270+
271+ var (
272+ maxBackupRetentionNumber = "1"
273+ // Daily rotation for gunicorn rotation
274+ gunicornRetentionPeriod = "D"
275+ )
276+
277+ // If the user has set a retention period, we will use those values for log rotation,
278+ // which is otherwise managed by python.
279+ if pgadmin .Spec .Instrumentation .Logs != nil &&
280+ pgadmin .Spec .Instrumentation .Logs .RetentionPeriod != nil {
281+
282+ retentionNumber , period := collector .ParseDurationForLogrotate (pgadmin .Spec .Instrumentation .Logs .RetentionPeriod .AsDuration ())
283+ // `LOG_ROTATION_MAX_LOG_FILES`` in pgadmin refers to the already rotated logs.
284+ // `backupCount` for gunicorn is similar.
285+ // Our retention unit is for total number of log files, so subtract 1 to account
286+ // for the currently-used log file.
287+ maxBackupRetentionNumber = strconv .Itoa (retentionNumber - 1 )
288+ if period == "hourly" {
289+ // If the period is hourly, set the pgadmin
290+ // and gunicorn retention periods to hourly.
291+ gunicornRetentionPeriod = "H"
292+ }
293+ }
294+
295+ // Gunicorn uses the Python logging package, which sets the following attributes:
296+ // https://docs.python.org/3/library/logging.html#logrecord-attributes.
297+ // JsonFormatter is used to format the log: https://pypi.org/project/jsonformatter/
298+ // We override the gunicorn defaults (using `logconfig_dict`) to set our own file handler.
299+ // - https://docs.gunicorn.org/en/stable/settings.html#logconfig-dict
300+ // - https://github.com/benoitc/gunicorn/blob/23.0.0/gunicorn/glogging.py#L47
301+ logSettings = map [string ]any {
302+
303+ "loggers" : map [string ]any {
304+ "gunicorn.access" : map [string ]any {
305+ "handlers" : []string {"file" },
306+ "level" : "INFO" ,
307+ "propagate" : true ,
308+ "qualname" : "gunicorn.access" ,
309+ },
310+ "gunicorn.error" : map [string ]any {
311+ "handlers" : []string {"file" },
312+ "level" : "INFO" ,
313+ "propagate" : true ,
314+ "qualname" : "gunicorn.error" ,
315+ },
316+ },
317+ "handlers" : map [string ]any {
318+ "file" : map [string ]any {
319+ "class" : "logging.handlers.TimedRotatingFileHandler" ,
320+ "filename" : GunicornLogFileAbsolutePath ,
321+ "backupCount" : maxBackupRetentionNumber ,
322+ "interval" : 1 ,
323+ "when" : gunicornRetentionPeriod ,
324+ "formatter" : "json" ,
325+ },
326+ "console" : map [string ]any {
327+ "class" : "logging.StreamHandler" ,
328+ "formatter" : "generic" ,
329+ "stream" : "ext://sys.stdout" ,
330+ },
331+ },
332+ "formatters" : map [string ]any {
333+ "generic" : map [string ]any {
334+ "class" : "logging.Formatter" ,
335+ "datefmt" : "[%Y-%m-%d %H:%M:%S %z]" ,
336+ "format" : "%(asctime)s [%(process)d] [%(levelname)s] %(message)s" ,
337+ },
338+ "json" : map [string ]any {
339+ "class" : "jsonformatter.JsonFormatter" ,
340+ "separators" : []string {"," , ":" },
341+ "format" : map [string ]string {
342+ "time" : "created" ,
343+ "name" : "name" ,
344+ "level" : "levelname" ,
345+ "message" : "message" ,
346+ },
347+ },
348+ },
349+ }
350+ }
351+
352+ // To avoid spurious reconciles, the following value must not change when
353+ // the spec does not change. [json.Encoder] and [json.Marshal] do this by
354+ // emitting map keys in sorted order. Indent so the value is not rendered
355+ // as one long line by `kubectl`.
356+ logBuffer := new (bytes.Buffer )
357+ logEncoder := json .NewEncoder (logBuffer )
358+ logEncoder .SetEscapeHTML (false )
359+ logEncoder .SetIndent ("" , " " )
360+
361+ // Combine errors
362+ err = logEncoder .Encode (logSettings )
363+
364+ return buffer .String (), logBuffer .String (), err
217365}
0 commit comments