Skip to content

Commit b366983

Browse files
committed
Store pgAdmin log file positions in the logs directory
This prevents log records from being emitted multiple times.
1 parent 5af84ea commit b366983

File tree

5 files changed

+76
-125
lines changed

5 files changed

+76
-125
lines changed

internal/collector/pgadmin.go

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,20 @@ func EnablePgAdminLogging(ctx context.Context, configmap *corev1.ConfigMap) erro
1717
if !feature.Enabled(ctx, feature.OpenTelemetryLogs) {
1818
return nil
1919
}
20+
2021
otelConfig := NewConfig()
21-
otelConfig.Extensions["file_storage/pgadmin"] = map[string]any{
22-
"directory": "/var/log/pgadmin/receiver",
23-
"create_directory": true,
24-
"fsync": true,
25-
}
26-
otelConfig.Extensions["file_storage/gunicorn"] = map[string]any{
27-
"directory": "/var/log/gunicorn" + "/receiver",
28-
"create_directory": true,
22+
otelConfig.Extensions["file_storage/pgadmin_data_logs"] = map[string]any{
23+
"directory": "/var/lib/pgadmin/logs/receiver",
24+
"create_directory": false,
2925
"fsync": true,
3026
}
3127
otelConfig.Receivers["filelog/pgadmin"] = map[string]any{
3228
"include": []string{"/var/lib/pgadmin/logs/pgadmin.log"},
33-
"storage": "file_storage/pgadmin",
29+
"storage": "file_storage/pgadmin_data_logs",
3430
}
3531
otelConfig.Receivers["filelog/gunicorn"] = map[string]any{
3632
"include": []string{"/var/lib/pgadmin/logs/gunicorn.log"},
37-
"storage": "file_storage/gunicorn",
33+
"storage": "file_storage/pgadmin_data_logs",
3834
}
3935

4036
otelConfig.Processors["resource/pgadmin"] = map[string]any{
@@ -71,7 +67,7 @@ func EnablePgAdminLogging(ctx context.Context, configmap *corev1.ConfigMap) erro
7167
}
7268

7369
otelConfig.Pipelines["logs/pgadmin"] = Pipeline{
74-
Extensions: []ComponentID{"file_storage/pgadmin"},
70+
Extensions: []ComponentID{"file_storage/pgadmin_data_logs"},
7571
Receivers: []ComponentID{"filelog/pgadmin"},
7672
Processors: []ComponentID{
7773
"resource/pgadmin",
@@ -83,7 +79,7 @@ func EnablePgAdminLogging(ctx context.Context, configmap *corev1.ConfigMap) erro
8379
}
8480

8581
otelConfig.Pipelines["logs/gunicorn"] = Pipeline{
86-
Extensions: []ComponentID{"file_storage/gunicorn"},
82+
Extensions: []ComponentID{"file_storage/pgadmin_data_logs"},
8783
Receivers: []ComponentID{"filelog/gunicorn"},
8884
Processors: []ComponentID{
8985
"resource/pgadmin",
@@ -95,9 +91,8 @@ func EnablePgAdminLogging(ctx context.Context, configmap *corev1.ConfigMap) erro
9591
}
9692

9793
otelYAML, err := otelConfig.ToYAML()
98-
if err != nil {
99-
return err
94+
if err == nil {
95+
configmap.Data["collector.yaml"] = otelYAML
10096
}
101-
configmap.Data["collector.yaml"] = otelYAML
102-
return nil
97+
return err
10398
}

internal/collector/pgadmin_test.go

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5-
package collector
5+
package collector_test
66

77
import (
88
"context"
@@ -11,11 +11,11 @@ import (
1111
"gotest.tools/v3/assert"
1212
corev1 "k8s.io/api/core/v1"
1313

14+
"github.com/crunchydata/postgres-operator/internal/collector"
15+
pgadmin "github.com/crunchydata/postgres-operator/internal/controller/standalone_pgadmin"
1416
"github.com/crunchydata/postgres-operator/internal/feature"
1517
"github.com/crunchydata/postgres-operator/internal/initialize"
16-
"github.com/crunchydata/postgres-operator/internal/naming"
1718
"github.com/crunchydata/postgres-operator/internal/testing/cmp"
18-
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1919
)
2020

2121
func TestEnablePgAdminLogging(t *testing.T) {
@@ -27,10 +27,9 @@ func TestEnablePgAdminLogging(t *testing.T) {
2727

2828
ctx := feature.NewContext(context.Background(), gate)
2929

30-
pgadmin := new(v1beta1.PGAdmin)
31-
configmap := &corev1.ConfigMap{ObjectMeta: naming.StandalonePGAdmin(pgadmin)}
30+
configmap := new(corev1.ConfigMap)
3231
initialize.Map(&configmap.Data)
33-
err := EnablePgAdminLogging(ctx, configmap)
32+
err := collector.EnablePgAdminLogging(ctx, configmap)
3433
assert.NilError(t, err)
3534

3635
assert.Assert(t, cmp.MarshalMatches(configmap.Data, `
@@ -41,13 +40,9 @@ collector.yaml: |
4140
debug:
4241
verbosity: detailed
4342
extensions:
44-
file_storage/gunicorn:
45-
create_directory: true
46-
directory: /var/log/gunicorn/receiver
47-
fsync: true
48-
file_storage/pgadmin:
49-
create_directory: true
50-
directory: /var/log/pgadmin/receiver
43+
file_storage/pgadmin_data_logs:
44+
create_directory: false
45+
directory: `+pgadmin.LogDirectoryAbsolutePath+`/receiver
5146
fsync: true
5247
processors:
5348
batch/1s:
@@ -83,16 +78,15 @@ collector.yaml: |
8378
receivers:
8479
filelog/gunicorn:
8580
include:
86-
- /var/lib/pgadmin/logs/gunicorn.log
87-
storage: file_storage/gunicorn
81+
- `+pgadmin.GunicornLogFileAbsolutePath+`
82+
storage: file_storage/pgadmin_data_logs
8883
filelog/pgadmin:
8984
include:
90-
- /var/lib/pgadmin/logs/pgadmin.log
91-
storage: file_storage/pgadmin
85+
- `+pgadmin.LogFileAbsolutePath+`
86+
storage: file_storage/pgadmin_data_logs
9287
service:
9388
extensions:
94-
- file_storage/gunicorn
95-
- file_storage/pgadmin
89+
- file_storage/pgadmin_data_logs
9690
pipelines:
9791
logs/gunicorn:
9892
exporters:

internal/controller/standalone_pgadmin/pod.go

Lines changed: 42 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package standalone_pgadmin
77
import (
88
"context"
99
"fmt"
10+
"path"
1011
"strings"
1112

1213
corev1 "k8s.io/api/core/v1"
@@ -17,6 +18,7 @@ import (
1718
"github.com/crunchydata/postgres-operator/internal/initialize"
1819
"github.com/crunchydata/postgres-operator/internal/kubernetes"
1920
"github.com/crunchydata/postgres-operator/internal/naming"
21+
"github.com/crunchydata/postgres-operator/internal/shell"
2022
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
2123
)
2224

@@ -28,8 +30,17 @@ const (
2830
ldapFilePath = "~postgres-operator/ldap-bind-password"
2931
gunicornConfigFilePath = "~postgres-operator/" + gunicornConfigKey
3032

31-
// Nothing should be mounted to this location except the script our initContainer writes
33+
// scriptMountPath is where to mount a temporary directory that is only
34+
// writable during Pod initialization.
35+
//
36+
// NOTE: No ConfigMap nor Secret should ever be mounted here because they
37+
// could be used to inject code through "config_system.py".
3238
scriptMountPath = "/etc/pgadmin"
39+
40+
dataMountPath = "/var/lib/pgadmin"
41+
LogDirectoryAbsolutePath = dataMountPath + "/logs"
42+
GunicornLogFileAbsolutePath = LogDirectoryAbsolutePath + "/gunicorn.log"
43+
LogFileAbsolutePath = LogDirectoryAbsolutePath + "/pgadmin.log"
3344
)
3445

3546
// pod populates a PodSpec with the container and volumes needed to run pgAdmin.
@@ -39,54 +50,28 @@ func pod(
3950
outPod *corev1.PodSpec,
4051
pgAdminVolume *corev1.PersistentVolumeClaim,
4152
) {
42-
const (
43-
// config and data volume names
44-
configVolumeName = "pgadmin-config"
45-
dataVolumeName = "pgadmin-data"
46-
pgAdminLogVolumeName = "pgadmin-log"
47-
gunicornLogVolumeName = "gunicorn-log"
48-
scriptVolumeName = "pgadmin-config-system"
49-
tempVolumeName = "tmp"
50-
)
51-
5253
// create the projected volume of config maps for use in
5354
// 1. dynamic server discovery
5455
// 2. adding the config variables during pgAdmin startup
55-
configVolume := corev1.Volume{Name: configVolumeName}
56+
configVolume := corev1.Volume{Name: "pgadmin-config"}
5657
configVolume.VolumeSource = corev1.VolumeSource{
5758
Projected: &corev1.ProjectedVolumeSource{
5859
Sources: podConfigFiles(inConfigMap, *inPGAdmin),
5960
},
6061
}
6162

6263
// create the data volume for the persistent database
63-
dataVolume := corev1.Volume{Name: dataVolumeName}
64+
dataVolume := corev1.Volume{Name: "pgadmin-data"}
6465
dataVolume.VolumeSource = corev1.VolumeSource{
6566
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
6667
ClaimName: pgAdminVolume.Name,
6768
ReadOnly: false,
6869
},
6970
}
7071

71-
// create the temp volume for logs
72-
pgAdminLogVolume := corev1.Volume{Name: pgAdminLogVolumeName}
73-
pgAdminLogVolume.VolumeSource = corev1.VolumeSource{
74-
EmptyDir: &corev1.EmptyDirVolumeSource{
75-
Medium: corev1.StorageMediumMemory,
76-
},
77-
}
78-
79-
// create the temp volume for gunicorn logs
80-
gunicornLogVolume := corev1.Volume{Name: gunicornLogVolumeName}
81-
gunicornLogVolume.VolumeSource = corev1.VolumeSource{
82-
EmptyDir: &corev1.EmptyDirVolumeSource{
83-
Medium: corev1.StorageMediumMemory,
84-
},
85-
}
86-
8772
// Volume used to write a custom config_system.py file in the initContainer
8873
// which then loads the configs found in the `configVolume`
89-
scriptVolume := corev1.Volume{Name: scriptVolumeName}
74+
scriptVolume := corev1.Volume{Name: "pgadmin-config-system"}
9075
scriptVolume.VolumeSource = corev1.VolumeSource{
9176
EmptyDir: &corev1.EmptyDirVolumeSource{
9277
Medium: corev1.StorageMediumMemory,
@@ -101,7 +86,7 @@ func pod(
10186

10287
// create a temp volume for restart pid/other/debugging use
10388
// TODO: discuss tmp vol vs. persistent vol
104-
tmpVolume := corev1.Volume{Name: tempVolumeName}
89+
tmpVolume := corev1.Volume{Name: "tmp"}
10590
tmpVolume.VolumeSource = corev1.VolumeSource{
10691
EmptyDir: &corev1.EmptyDirVolumeSource{
10792
Medium: corev1.StorageMediumMemory,
@@ -142,29 +127,21 @@ func pod(
142127
},
143128
VolumeMounts: []corev1.VolumeMount{
144129
{
145-
Name: configVolumeName,
130+
Name: configVolume.Name,
146131
MountPath: configMountPath,
147132
ReadOnly: true,
148133
},
149134
{
150-
Name: dataVolumeName,
151-
MountPath: "/var/lib/pgadmin",
152-
},
153-
{
154-
Name: gunicornLogVolumeName,
155-
MountPath: "/var/log/gunicorn",
156-
},
157-
{
158-
Name: pgAdminLogVolumeName,
159-
MountPath: "/var/log/pgadmin",
135+
Name: dataVolume.Name,
136+
MountPath: dataMountPath,
160137
},
161138
{
162-
Name: scriptVolumeName,
139+
Name: scriptVolume.Name,
163140
MountPath: scriptMountPath,
164141
ReadOnly: true,
165142
},
166143
{
167-
Name: tempVolumeName,
144+
Name: tmpVolume.Name,
168145
MountPath: "/tmp",
169146
},
170147
},
@@ -199,19 +176,21 @@ func pod(
199176
VolumeMounts: []corev1.VolumeMount{
200177
// Volume to write a custom `config_system.py` file to.
201178
{
202-
Name: scriptVolumeName,
179+
Name: scriptVolume.Name,
203180
MountPath: scriptMountPath,
204181
ReadOnly: false,
205182
},
183+
{
184+
Name: dataVolume.Name,
185+
MountPath: dataMountPath,
186+
},
206187
},
207188
}
208189

209190
// add volumes and containers
210191
outPod.Volumes = []corev1.Volume{
211192
configVolume,
212193
dataVolume,
213-
pgAdminLogVolume,
214-
gunicornLogVolume,
215194
scriptVolume,
216195
tmpVolume,
217196
}
@@ -426,8 +405,8 @@ if os.path.isfile('` + configDatabaseURIPathAbsolutePath + `'):
426405
with open('` + configDatabaseURIPathAbsolutePath + `') as _f:
427406
CONFIG_DATABASE_URI = _f.read()
428407
429-
DATA_DIR = '/var/lib/pgadmin'
430-
LOG_FILE = '/var/lib/pgadmin/logs/pgadmin.log'
408+
DATA_DIR = '` + dataMountPath + `'
409+
LOG_FILE = '` + LogFileAbsolutePath + `'
431410
LOG_ROTATION_AGE = 24 * 60 # minutes
432411
LOG_ROTATION_SIZE = 5 # MiB
433412
LOG_ROTATION_MAX_LOG_FILES = 1
@@ -437,18 +416,18 @@ CONSOLE_LOG_LEVEL = logging.WARNING
437416
FILE_LOG_LEVEL = logging.INFO
438417
FILE_LOG_FORMAT_JSON = {'time': 'created', 'name': 'name', 'level': 'levelname', 'message': 'message'}
439418
`
440-
// gunicorn reads from the `/etc/pgadmin/gunicorn_config.py` file during startup
419+
// Gunicorn reads from the `/etc/pgadmin/gunicorn_config.py` file during startup
441420
// after all other config files.
442421
// - https://docs.gunicorn.org/en/latest/configure.html#configuration-file
443422
//
444423
// This command writes a script in `/etc/pgadmin/gunicorn_config.py` that reads
445424
// from the `gunicorn-config.json` file and sets those variables globally.
446-
// That way those values are available as settings when gunicorn starts.
425+
// That way those values are available as settings when Gunicorn starts.
447426
//
448-
// Note: All gunicorn settings are lowercase with underscores, so ignore
427+
// Note: All Gunicorn settings are lowercase with underscores, so ignore
449428
// any keys/names that are not.
450429
//
451-
// gunicorn uses the Python logging package, which sets the following attributes:
430+
// Gunicorn uses the Python logging package, which sets the following attributes:
452431
// https://docs.python.org/3/library/logging.html#logrecord-attributes.
453432
// JsonFormatter is used to format the log: https://pypi.org/project/jsonformatter/
454433
gunicornConfig = `
@@ -457,13 +436,14 @@ with open('` + configMountPath + `/` + gunicornConfigFilePath + `') as _f:
457436
_conf, _data = re.compile(r'[a-z_]+'), json.load(_f)
458437
if type(_data) is dict:
459438
globals().update({k: v for k, v in _data.items() if _conf.fullmatch(k)})
439+
460440
gunicorn.SERVER_SOFTWARE = 'Python'
461441
logconfig_dict = copy.deepcopy(gunicorn.glogging.CONFIG_DEFAULTS)
462442
logconfig_dict['loggers']['gunicorn.access']['handlers'] = ['file']
463443
logconfig_dict['loggers']['gunicorn.error']['handlers'] = ['file']
464444
logconfig_dict['handlers']['file'] = {
465445
'class': 'logging.handlers.RotatingFileHandler',
466-
'filename': '/var/lib/pgadmin/logs/gunicorn.log',
446+
'filename': '` + GunicornLogFileAbsolutePath + `',
467447
'backupCount': 1, 'maxBytes': 2 << 20, # MiB
468448
'formatter': 'json',
469449
}
@@ -483,9 +463,15 @@ logconfig_dict['formatters']['json'] = {
483463
args := []string{strings.TrimLeft(configSystem, "\n"), strings.TrimLeft(gunicornConfig, "\n")}
484464

485465
script := strings.Join([]string{
486-
// Use the initContainer to create this path to avoid the error noted here:
466+
// Create the config directory so Kubernetes can mount it later.
487467
// - https://issue.k8s.io/121294
488-
`mkdir -p ` + configMountPath,
468+
shell.MakeDirectories(0o775, scriptMountPath, configMountPath),
469+
470+
// Create the logs directory with g+rwx so the OTel Collector can
471+
// write to it as well.
472+
// TODO(log-rotation): Move the last segment into the Collector startup.
473+
shell.MakeDirectories(0o775, dataMountPath, path.Join(LogDirectoryAbsolutePath, "receiver")),
474+
489475
// Write the system and server configurations.
490476
`echo "$1" > ` + scriptMountPath + `/config_system.py`,
491477
`echo "$2" > ` + scriptMountPath + `/gunicorn_config.py`,

0 commit comments

Comments
 (0)