Skip to content

Commit 96e1ffb

Browse files
tony-landrethcbandy
authored andcommitted
Scrape pgAdmin logs using the OTel collector
Collect JSON-formatted logs from pgAdmin when the feature gate is enabled. Issue: PGO-2057
1 parent 2e59c1b commit 96e1ffb

File tree

9 files changed

+400
-22
lines changed

9 files changed

+400
-22
lines changed

internal/collector/instance.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/crunchydata/postgres-operator/internal/feature"
1313
"github.com/crunchydata/postgres-operator/internal/initialize"
1414
"github.com/crunchydata/postgres-operator/internal/naming"
15-
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1615
)
1716

1817
// AddToConfigMap populates the shared ConfigMap with fields needed to run the Collector.
@@ -34,7 +33,7 @@ func AddToConfigMap(
3433
// AddToPod adds the OpenTelemetry collector container to a given Pod
3534
func AddToPod(
3635
ctx context.Context,
37-
inCluster *v1beta1.PostgresCluster,
36+
pullPolicy corev1.PullPolicy,
3837
inInstanceConfigMap *corev1.ConfigMap,
3938
outPod *corev1.PodSpec,
4039
volumeMounts []corev1.VolumeMount,
@@ -65,10 +64,9 @@ func AddToPod(
6564
}
6665

6766
container := corev1.Container{
68-
Name: naming.ContainerCollector,
69-
67+
Name: naming.ContainerCollector,
7068
Image: "ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.117.0",
71-
ImagePullPolicy: inCluster.Spec.ImagePullPolicy,
69+
ImagePullPolicy: pullPolicy,
7270
Command: []string{"/otelcol-contrib", "--config", "/etc/otel-collector/config.yaml"},
7371
Env: []corev1.EnvVar{
7472
{

internal/collector/pgadmin.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2024 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package collector
6+
7+
import (
8+
"context"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
12+
"github.com/crunchydata/postgres-operator/internal/feature"
13+
"github.com/crunchydata/postgres-operator/internal/naming"
14+
)
15+
16+
func EnablePgAdminLogging(ctx context.Context, configmap *corev1.ConfigMap) error {
17+
if !feature.Enabled(ctx, feature.OpenTelemetryLogs) {
18+
return nil
19+
}
20+
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,
29+
"fsync": true,
30+
}
31+
otelConfig.Receivers["filelog/pgadmin"] = map[string]any{
32+
"include": []string{"/var/lib/pgadmin/logs/pgadmin.log"},
33+
"storage": "file_storage/pgadmin",
34+
}
35+
otelConfig.Receivers["filelog/gunicorn"] = map[string]any{
36+
"include": []string{"/var/lib/pgadmin/logs/gunicorn.log"},
37+
"storage": "file_storage/gunicorn",
38+
}
39+
40+
otelConfig.Processors["resource/pgadmin"] = map[string]any{
41+
"attributes": []map[string]any{
42+
// Container and Namespace names need no escaping because they are DNS labels.
43+
// Pod names need no escaping because they are DNS subdomains.
44+
//
45+
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names
46+
// https://github.com/open-telemetry/semantic-conventions/blob/v1.29.0/docs/resource/k8s.md
47+
// https://github.com/open-telemetry/semantic-conventions/blob/v1.29.0/docs/general/logs.md
48+
{"action": "insert", "key": "k8s.container.name", "value": naming.ContainerPGAdmin},
49+
{"action": "insert", "key": "k8s.namespace.name", "value": "${env:K8S_POD_NAMESPACE}"},
50+
{"action": "insert", "key": "k8s.pod.name", "value": "${env:K8S_POD_NAME}"},
51+
},
52+
}
53+
54+
otelConfig.Processors["transform/pgadmin_log"] = map[string]any{
55+
"log_statements": []map[string]any{
56+
{
57+
"context": "log",
58+
"statements": []string{
59+
`set(cache, ParseJSON(body))`,
60+
`merge_maps(attributes, ExtractPatterns(cache["message"], "(?P<webrequest>[A-Z]{3}.*?[\\d]{3})"), "insert")`,
61+
`set(severity_text, cache["level"])`,
62+
`set(time_unix_nano, Int(cache["time"]*1000000000))`,
63+
`set(severity_number, SEVERITY_NUMBER_DEBUG) where severity_text == "DEBUG"`,
64+
`set(severity_number, SEVERITY_NUMBER_INFO) where severity_text == "INFO"`,
65+
`set(severity_number, SEVERITY_NUMBER_WARN) where severity_text == "WARNING"`,
66+
`set(severity_number, SEVERITY_NUMBER_ERROR) where severity_text == "ERROR"`,
67+
`set(severity_number, SEVERITY_NUMBER_FATAL) where severity_text == "CRITICAL"`,
68+
},
69+
},
70+
},
71+
}
72+
73+
otelConfig.Pipelines["logs/pgadmin"] = Pipeline{
74+
Extensions: []ComponentID{"file_storage/pgadmin"},
75+
Receivers: []ComponentID{"filelog/pgadmin"},
76+
Processors: []ComponentID{
77+
"resource/pgadmin",
78+
"transform/pgadmin_log",
79+
SubSecondBatchProcessor,
80+
CompactingProcessor,
81+
},
82+
Exporters: []ComponentID{DebugExporter},
83+
}
84+
85+
otelConfig.Pipelines["logs/gunicorn"] = Pipeline{
86+
Extensions: []ComponentID{"file_storage/gunicorn"},
87+
Receivers: []ComponentID{"filelog/gunicorn"},
88+
Processors: []ComponentID{
89+
"resource/pgadmin",
90+
"transform/pgadmin_log",
91+
SubSecondBatchProcessor,
92+
CompactingProcessor,
93+
},
94+
Exporters: []ComponentID{DebugExporter},
95+
}
96+
97+
otelYAML, err := otelConfig.ToYAML()
98+
if err != nil {
99+
return err
100+
}
101+
configmap.Data["collector.yaml"] = otelYAML
102+
return nil
103+
}

internal/collector/pgadmin_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2024 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package collector
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
"gotest.tools/v3/assert"
12+
corev1 "k8s.io/api/core/v1"
13+
14+
"github.com/crunchydata/postgres-operator/internal/feature"
15+
"github.com/crunchydata/postgres-operator/internal/initialize"
16+
"github.com/crunchydata/postgres-operator/internal/naming"
17+
"github.com/crunchydata/postgres-operator/internal/testing/cmp"
18+
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
19+
)
20+
21+
func TestEnablePgAdminLogging(t *testing.T) {
22+
t.Run("Enabled", func(t *testing.T) {
23+
gate := feature.NewGate()
24+
assert.NilError(t, gate.SetFromMap(map[string]bool{
25+
feature.OpenTelemetryLogs: true,
26+
}))
27+
28+
ctx := feature.NewContext(context.Background(), gate)
29+
30+
pgadmin := new(v1beta1.PGAdmin)
31+
configmap := &corev1.ConfigMap{ObjectMeta: naming.StandalonePGAdmin(pgadmin)}
32+
initialize.Map(&configmap.Data)
33+
err := EnablePgAdminLogging(ctx, configmap)
34+
assert.NilError(t, err)
35+
36+
assert.Assert(t, cmp.MarshalMatches(configmap.Data, `
37+
collector.yaml: |
38+
# Generated by postgres-operator. DO NOT EDIT.
39+
# Your changes will not be saved.
40+
exporters:
41+
debug:
42+
verbosity: detailed
43+
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
51+
fsync: true
52+
processors:
53+
batch/1s:
54+
timeout: 1s
55+
batch/200ms:
56+
timeout: 200ms
57+
groupbyattrs/compact: {}
58+
resource/pgadmin:
59+
attributes:
60+
- action: insert
61+
key: k8s.container.name
62+
value: pgadmin
63+
- action: insert
64+
key: k8s.namespace.name
65+
value: ${env:K8S_POD_NAMESPACE}
66+
- action: insert
67+
key: k8s.pod.name
68+
value: ${env:K8S_POD_NAME}
69+
transform/pgadmin_log:
70+
log_statements:
71+
- context: log
72+
statements:
73+
- set(cache, ParseJSON(body))
74+
- merge_maps(attributes, ExtractPatterns(cache["message"], "(?P<webrequest>[A-Z]{3}.*?[\\d]{3})"),
75+
"insert")
76+
- set(severity_text, cache["level"])
77+
- set(time_unix_nano, Int(cache["time"]*1000000000))
78+
- set(severity_number, SEVERITY_NUMBER_DEBUG) where severity_text == "DEBUG"
79+
- set(severity_number, SEVERITY_NUMBER_INFO) where severity_text == "INFO"
80+
- set(severity_number, SEVERITY_NUMBER_WARN) where severity_text == "WARNING"
81+
- set(severity_number, SEVERITY_NUMBER_ERROR) where severity_text == "ERROR"
82+
- set(severity_number, SEVERITY_NUMBER_FATAL) where severity_text == "CRITICAL"
83+
receivers:
84+
filelog/gunicorn:
85+
include:
86+
- /var/lib/pgadmin/logs/gunicorn.log
87+
storage: file_storage/gunicorn
88+
filelog/pgadmin:
89+
include:
90+
- /var/lib/pgadmin/logs/pgadmin.log
91+
storage: file_storage/pgadmin
92+
service:
93+
extensions:
94+
- file_storage/gunicorn
95+
- file_storage/pgadmin
96+
pipelines:
97+
logs/gunicorn:
98+
exporters:
99+
- debug
100+
processors:
101+
- resource/pgadmin
102+
- transform/pgadmin_log
103+
- batch/200ms
104+
- groupbyattrs/compact
105+
receivers:
106+
- filelog/gunicorn
107+
logs/pgadmin:
108+
exporters:
109+
- debug
110+
processors:
111+
- resource/pgadmin
112+
- transform/pgadmin_log
113+
- batch/200ms
114+
- groupbyattrs/compact
115+
receivers:
116+
- filelog/pgadmin
117+
`))
118+
})
119+
}

internal/controller/postgrescluster/instance.go

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

12031203
if err == nil &&
12041204
(feature.Enabled(ctx, feature.OpenTelemetryLogs) || feature.Enabled(ctx, feature.OpenTelemetryMetrics)) {
1205-
collector.AddToPod(ctx, cluster, instanceConfigMap, &instance.Spec.Template.Spec,
1205+
collector.AddToPod(ctx, cluster.Spec.ImagePullPolicy, instanceConfigMap, &instance.Spec.Template.Spec,
12061206
[]corev1.VolumeMount{postgres.DataVolumeMount()}, "")
12071207
}
12081208

internal/controller/standalone_pgadmin/configmap.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"github.com/pkg/errors"
2020

21+
"github.com/crunchydata/postgres-operator/internal/collector"
2122
"github.com/crunchydata/postgres-operator/internal/initialize"
2223
"github.com/crunchydata/postgres-operator/internal/naming"
2324
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
@@ -32,6 +33,12 @@ func (r *PGAdminReconciler) reconcilePGAdminConfigMap(
3233
clusters map[string][]*v1beta1.PostgresCluster,
3334
) (*corev1.ConfigMap, error) {
3435
configmap, err := configmap(pgadmin, clusters)
36+
if err != nil {
37+
return configmap, err
38+
}
39+
40+
err = collector.EnablePgAdminLogging(ctx, configmap)
41+
3542
if err == nil {
3643
err = errors.WithStack(r.setControllerReference(pgadmin, configmap))
3744
}

0 commit comments

Comments
 (0)