@@ -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,7 +64,38 @@ func configmap(pgadmin *v1beta1.PGAdmin,
6364
6465 // TODO(tjmoore4): Populate configuration details.
6566 initialize .Map (& configmap .Data )
66- configSettings , err := generateConfig (pgadmin )
67+ var (
68+ logRetention bool
69+ maxBackupRetentionNumber = 1
70+ // One day in minutes for pgadmin rotation
71+ pgAdminRetentionPeriod = 24 * 60
72+ // Daily rotation for gunicorn rotation
73+ gunicornRetentionPeriod = "D"
74+ )
75+ // If OTel logs feature gate is enabled, we want to change the pgAdmin/gunicorn logging
76+ if feature .Enabled (ctx , feature .OpenTelemetryLogs ) && pgadmin .Spec .Instrumentation != nil {
77+ logRetention = true
78+
79+ // If the user has set a retention period, we will use those values for log rotation,
80+ // which is otherwise managed by python.
81+ if pgadmin .Spec .Instrumentation .Logs != nil &&
82+ pgadmin .Spec .Instrumentation .Logs .RetentionPeriod != nil {
83+
84+ retentionNumber , period := collector .ParseDurationForLogrotate (pgadmin .Spec .Instrumentation .Logs .RetentionPeriod .AsDuration ())
85+ // `LOG_ROTATION_MAX_LOG_FILES`` in pgadmin refers to the already rotated logs.
86+ // `backupCount` for gunicorn is similar.
87+ // Our retention unit is for total number of log files, so subtract 1 to account
88+ // for the currently-used log file.
89+ maxBackupRetentionNumber = retentionNumber - 1
90+ if period == "hourly" {
91+ // If the period is hourly, set the pgadmin
92+ // and gunicorn retention periods to hourly.
93+ pgAdminRetentionPeriod = 60
94+ gunicornRetentionPeriod = "H"
95+ }
96+ }
97+ }
98+ configSettings , err := generateConfig (pgadmin , logRetention , maxBackupRetentionNumber , pgAdminRetentionPeriod )
6799 if err == nil {
68100 configmap .Data [settingsConfigMapKey ] = configSettings
69101 }
@@ -73,16 +105,19 @@ func configmap(pgadmin *v1beta1.PGAdmin,
73105 configmap .Data [settingsClusterMapKey ] = clusterSettings
74106 }
75107
76- gunicornSettings , err := generateGunicornConfig (pgadmin )
108+ gunicornSettings , err := generateGunicornConfig (pgadmin ,
109+ logRetention , maxBackupRetentionNumber , gunicornRetentionPeriod )
77110 if err == nil {
78111 configmap .Data [gunicornConfigKey ] = gunicornSettings
79112 }
80113
81114 return configmap , err
82115}
83116
84- // generateConfig generates the config settings for the pgAdmin
85- func generateConfig (pgadmin * v1beta1.PGAdmin ) (string , error ) {
117+ // generateConfigs generates the config settings for the pgAdmin and gunicorn
118+ func generateConfig (pgadmin * v1beta1.PGAdmin ,
119+ logRetention bool , maxBackupRetentionNumber , pgAdminRetentionPeriod int ) (
120+ string , error ) {
86121 settings := map [string ]any {
87122 // Bind to all IPv4 addresses by default. "0.0.0.0" here represents INADDR_ANY.
88123 // - https://flask.palletsprojects.com/en/2.2.x/api/#flask.Flask.run
@@ -102,6 +137,22 @@ func generateConfig(pgadmin *v1beta1.PGAdmin) (string, error) {
102137 settings ["UPGRADE_CHECK_ENABLED" ] = false
103138 settings ["UPGRADE_CHECK_URL" ] = ""
104139 settings ["UPGRADE_CHECK_KEY" ] = ""
140+ settings ["DATA_DIR" ] = dataMountPath
141+ settings ["LOG_FILE" ] = LogFileAbsolutePath
142+
143+ if logRetention {
144+ settings ["LOG_ROTATION_AGE" ] = pgAdminRetentionPeriod
145+ settings ["LOG_ROTATION_MAX_LOG_FILES" ] = maxBackupRetentionNumber
146+ settings ["JSON_LOGGER" ] = true
147+ settings ["CONSOLE_LOG_LEVEL" ] = "WARNING"
148+ settings ["FILE_LOG_LEVEL" ] = "INFO"
149+ settings ["FILE_LOG_FORMAT_JSON" ] = map [string ]string {
150+ "time" : "created" ,
151+ "name" : "name" ,
152+ "level" : "levelname" ,
153+ "message" : "message" ,
154+ }
155+ }
105156
106157 // To avoid spurious reconciles, the following value must not change when
107158 // the spec does not change. [json.Encoder] and [json.Marshal] do this by
@@ -185,7 +236,9 @@ func generateClusterConfig(
185236
186237// generateGunicornConfig generates the config settings for the gunicorn server
187238// - https://docs.gunicorn.org/en/latest/settings.html
188- func generateGunicornConfig (pgadmin * v1beta1.PGAdmin ) (string , error ) {
239+ func generateGunicornConfig (pgadmin * v1beta1.PGAdmin ,
240+ logRetention bool , maxBackupRetentionNumber int , gunicornRetentionPeriod string ,
241+ ) (string , error ) {
189242 settings := map [string ]any {
190243 // Bind to all IPv4 addresses and set 25 threads by default.
191244 // - https://docs.gunicorn.org/en/latest/settings.html#bind
@@ -202,6 +255,69 @@ func generateGunicornConfig(pgadmin *v1beta1.PGAdmin) (string, error) {
202255 // Write mandatory settings over any specified ones.
203256 // - https://docs.gunicorn.org/en/latest/settings.html#workers
204257 settings ["workers" ] = 1
258+ // Gunicorn logging dict settings
259+ logSettings := map [string ]any {}
260+
261+ // If OTel logs feature gate is enabled, we want to change the gunicorn logging
262+ if logRetention {
263+
264+ // Gunicorn uses the Python logging package, which sets the following attributes:
265+ // https://docs.python.org/3/library/logging.html#logrecord-attributes.
266+ // JsonFormatter is used to format the log: https://pypi.org/project/jsonformatter/
267+ // We override the gunicorn defaults (using `logconfig_dict`) to set our own file handler.
268+ // - https://docs.gunicorn.org/en/stable/settings.html#logconfig-dict
269+ // - https://github.com/benoitc/gunicorn/blob/23.0.0/gunicorn/glogging.py#L47
270+ logSettings = map [string ]any {
271+
272+ "loggers" : map [string ]any {
273+ "gunicorn.access" : map [string ]any {
274+ "handlers" : []string {"file" },
275+ "level" : "INFO" ,
276+ "propagate" : true ,
277+ "qualname" : "gunicorn.access" ,
278+ },
279+ "gunicorn.error" : map [string ]any {
280+ "handlers" : []string {"file" },
281+ "level" : "INFO" ,
282+ "propagate" : true ,
283+ "qualname" : "gunicorn.error" ,
284+ },
285+ },
286+ "handlers" : map [string ]any {
287+ "file" : map [string ]any {
288+ "class" : "logging.handlers.TimedRotatingFileHandler" ,
289+ "filename" : GunicornLogFileAbsolutePath ,
290+ "backupCount" : maxBackupRetentionNumber ,
291+ "interval" : 1 ,
292+ "when" : gunicornRetentionPeriod ,
293+ "formatter" : "json" ,
294+ },
295+ "console" : map [string ]any {
296+ "class" : "logging.StreamHandler" ,
297+ "formatter" : "generic" ,
298+ "stream" : "ext://sys.stdout" ,
299+ },
300+ },
301+ "formatters" : map [string ]any {
302+ "generic" : map [string ]any {
303+ "class" : "logging.Formatter" ,
304+ "datefmt" : "[%Y-%m-%d %H:%M:%S %z]" ,
305+ "format" : "%(asctime)s [%(process)d] [%(levelname)s] %(message)s" ,
306+ },
307+ "json" : map [string ]any {
308+ "class" : "jsonformatter.JsonFormatter" ,
309+ "separators" : []string {"," , ":" },
310+ "format" : map [string ]string {
311+ "time" : "created" ,
312+ "name" : "name" ,
313+ "level" : "levelname" ,
314+ "message" : "message" ,
315+ },
316+ },
317+ },
318+ }
319+ }
320+ settings ["logconfig_dict" ] = logSettings
205321
206322 // To avoid spurious reconciles, the following value must not change when
207323 // the spec does not change. [json.Encoder] and [json.Marshal] do this by
0 commit comments