Skip to content

Commit 5161445

Browse files
committed
WIP: rotate pgbouncer logs
1 parent 8dbe427 commit 5161445

File tree

6 files changed

+256
-2
lines changed

6 files changed

+256
-2
lines changed

internal/collector/config.go

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

77
import (
8+
_ "embed"
9+
"errors"
10+
"fmt"
11+
"regexp"
12+
813
"k8s.io/apimachinery/pkg/util/sets"
914
"sigs.k8s.io/yaml"
1015

1116
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1217
)
1318

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

103114
return config
104115
}
116+
117+
func generateLogrotateConfig(logFilePath string, retentionPeriod string, postrotateScript string) (string, error) {
118+
retentionPeriodMap, err := parseRetentionPeriodForLogrotate(retentionPeriod)
119+
if err != nil {
120+
return "", err
121+
}
122+
123+
return fmt.Sprintf(
124+
logrotateConfigFormatString,
125+
logFilePath,
126+
retentionPeriodMap["number"],
127+
retentionPeriodMap["interval"],
128+
postrotateScript,
129+
), err
130+
}
131+
132+
func parseRetentionPeriodForLogrotate(retentionPeriod string) (map[string]string, error) {
133+
// logrotate does not have an "hourly" interval, but if an interval is not
134+
// set it will rotate whenever logrotate is called, so set an empty string
135+
// in the config file for hourly
136+
unitIntervalMap := map[string]string{
137+
"h": "",
138+
"hr": "",
139+
"hour": "",
140+
"d": "daily",
141+
"day": "daily",
142+
"w": "weekly",
143+
"wk": "weekly",
144+
"week": "weekly",
145+
}
146+
147+
// Define duration regex and capture matches
148+
durationMatcher := regexp.MustCompile(`(?P<number>\d+)\s*(?P<interval>[A-Za-zµ]+)`)
149+
matches := durationMatcher.FindStringSubmatch(retentionPeriod)
150+
151+
// If three matches were not found (whole match and two submatch captures),
152+
// the retentionPeriod format must be invalid. Return an error.
153+
if len(matches) < 3 {
154+
return nil, errors.New("invalid retentionPeriod; must be number of hours, days, or weeks")
155+
}
156+
157+
// Create a map with "number" and "interval" keys
158+
retentionPeriodMap := make(map[string]string)
159+
for i, name := range durationMatcher.SubexpNames() {
160+
if i > 0 && i <= len(matches) {
161+
retentionPeriodMap[name] = matches[i]
162+
}
163+
}
164+
165+
// If the duration unit provided is found in the unitIntervalMap, set the
166+
// "interval" value to the "interval" string expected by logrotate; otherwise,
167+
// return an error
168+
if _, exists := unitIntervalMap[retentionPeriodMap["interval"]]; exists {
169+
retentionPeriodMap["interval"] = unitIntervalMap[retentionPeriodMap["interval"]]
170+
} else {
171+
return nil, fmt.Errorf("invalid retentionPeriod; %s is not a valid unit", retentionPeriodMap["interval"])
172+
}
173+
174+
return retentionPeriodMap, nil
175+
}

internal/collector/config_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,111 @@ service:
6161
`)
6262
})
6363
}
64+
65+
func TestParseRetentionPeriodForLogrotate(t *testing.T) {
66+
t.Run("SuccessfulParse", func(t *testing.T) {
67+
for _, tt := range []struct {
68+
retentionPeriod string
69+
retentionPeriodMap map[string]string
70+
}{
71+
{
72+
retentionPeriod: "12h",
73+
retentionPeriodMap: map[string]string{
74+
"number": "12",
75+
"interval": "",
76+
},
77+
},
78+
{
79+
retentionPeriod: "24hr",
80+
retentionPeriodMap: map[string]string{
81+
"number": "24",
82+
"interval": "",
83+
},
84+
},
85+
{
86+
retentionPeriod: "36hour",
87+
retentionPeriodMap: map[string]string{
88+
"number": "36",
89+
"interval": "",
90+
},
91+
},
92+
{
93+
retentionPeriod: "3d",
94+
retentionPeriodMap: map[string]string{
95+
"number": "3",
96+
"interval": "daily",
97+
},
98+
},
99+
{
100+
retentionPeriod: "365day",
101+
retentionPeriodMap: map[string]string{
102+
"number": "365",
103+
"interval": "daily",
104+
},
105+
},
106+
{
107+
retentionPeriod: "1w",
108+
retentionPeriodMap: map[string]string{
109+
"number": "1",
110+
"interval": "weekly",
111+
},
112+
},
113+
{
114+
retentionPeriod: "4wk",
115+
retentionPeriodMap: map[string]string{
116+
"number": "4",
117+
"interval": "weekly",
118+
},
119+
},
120+
{
121+
retentionPeriod: "52week",
122+
retentionPeriodMap: map[string]string{
123+
"number": "52",
124+
"interval": "weekly",
125+
},
126+
},
127+
} {
128+
t.Run(tt.retentionPeriod, func(t *testing.T) {
129+
rpm, err := parseRetentionPeriodForLogrotate(tt.retentionPeriod)
130+
assert.NilError(t, err)
131+
assert.Equal(t, tt.retentionPeriodMap["number"], rpm["number"])
132+
})
133+
}
134+
})
135+
136+
t.Run("UnsuccessfulParse", func(t *testing.T) {
137+
for _, tt := range []struct {
138+
retentionPeriod string
139+
errMessage string
140+
}{
141+
{
142+
retentionPeriod: "",
143+
errMessage: "invalid retentionPeriod; must be number of hours, days, or weeks",
144+
},
145+
{
146+
retentionPeriod: "asdf",
147+
errMessage: "invalid retentionPeriod; must be number of hours, days, or weeks",
148+
},
149+
{
150+
retentionPeriod: "1234",
151+
errMessage: "invalid retentionPeriod; must be number of hours, days, or weeks",
152+
},
153+
{
154+
retentionPeriod: "d2",
155+
errMessage: "invalid retentionPeriod; must be number of hours, days, or weeks",
156+
},
157+
{
158+
retentionPeriod: "1000z",
159+
errMessage: "invalid retentionPeriod; z is not a valid unit",
160+
},
161+
} {
162+
t.Run(tt.retentionPeriod, func(t *testing.T) {
163+
rpm, err := parseRetentionPeriodForLogrotate(tt.retentionPeriod)
164+
assert.Assert(t, rpm == nil)
165+
assert.Assert(t, err != nil)
166+
assert.ErrorContains(t, err, tt.errMessage)
167+
// assert.Equal(t, tt.retentionPeriodMap["number"], rpm["number"])
168+
})
169+
}
170+
})
171+
}

internal/collector/instance.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ func AddToPod(
7171
configVolume.Projected.Sources = append(configVolume.Projected.Sources, spec.Config.Files...)
7272
}
7373

74+
// TODO: wrap the following in `if` statement that checks for existence of
75+
// retentionPeriod in the API
76+
logrotateConfigVolumeMount := corev1.VolumeMount{
77+
Name: "logrotate-config",
78+
MountPath: "/etc/logrotate.d",
79+
ReadOnly: true,
80+
}
81+
logrotateConfigVolume := corev1.Volume{Name: logrotateConfigVolumeMount.Name}
82+
logrotateConfigVolume.Projected = &corev1.ProjectedVolumeSource{
83+
Sources: []corev1.VolumeProjection{{
84+
ConfigMap: &corev1.ConfigMapProjection{
85+
LocalObjectReference: corev1.LocalObjectReference{
86+
Name: inInstanceConfigMap.Name,
87+
},
88+
Items: []corev1.KeyToPath{{
89+
Key: "logrotate.conf",
90+
Path: "logrotate.conf",
91+
}},
92+
},
93+
}},
94+
}
95+
7496
container := corev1.Container{
7597
Name: naming.ContainerCollector,
7698
Image: config.CollectorContainerImage(spec),
@@ -96,7 +118,7 @@ func AddToPod(
96118
},
97119

98120
SecurityContext: initialize.RestrictedSecurityContext(),
99-
VolumeMounts: append(volumeMounts, configVolumeMount),
121+
VolumeMounts: append(volumeMounts, configVolumeMount, logrotateConfigVolumeMount),
100122
}
101123

102124
if feature.Enabled(ctx, feature.OpenTelemetryMetrics) {
@@ -108,5 +130,5 @@ func AddToPod(
108130
}
109131

110132
outPod.Containers = append(outPod.Containers, container)
111-
outPod.Volumes = append(outPod.Volumes, configVolume)
133+
outPod.Volumes = append(outPod.Volumes, configVolume, logrotateConfigVolume)
112134
}

internal/collector/logrotate.conf

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
%s {
2+
3+
rotate %s
4+
5+
missingok
6+
7+
sharedscripts
8+
9+
notifempty
10+
11+
nocompress
12+
13+
size 10m
14+
15+
%s
16+
17+
create 0640 postgres postgres
18+
19+
postrotate
20+
21+
%s
22+
23+
endscript
24+
25+
}

internal/collector/pgbouncer.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"fmt"
1212
"slices"
1313

14+
corev1 "k8s.io/api/core/v1"
15+
1416
"github.com/crunchydata/postgres-operator/internal/feature"
1517
"github.com/crunchydata/postgres-operator/internal/naming"
1618
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
@@ -182,3 +184,24 @@ func EnablePgBouncerMetrics(ctx context.Context, config *Config, sqlQueryUsernam
182184
}
183185
}
184186
}
187+
188+
func AddPgBouncerLogrotateConfig(ctx context.Context, outInstanceConfigMap *corev1.ConfigMap) error {
189+
var err error
190+
if outInstanceConfigMap.Data == nil {
191+
outInstanceConfigMap.Data = make(map[string]string)
192+
}
193+
194+
// FIXME: get retentionPeriod from instrumentationSpec
195+
pgbouncerLogPath := naming.PGBouncerLogPath + "/pgbouncer.log"
196+
retentionPeriod := "1d"
197+
postrotateScript := "/bin/kill -HUP `cat /var/pgbouncer-postgres/pgbouncer.pid 2> /dev/null` 2>/dev/null ||true"
198+
199+
logrotateConfig, err := generateLogrotateConfig(pgbouncerLogPath, retentionPeriod, postrotateScript)
200+
if err != nil {
201+
return err
202+
}
203+
204+
outInstanceConfigMap.Data["logrotate.conf"] = logrotateConfig
205+
206+
return err
207+
}

internal/controller/postgrescluster/pgbouncer.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ func (r *Reconciler) reconcilePGBouncerConfigMap(
103103
(feature.Enabled(ctx, feature.OpenTelemetryLogs) || feature.Enabled(ctx, feature.OpenTelemetryMetrics)) {
104104
err = collector.AddToConfigMap(ctx, otelConfig, configmap)
105105
}
106+
// If OTel logging is enabled and retentionPeriod is set, add logrotate config
107+
// FIXME: change `true` to checking for retentionPeriod existence
108+
if err == nil && otelConfig != nil && feature.Enabled(ctx, feature.OpenTelemetryLogs) && true {
109+
err = collector.AddPgBouncerLogrotateConfig(ctx, configmap)
110+
}
106111
if err == nil {
107112
err = errors.WithStack(r.apply(ctx, configmap))
108113
}

0 commit comments

Comments
 (0)