Skip to content

Commit 1797f8f

Browse files
dsessler7cbandy
authored andcommitted
Rotate PgBouncer logs using specified retention
Defaults to 1 day when no retention is set. Issue: PGO-2169
1 parent 8dbe427 commit 1797f8f

File tree

12 files changed

+303
-8
lines changed

12 files changed

+303
-8
lines changed

internal/collector/config.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,24 @@
55
package collector
66

77
import (
8+
"context"
9+
_ "embed"
10+
"fmt"
11+
"math"
12+
13+
corev1 "k8s.io/api/core/v1"
814
"k8s.io/apimachinery/pkg/util/sets"
915
"sigs.k8s.io/yaml"
1016

1117
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1218
)
1319

20+
// The contents of "logrotate.conf" as a string.
21+
// See: https://pkg.go.dev/embed
22+
//
23+
//go:embed "logrotate.conf"
24+
var logrotateConfigFormatString string
25+
1426
// ComponentID represents a component identifier within an OpenTelemetry
1527
// Collector YAML configuration. Each value is a "type" followed by an optional
1628
// slash-then-name: `type[/name]`
@@ -102,3 +114,61 @@ func NewConfig(spec *v1beta1.InstrumentationSpec) *Config {
102114

103115
return config
104116
}
117+
118+
// AddLogrotateConfig generates a logrotate configuration and adds it to the
119+
// provided configmap
120+
func AddLogrotateConfig(ctx context.Context, spec *v1beta1.InstrumentationSpec,
121+
outInstanceConfigMap *corev1.ConfigMap, logFilePath, postrotateScript string,
122+
) error {
123+
var err error
124+
var retentionPeriod *v1beta1.Duration
125+
126+
if outInstanceConfigMap.Data == nil {
127+
outInstanceConfigMap.Data = make(map[string]string)
128+
}
129+
130+
// If retentionPeriod is set in the spec, use that value; otherwise, we want
131+
// to use a reasonably short duration. Defaulting to 1 day.
132+
if spec != nil && spec.Logs != nil && spec.Logs.RetentionPeriod != nil {
133+
retentionPeriod = spec.Logs.RetentionPeriod
134+
} else {
135+
retentionPeriod, err = v1beta1.NewDuration("1d")
136+
if err != nil {
137+
return err
138+
}
139+
}
140+
141+
outInstanceConfigMap.Data["logrotate.conf"] = generateLogrotateConfig(logFilePath,
142+
retentionPeriod, postrotateScript)
143+
144+
return err
145+
}
146+
147+
// generateLogrotateConfig generates a configuration string for logrotate based
148+
// on the provided full log file path, retention period, and postrotate script
149+
func generateLogrotateConfig(logFilePath string, retentionPeriod *v1beta1.Duration,
150+
postrotateScript string,
151+
) string {
152+
number, interval := parseDurationForLogrotate(retentionPeriod)
153+
154+
return fmt.Sprintf(
155+
logrotateConfigFormatString,
156+
logFilePath,
157+
number,
158+
interval,
159+
postrotateScript,
160+
)
161+
}
162+
163+
// parseDurationForLogrotate takes a retention period and returns the rotate
164+
// number and interval string that should be used in the logrotate config.
165+
// If the retentionPeriod is less than 24 hours, the function will return the
166+
// number of hours and "hourly"; otherwise, we will round up to the nearest day
167+
// and return the day count and "daily"
168+
func parseDurationForLogrotate(retentionPeriod *v1beta1.Duration) (int, string) {
169+
hours := math.Ceil(retentionPeriod.AsDuration().Hours())
170+
if hours < 24 {
171+
return int(hours), "hourly"
172+
}
173+
return int(math.Ceil(hours / 24)), "daily"
174+
}

internal/collector/config_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"testing"
99

1010
"gotest.tools/v3/assert"
11+
12+
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1113
)
1214

1315
func TestConfigToYAML(t *testing.T) {
@@ -61,3 +63,138 @@ service:
6163
`)
6264
})
6365
}
66+
67+
func TestGenerateLogrotateConfig(t *testing.T) {
68+
for _, tt := range []struct {
69+
logFilePath string
70+
retentionPeriod string
71+
postrotateScript string
72+
result string
73+
}{
74+
{
75+
logFilePath: "/this/is/a/file.path",
76+
retentionPeriod: "12h",
77+
postrotateScript: "echo 'Hello, World'",
78+
result: `/this/is/a/file.path {
79+
rotate 12
80+
missingok
81+
sharedscripts
82+
notifempty
83+
nocompress
84+
hourly
85+
postrotate
86+
echo 'Hello, World'
87+
endscript
88+
}
89+
`,
90+
},
91+
{
92+
logFilePath: "/tmp/test.log",
93+
retentionPeriod: "5 days",
94+
postrotateScript: "",
95+
result: `/tmp/test.log {
96+
rotate 5
97+
missingok
98+
sharedscripts
99+
notifempty
100+
nocompress
101+
daily
102+
postrotate
103+
104+
endscript
105+
}
106+
`,
107+
},
108+
{
109+
logFilePath: "/tmp/test.log",
110+
retentionPeriod: "5wk",
111+
postrotateScript: "pkill -HUP --exact pgbouncer",
112+
result: `/tmp/test.log {
113+
rotate 35
114+
missingok
115+
sharedscripts
116+
notifempty
117+
nocompress
118+
daily
119+
postrotate
120+
pkill -HUP --exact pgbouncer
121+
endscript
122+
}
123+
`,
124+
},
125+
} {
126+
t.Run(tt.retentionPeriod, func(t *testing.T) {
127+
duration, err := v1beta1.NewDuration(tt.retentionPeriod)
128+
assert.NilError(t, err)
129+
result := generateLogrotateConfig(tt.logFilePath, duration, tt.postrotateScript)
130+
assert.Equal(t, tt.result, result)
131+
})
132+
}
133+
}
134+
135+
func TestParseDurationForLogrotate(t *testing.T) {
136+
for _, tt := range []struct {
137+
retentionPeriod string
138+
number int
139+
interval string
140+
}{
141+
{
142+
retentionPeriod: "1 h 20 min",
143+
number: 2,
144+
interval: "hourly",
145+
},
146+
{
147+
retentionPeriod: "12h",
148+
number: 12,
149+
interval: "hourly",
150+
},
151+
{
152+
retentionPeriod: "24hr",
153+
number: 1,
154+
interval: "daily",
155+
},
156+
{
157+
retentionPeriod: "35hour",
158+
number: 2,
159+
interval: "daily",
160+
},
161+
{
162+
retentionPeriod: "36 hours",
163+
number: 2,
164+
interval: "daily",
165+
},
166+
{
167+
retentionPeriod: "3d",
168+
number: 3,
169+
interval: "daily",
170+
},
171+
{
172+
retentionPeriod: "365day",
173+
number: 365,
174+
interval: "daily",
175+
},
176+
{
177+
retentionPeriod: "1w",
178+
number: 7,
179+
interval: "daily",
180+
},
181+
{
182+
retentionPeriod: "4wk",
183+
number: 28,
184+
interval: "daily",
185+
},
186+
{
187+
retentionPeriod: "52week",
188+
number: 364,
189+
interval: "daily",
190+
},
191+
} {
192+
t.Run(tt.retentionPeriod, func(t *testing.T) {
193+
duration, err := v1beta1.NewDuration(tt.retentionPeriod)
194+
assert.NilError(t, err)
195+
number, interval := parseDurationForLogrotate(duration)
196+
assert.Equal(t, tt.number, number)
197+
assert.Equal(t, tt.interval, interval)
198+
})
199+
}
200+
}

internal/collector/instance.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ func AddToPod(
4141
outPod *corev1.PodSpec,
4242
volumeMounts []corev1.VolumeMount,
4343
sqlQueryPassword string,
44+
includeLogrotate bool,
4445
) {
4546
if !(feature.Enabled(ctx, feature.OpenTelemetryLogs) || feature.Enabled(ctx, feature.OpenTelemetryMetrics)) {
4647
return
4748
}
4849

50+
// Create volume and volume mount for otel collector config
4951
configVolumeMount := corev1.VolumeMount{
5052
Name: "collector-config",
5153
MountPath: "/etc/otel-collector",
@@ -71,11 +73,15 @@ func AddToPod(
7173
configVolume.Projected.Sources = append(configVolume.Projected.Sources, spec.Config.Files...)
7274
}
7375

76+
// Add configVolume to the pod's volumes
77+
outPod.Volumes = append(outPod.Volumes, configVolume)
78+
79+
// Create collector container
7480
container := corev1.Container{
7581
Name: naming.ContainerCollector,
7682
Image: config.CollectorContainerImage(spec),
7783
ImagePullPolicy: pullPolicy,
78-
Command: []string{"/otelcol-contrib", "--config", "/etc/otel-collector/config.yaml"},
84+
Command: startCommand(includeLogrotate),
7985
Env: []corev1.EnvVar{
8086
{
8187
Name: "K8S_POD_NAMESPACE",
@@ -99,6 +105,32 @@ func AddToPod(
99105
VolumeMounts: append(volumeMounts, configVolumeMount),
100106
}
101107

108+
// If this is a pod that uses logrotate for log rotation, add config volume
109+
// and mount for logrotate config
110+
if includeLogrotate {
111+
logrotateConfigVolumeMount := corev1.VolumeMount{
112+
Name: "logrotate-config",
113+
MountPath: "/etc/logrotate.d",
114+
ReadOnly: true,
115+
}
116+
logrotateConfigVolume := corev1.Volume{Name: logrotateConfigVolumeMount.Name}
117+
logrotateConfigVolume.Projected = &corev1.ProjectedVolumeSource{
118+
Sources: []corev1.VolumeProjection{{
119+
ConfigMap: &corev1.ConfigMapProjection{
120+
LocalObjectReference: corev1.LocalObjectReference{
121+
Name: inInstanceConfigMap.Name,
122+
},
123+
Items: []corev1.KeyToPath{{
124+
Key: "logrotate.conf",
125+
Path: "logrotate.conf",
126+
}},
127+
},
128+
}},
129+
}
130+
container.VolumeMounts = append(container.VolumeMounts, logrotateConfigVolumeMount)
131+
outPod.Volumes = append(outPod.Volumes, logrotateConfigVolume)
132+
}
133+
102134
if feature.Enabled(ctx, feature.OpenTelemetryMetrics) {
103135
container.Ports = []corev1.ContainerPort{{
104136
ContainerPort: int32(8889),
@@ -108,5 +140,26 @@ func AddToPod(
108140
}
109141

110142
outPod.Containers = append(outPod.Containers, container)
111-
outPod.Volumes = append(outPod.Volumes, configVolume)
143+
}
144+
145+
// startCommand generates the command script used by the collector container
146+
func startCommand(includeLogrotate bool) []string {
147+
var startScript = `
148+
/otelcol-contrib --config /etc/otel-collector/config.yaml
149+
`
150+
151+
if includeLogrotate {
152+
startScript = `
153+
/otelcol-contrib --config /etc/otel-collector/config.yaml &
154+
155+
exec {fd}<> <(:||:)
156+
while read -r -t 5 -u "${fd}" ||:; do
157+
logrotate -s /tmp/logrotate.status /etc/logrotate.d/logrotate.conf
158+
done
159+
`
160+
}
161+
162+
wrapper := `monitor() {` + startScript + `}; export -f monitor; exec -a "$0" bash -ceu monitor`
163+
164+
return []string{"bash", "-ceu", "--", wrapper, "collector"}
112165
}

internal/collector/logrotate.conf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
%s {
2+
rotate %d
3+
missingok
4+
sharedscripts
5+
notifempty
6+
nocompress
7+
%s
8+
postrotate
9+
%s
10+
endscript
11+
}

internal/collector/pgbouncer.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import (
2222
//go:embed "generated/pgbouncer_metrics_queries.json"
2323
var pgBouncerMetricsQueries json.RawMessage
2424

25+
// PGBouncerPostRotateScript is the script that is run after pgBouncer's log
26+
// files have been rotated. The pgbouncer process is sent a sighup signal.
27+
const PGBouncerPostRotateScript = "pkill -HUP --exact pgbouncer"
28+
2529
// NewConfigForPgBouncerPod creates a config for the OTel collector container
2630
// that runs as a sidecar in the pgBouncer Pod
2731
func NewConfigForPgBouncerPod(
@@ -62,7 +66,11 @@ func EnablePgBouncerLogging(ctx context.Context,
6266
// https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/-/receiver/filelogreceiver#readme
6367
outConfig.Receivers["filelog/pgbouncer_log"] = map[string]any{
6468
// Read the log files and keep track of what has been processed.
65-
"include": []string{directory + "/*.log"},
69+
// We want to watch the ".log.1" file as well as it is possible that
70+
// a log entry or two will end up there after the original ".log"
71+
// file is renamed to ".log.1" during rotation. OTel will not create
72+
// duplicate log entries.
73+
"include": []string{directory + "/*.log", directory + "/*.log.1"},
6674
"storage": "file_storage/pgbouncer_logs",
6775
}
6876

internal/collector/pgbouncer_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ receivers:
7878
filelog/pgbouncer_log:
7979
include:
8080
- /tmp/*.log
81+
- /tmp/*.log.1
8182
storage: file_storage/pgbouncer_logs
8283
service:
8384
extensions:
@@ -166,6 +167,7 @@ receivers:
166167
filelog/pgbouncer_log:
167168
include:
168169
- /tmp/*.log
170+
- /tmp/*.log.1
169171
storage: file_storage/pgbouncer_logs
170172
service:
171173
extensions:

internal/controller/postgrescluster/instance.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1202,8 +1202,10 @@ func (r *Reconciler) reconcileInstance(
12021202

12031203
if err == nil &&
12041204
(feature.Enabled(ctx, feature.OpenTelemetryLogs) || feature.Enabled(ctx, feature.OpenTelemetryMetrics)) {
1205+
// TODO: Setting the includeLogrotate argument to false for now. This
1206+
// should be changed when we implement log rotation for postgres
12051207
collector.AddToPod(ctx, cluster.Spec.Instrumentation, cluster.Spec.ImagePullPolicy, instanceConfigMap, &instance.Spec.Template.Spec,
1206-
[]corev1.VolumeMount{postgres.DataVolumeMount()}, "")
1208+
[]corev1.VolumeMount{postgres.DataVolumeMount()}, "", false)
12071209
}
12081210

12091211
// Add pgMonitor resources to the instance Pod spec
@@ -1407,6 +1409,7 @@ func (r *Reconciler) reconcileInstanceConfigMap(
14071409
naming.LabelInstance: instance.Name,
14081410
})
14091411

1412+
// If OTel logging or metrics is enabled, add collector config
14101413
if err == nil && (feature.Enabled(ctx, feature.OpenTelemetryLogs) || feature.Enabled(ctx, feature.OpenTelemetryMetrics)) {
14111414
err = collector.AddToConfigMap(ctx, otelConfig, instanceConfigMap)
14121415
}

0 commit comments

Comments
 (0)