Skip to content

Commit 727d227

Browse files
committed
Rotate pgAdmin logs
pgAdmin has a built in logfile rotation system, as does gunicorn. We want to use those systems, but respect the retention setting that a user sets in the instrumentation section. This PR uses the RetentionPeriod to - set the number of backups for gunicorn and pgAdmin; - set the rotation cycle for pgAdmin. Issues: [PGO-2168]
1 parent 85636a8 commit 727d227

File tree

5 files changed

+75
-33
lines changed

5 files changed

+75
-33
lines changed

internal/collector/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func AddLogrotateConfig(ctx context.Context, spec *v1beta1.InstrumentationSpec,
149149
func generateLogrotateConfig(logFilePath string, retentionPeriod *v1beta1.Duration,
150150
postrotateScript string,
151151
) string {
152-
number, interval := parseDurationForLogrotate(retentionPeriod)
152+
number, interval := ParseDurationForLogrotate(retentionPeriod)
153153

154154
return fmt.Sprintf(
155155
logrotateConfigFormatString,
@@ -160,12 +160,12 @@ func generateLogrotateConfig(logFilePath string, retentionPeriod *v1beta1.Durati
160160
)
161161
}
162162

163-
// parseDurationForLogrotate takes a retention period and returns the rotate
163+
// ParseDurationForLogrotate takes a retention period and returns the rotate
164164
// number and interval string that should be used in the logrotate config.
165165
// If the retentionPeriod is less than 24 hours, the function will return the
166166
// number of hours and "hourly"; otherwise, we will round up to the nearest day
167167
// and return the day count and "daily"
168-
func parseDurationForLogrotate(retentionPeriod *v1beta1.Duration) (int, string) {
168+
func ParseDurationForLogrotate(retentionPeriod *v1beta1.Duration) (int, string) {
169169
hours := math.Ceil(retentionPeriod.AsDuration().Hours())
170170
if hours < 24 {
171171
return int(hours), "hourly"

internal/collector/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func TestParseDurationForLogrotate(t *testing.T) {
192192
t.Run(tt.retentionPeriod, func(t *testing.T) {
193193
duration, err := v1beta1.NewDuration(tt.retentionPeriod)
194194
assert.NilError(t, err)
195-
number, interval := parseDurationForLogrotate(duration)
195+
number, interval := ParseDurationForLogrotate(duration)
196196
assert.Equal(t, tt.number, number)
197197
assert.Equal(t, tt.interval, interval)
198198
})

internal/controller/standalone_pgadmin/pod.go

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import (
88
"context"
99
"fmt"
1010
"path"
11+
"strconv"
1112
"strings"
1213

1314
corev1 "k8s.io/api/core/v1"
1415
"k8s.io/apimachinery/pkg/api/resource"
1516
"k8s.io/apimachinery/pkg/util/intstr"
1617

18+
"github.com/crunchydata/postgres-operator/internal/collector"
1719
"github.com/crunchydata/postgres-operator/internal/config"
20+
"github.com/crunchydata/postgres-operator/internal/feature"
1821
"github.com/crunchydata/postgres-operator/internal/initialize"
1922
"github.com/crunchydata/postgres-operator/internal/kubernetes"
2023
"github.com/crunchydata/postgres-operator/internal/naming"
@@ -45,6 +48,7 @@ const (
4548

4649
// pod populates a PodSpec with the container and volumes needed to run pgAdmin.
4750
func pod(
51+
ctx context.Context,
4852
inPGAdmin *v1beta1.PGAdmin,
4953
inConfigMap *corev1.ConfigMap,
5054
outPod *corev1.PodSpec,
@@ -168,7 +172,7 @@ func pod(
168172

169173
startup := corev1.Container{
170174
Name: naming.ContainerPGAdminStartup,
171-
Command: startupCommand(),
175+
Command: startupCommand(ctx, inPGAdmin),
172176
Image: container.Image,
173177
ImagePullPolicy: container.ImagePullPolicy,
174178
Resources: container.Resources,
@@ -352,7 +356,7 @@ done
352356
}
353357

354358
// startupCommand returns an entrypoint that prepares the filesystem for pgAdmin.
355-
func startupCommand() []string {
359+
func startupCommand(ctx context.Context, inPgadmin *v1beta1.PGAdmin) []string {
356360
// pgAdmin reads from the `/etc/pgadmin/config_system.py` file during startup
357361
// after all other config files.
358362
// - https://github.com/pgadmin-org/pgadmin4/blob/REL-7_7/docs/en_US/config_py.rst
@@ -374,10 +378,33 @@ func startupCommand() []string {
374378

375379
// configDatabaseURIPath is the path for mounting the database URI connection string
376380
configDatabaseURIPathAbsolutePath = configMountPath + "/" + configDatabaseURIPath
381+
)
382+
var (
383+
maxBackupRetentionNumber = "1"
384+
retentionPeriod = "24 * 60"
385+
)
377386

378-
// The constants set in configSystem will not be overridden through
379-
// spec.config.settings.
380-
configSystem = `
387+
// If the OpenTelemetryLogs Feature is enabled and the user has set a retention period,
388+
// we will use those values for pgAdmin log rotation, which is otherwise managed by python.
389+
if feature.Enabled(ctx, feature.OpenTelemetryLogs) &&
390+
inPgadmin.Spec.Instrumentation != nil &&
391+
inPgadmin.Spec.Instrumentation.Logs != nil &&
392+
inPgadmin.Spec.Instrumentation.Logs.RetentionPeriod != nil {
393+
394+
retentionNumber, period := collector.ParseDurationForLogrotate(inPgadmin.Spec.Instrumentation.Logs.RetentionPeriod)
395+
// `LOG_ROTATION_MAX_LOG_FILES`` in pgadmin refers to the already rotated logs.
396+
// `backupCount` for gunicorn is similar.
397+
// Our retention unit is for total number of log files, so subtract 1 to account
398+
// for the currently-used log file.
399+
maxBackupRetentionNumber = strconv.Itoa(retentionNumber - 1)
400+
if period == "hourly" {
401+
retentionPeriod = "60"
402+
}
403+
}
404+
405+
// The constants set in configSystem will not be overridden through
406+
// spec.config.settings.
407+
var configSystem = `
381408
import glob, json, re, os, logging
382409
DEFAULT_BINARY_PATHS = {'pg': sorted([''] + glob.glob('/usr/pgsql-*/bin')).pop()}
383410
with open('` + configMountPath + `/` + configFilePath + `') as _f:
@@ -393,30 +420,30 @@ if os.path.isfile('` + configDatabaseURIPathAbsolutePath + `'):
393420
394421
DATA_DIR = '` + dataMountPath + `'
395422
LOG_FILE = '` + LogFileAbsolutePath + `'
396-
LOG_ROTATION_AGE = 24 * 60 # minutes
423+
LOG_ROTATION_AGE = ` + retentionPeriod + ` # minutes
397424
LOG_ROTATION_SIZE = 5 # MiB
398-
LOG_ROTATION_MAX_LOG_FILES = 1
425+
LOG_ROTATION_MAX_LOG_FILES = ` + maxBackupRetentionNumber + `
399426
400427
JSON_LOGGER = True
401428
CONSOLE_LOG_LEVEL = logging.WARNING
402429
FILE_LOG_LEVEL = logging.INFO
403430
FILE_LOG_FORMAT_JSON = {'time': 'created', 'name': 'name', 'level': 'levelname', 'message': 'message'}
404431
`
405-
// Gunicorn reads from the `/etc/pgadmin/gunicorn_config.py` file during startup
406-
// after all other config files.
407-
// - https://docs.gunicorn.org/en/latest/configure.html#configuration-file
408-
//
409-
// This command writes a script in `/etc/pgadmin/gunicorn_config.py` that reads
410-
// from the `gunicorn-config.json` file and sets those variables globally.
411-
// That way those values are available as settings when Gunicorn starts.
412-
//
413-
// Note: All Gunicorn settings are lowercase with underscores, so ignore
414-
// any keys/names that are not.
415-
//
416-
// Gunicorn uses the Python logging package, which sets the following attributes:
417-
// https://docs.python.org/3/library/logging.html#logrecord-attributes.
418-
// JsonFormatter is used to format the log: https://pypi.org/project/jsonformatter/
419-
gunicornConfig = `
432+
// Gunicorn reads from the `/etc/pgadmin/gunicorn_config.py` file during startup
433+
// after all other config files.
434+
// - https://docs.gunicorn.org/en/latest/configure.html#configuration-file
435+
//
436+
// This command writes a script in `/etc/pgadmin/gunicorn_config.py` that reads
437+
// from the `gunicorn-config.json` file and sets those variables globally.
438+
// That way those values are available as settings when Gunicorn starts.
439+
//
440+
// Note: All Gunicorn settings are lowercase with underscores, so ignore
441+
// any keys/names that are not.
442+
//
443+
// Gunicorn uses the Python logging package, which sets the following attributes:
444+
// https://docs.python.org/3/library/logging.html#logrecord-attributes.
445+
// JsonFormatter is used to format the log: https://pypi.org/project/jsonformatter/
446+
var gunicornConfig = `
420447
import json, re, collections, copy, gunicorn, gunicorn.glogging
421448
with open('` + configMountPath + `/` + gunicornConfigFilePath + `') as _f:
422449
_conf, _data = re.compile(r'[a-z_]+'), json.load(_f)
@@ -430,7 +457,7 @@ logconfig_dict['loggers']['gunicorn.error']['handlers'] = ['file']
430457
logconfig_dict['handlers']['file'] = {
431458
'class': 'logging.handlers.RotatingFileHandler',
432459
'filename': '` + GunicornLogFileAbsolutePath + `',
433-
'backupCount': 1, 'maxBytes': 2 << 20, # MiB
460+
'backupCount': ` + maxBackupRetentionNumber + `, 'maxBytes': 2 << 20, # MiB
434461
'formatter': 'json',
435462
}
436463
logconfig_dict['formatters']['json'] = {
@@ -444,7 +471,6 @@ logconfig_dict['formatters']['json'] = {
444471
])
445472
}
446473
`
447-
)
448474

449475
args := []string{strings.TrimLeft(configSystem, "\n"), strings.TrimLeft(gunicornConfig, "\n")}
450476

internal/controller/standalone_pgadmin/pod_test.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"k8s.io/apimachinery/pkg/api/resource"
1414
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1515

16+
"github.com/crunchydata/postgres-operator/internal/feature"
1617
"github.com/crunchydata/postgres-operator/internal/initialize"
1718
"github.com/crunchydata/postgres-operator/internal/kubernetes"
1819
"github.com/crunchydata/postgres-operator/internal/testing/cmp"
@@ -28,8 +29,9 @@ func TestPod(t *testing.T) {
2829
config := new(corev1.ConfigMap)
2930
testpod := new(corev1.PodSpec)
3031
pvc := new(corev1.PersistentVolumeClaim)
32+
ctx := context.Background()
3133

32-
call := func() { pod(pgadmin, config, testpod, pvc) }
34+
call := func() { pod(ctx, pgadmin, config, testpod, pvc) }
3335

3436
t.Run("Defaults", func(t *testing.T) {
3537

@@ -247,6 +249,20 @@ volumes:
247249
pgadmin.Spec.Resources.Requests = corev1.ResourceList{
248250
corev1.ResourceCPU: resource.MustParse("100m"),
249251
}
252+
retentionPeriod, err := v1beta1.NewDuration("12 hours")
253+
assert.NilError(t, err)
254+
pgadmin.Spec.Instrumentation = &v1beta1.InstrumentationSpec{
255+
Logs: &v1beta1.InstrumentationLogsSpec{
256+
RetentionPeriod: retentionPeriod,
257+
},
258+
}
259+
260+
// Turn on the Feature gate
261+
gate := feature.NewGate()
262+
assert.NilError(t, gate.SetFromMap(map[string]bool{
263+
feature.OpenTelemetryLogs: true,
264+
}))
265+
ctx = feature.NewContext(context.Background(), gate)
250266

251267
call()
252268

@@ -378,9 +394,9 @@ initContainers:
378394
379395
DATA_DIR = '/var/lib/pgadmin'
380396
LOG_FILE = '/var/lib/pgadmin/logs/pgadmin.log'
381-
LOG_ROTATION_AGE = 24 * 60 # minutes
397+
LOG_ROTATION_AGE = 60 # minutes
382398
LOG_ROTATION_SIZE = 5 # MiB
383-
LOG_ROTATION_MAX_LOG_FILES = 1
399+
LOG_ROTATION_MAX_LOG_FILES = 11
384400
385401
JSON_LOGGER = True
386402
CONSOLE_LOG_LEVEL = logging.WARNING
@@ -400,7 +416,7 @@ initContainers:
400416
logconfig_dict['handlers']['file'] = {
401417
'class': 'logging.handlers.RotatingFileHandler',
402418
'filename': '/var/lib/pgadmin/logs/gunicorn.log',
403-
'backupCount': 1, 'maxBytes': 2 << 20, # MiB
419+
'backupCount': 11, 'maxBytes': 2 << 20, # MiB
404420
'formatter': 'json',
405421
}
406422
logconfig_dict['formatters']['json'] = {

internal/controller/standalone_pgadmin/statefulset.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func statefulset(
119119

120120
sts.Spec.Template.Spec.SecurityContext = podSecurityContext(ctx)
121121

122-
pod(pgadmin, configmap, &sts.Spec.Template.Spec, dataVolume)
122+
pod(ctx, pgadmin, configmap, &sts.Spec.Template.Spec, dataVolume)
123123

124124
if feature.Enabled(ctx, feature.OpenTelemetryLogs) {
125125
// Logs for gunicorn and pgadmin write to /var/lib/pgadmin/logs

0 commit comments

Comments
 (0)