Skip to content

Commit c5665b6

Browse files
authored
Rotate pgAdmin/gunicorn logs according to retentionPeriod (#4101)
Add pgadmin/gunicorn log rotation configuration This PR continues the project of adding log retention configuration for users who have turned on the OTEL logging feature gate. This PR also makes some changes to our pgAdmin configuration. * Parse log retention in spec to use in pgadmin and gunicorn configuration * Restart gunicorn if logging changes to create new logger. (This is due to gunicorn behavior.) * Change the way we add a /tmp dir to pgAdmin to add to all containers. * Have the collector container create the receiver dir it needs. * Change the way we add config: add it to the configmap rather than the pod. Issues: [PGO-2168]
1 parent 37ea0c3 commit c5665b6

File tree

13 files changed

+303
-172
lines changed

13 files changed

+303
-172
lines changed

internal/collector/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func AddLogrotateConfigs(ctx context.Context, spec *v1beta1.InstrumentationSpec,
205205
func generateLogrotateConfig(
206206
config LogrotateConfig, retentionPeriod metav1.Duration,
207207
) string {
208-
number, interval := parseDurationForLogrotate(retentionPeriod)
208+
number, interval := ParseDurationForLogrotate(retentionPeriod)
209209

210210
return fmt.Sprintf(
211211
logrotateConfigFormatString,
@@ -216,12 +216,12 @@ func generateLogrotateConfig(
216216
)
217217
}
218218

219-
// parseDurationForLogrotate takes a retention period and returns the rotate
219+
// ParseDurationForLogrotate takes a retention period and returns the rotate
220220
// number and interval string that should be used in the logrotate config.
221221
// If the retentionPeriod is less than 24 hours, the function will return the
222222
// number of hours and "hourly"; otherwise, we will round up to the nearest day
223223
// and return the day count and "daily"
224-
func parseDurationForLogrotate(retentionPeriod metav1.Duration) (int, string) {
224+
func ParseDurationForLogrotate(retentionPeriod metav1.Duration) (int, string) {
225225
hours := math.Ceil(retentionPeriod.Hours())
226226
if hours < 24 {
227227
return int(hours), "hourly"

internal/collector/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ func TestParseDurationForLogrotate(t *testing.T) {
279279
t.Run(tt.retentionPeriod, func(t *testing.T) {
280280
duration, err := v1beta1.NewDuration(tt.retentionPeriod)
281281
assert.NilError(t, err)
282-
number, interval := parseDurationForLogrotate(duration.AsDuration())
282+
number, interval := ParseDurationForLogrotate(duration.AsDuration())
283283
assert.Equal(t, tt.number, number)
284284
assert.Equal(t, tt.interval, interval)
285285
})

internal/controller/postgrescluster/instance.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1242,7 +1242,7 @@ func (r *Reconciler) reconcileInstance(
12421242
// add an emptyDir volume to the PodTemplateSpec and an associated '/tmp' volume mount to
12431243
// all containers included within that spec
12441244
if err == nil {
1245-
addTMPEmptyDir(&instance.Spec.Template)
1245+
AddTMPEmptyDir(&instance.Spec.Template)
12461246
}
12471247

12481248
// mount shared memory to the Postgres instance

internal/controller/postgrescluster/pgadmin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ func (r *Reconciler) reconcilePGAdminStatefulSet(
365365

366366
// add an emptyDir volume to the PodTemplateSpec and an associated '/tmp'
367367
// volume mount to all containers included within that spec
368-
addTMPEmptyDir(&sts.Spec.Template)
368+
AddTMPEmptyDir(&sts.Spec.Template)
369369

370370
return errors.WithStack(r.apply(ctx, sts))
371371
}

internal/controller/postgrescluster/pgbackrest.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ func (r *Reconciler) generateRepoHostIntent(ctx context.Context, postgresCluster
720720
postgresCluster.Spec.ImagePullPolicy,
721721
&repo.Spec.Template)
722722

723-
addTMPEmptyDir(&repo.Spec.Template)
723+
AddTMPEmptyDir(&repo.Spec.Template)
724724

725725
// set ownership references
726726
if err := r.setControllerReference(postgresCluster, repo); err != nil {
@@ -1272,7 +1272,7 @@ func (r *Reconciler) reconcileRestoreJob(ctx context.Context,
12721272
cluster.Spec.ImagePullPolicy,
12731273
&restoreJob.Spec.Template)
12741274

1275-
addTMPEmptyDir(&restoreJob.Spec.Template)
1275+
AddTMPEmptyDir(&restoreJob.Spec.Template)
12761276

12771277
return errors.WithStack(r.apply(ctx, restoreJob))
12781278
}

internal/controller/postgrescluster/pgbouncer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ func (r *Reconciler) generatePGBouncerDeployment(
476476
}
477477

478478
// Add tmp directory and volume for log files
479-
addTMPEmptyDir(&deploy.Spec.Template)
479+
AddTMPEmptyDir(&deploy.Spec.Template)
480480

481481
return deploy, true, err
482482
}

internal/controller/postgrescluster/snapshots.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ func (r *Reconciler) dedicatedSnapshotVolumeRestore(ctx context.Context,
394394
cluster.Spec.ImagePullPolicy,
395395
&restoreJob.Spec.Template)
396396

397-
addTMPEmptyDir(&restoreJob.Spec.Template)
397+
AddTMPEmptyDir(&restoreJob.Spec.Template)
398398

399399
restoreJob.Annotations[naming.PGBackRestBackupJobCompletion] = backupJob.Status.CompletionTime.Format(time.RFC3339)
400400
return errors.WithStack(r.apply(ctx, restoreJob))

internal/controller/postgrescluster/util.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ func addDevSHM(template *corev1.PodTemplateSpec) {
134134
}
135135
}
136136

137-
// addTMPEmptyDir adds a "tmp" EmptyDir volume to the provided Pod template, while then also adding a
137+
// AddTMPEmptyDir adds a "tmp" EmptyDir volume to the provided Pod template, while then also adding a
138138
// volume mount at /tmp for all containers defined within the Pod template
139139
// The '/tmp' directory is currently utilized for the following:
140140
// - As the pgBackRest lock directory (this is the default lock location for pgBackRest)
141141
// - The location where the replication client certificates can be loaded with the proper
142142
// permissions set
143-
func addTMPEmptyDir(template *corev1.PodTemplateSpec) {
143+
func AddTMPEmptyDir(template *corev1.PodTemplateSpec) {
144144

145145
template.Spec.Volumes = append(template.Spec.Volumes, corev1.Volume{
146146
Name: "tmp",

internal/controller/standalone_pgadmin/configmap.go

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)