Skip to content

Commit 50fd5ef

Browse files
committed
Allow OTel metrics to work with postgres versions greater than 17. If postgres_exporter is used with postgres versions greater than 17, create a warning event and set the setup.sql blank.
1 parent ea77be2 commit 50fd5ef

File tree

4 files changed

+199
-15
lines changed

4 files changed

+199
-15
lines changed

internal/controller/postgrescluster/pgmonitor.go

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func (r *Reconciler) reconcilePGMonitorExporter(ctx context.Context,
4343
monitoringSecret *corev1.Secret) error {
4444

4545
var (
46+
err error
4647
writableInstance *Instance
4748
writablePod *corev1.Pod
4849
setup string
@@ -64,23 +65,11 @@ func (r *Reconciler) reconcilePGMonitorExporter(ctx context.Context,
6465
// that function against an updated and running pod.
6566

6667
if pgmonitor.ExporterEnabled(ctx, cluster) || collector.OpenTelemetryMetricsEnabled(ctx, cluster) {
67-
sql, err := os.ReadFile(fmt.Sprintf("%s/pg%d/setup.sql", pgmonitor.GetQueriesConfigDir(ctx), cluster.Spec.PostgresVersion))
68+
setup, err = r.reconcileExporterSqlSetup(ctx, cluster)
6869
if err != nil {
6970
return err
7071
}
7172

72-
if collector.OpenTelemetryMetricsEnabled(ctx, cluster) {
73-
setup = metricsSetupForOTelCollector
74-
} else {
75-
// TODO: Revisit how pgbackrest_info.sh is used with pgMonitor.
76-
// pgMonitor queries expect a path to a script that runs pgBackRest
77-
// info and provides json output. In the queries yaml for pgBackRest
78-
// the default path is `/usr/bin/pgbackrest-info.sh`. We update
79-
// the path to point to the script in our database image.
80-
setup = strings.ReplaceAll(string(sql), "/usr/bin/pgbackrest-info.sh",
81-
"/opt/crunchy/bin/postgres/pgbackrest_info.sh")
82-
}
83-
8473
for _, containerStatus := range writablePod.Status.ContainerStatuses {
8574
if containerStatus.Name == naming.ContainerDatabase {
8675
pgImageSHA = containerStatus.ImageID
@@ -145,6 +134,47 @@ func (r *Reconciler) reconcilePGMonitorExporter(ctx context.Context,
145134
return err
146135
}
147136

137+
// reconcileExporterSqlSetup generates the setup.sql string based on
138+
// whether the OTel metrics feature is enabled or not and the postgres
139+
// version being used. This function assumes that at least one of
140+
// OTel metrics or postgres_exporter are enabled.
141+
func (r *Reconciler) reconcileExporterSqlSetup(ctx context.Context,
142+
cluster *v1beta1.PostgresCluster) (string, error) {
143+
144+
// If OTel Metrics is enabled we always want to use it. Otherwise,
145+
// we can assume that postgres_exporter is enabled and we should
146+
// use that
147+
if collector.OpenTelemetryMetricsEnabled(ctx, cluster) {
148+
return metricsSetupForOTelCollector, nil
149+
}
150+
151+
// pgMonitor will not be adding support for postgres_exporter for postgres
152+
// versions past 17. If using postgres 18 or later with the postgres_exporter,
153+
// create a warning event and set the sql setup to an empty string
154+
pgVersion := cluster.Spec.PostgresVersion
155+
if pgVersion > 17 {
156+
r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "ExporterNotSupportedForPostgresVersion",
157+
"postgres_exporter not supported for pg%d; use OTel for postgres 18 and later",
158+
pgVersion)
159+
return "", nil
160+
}
161+
162+
// OTel Metrics is not enabled and postgres is version 17 or less,
163+
// go ahead and read the appropriate sql file, format the string,
164+
// and return it
165+
sql, err := os.ReadFile(fmt.Sprintf("%s/pg%d/setup.sql", pgmonitor.GetQueriesConfigDir(ctx), pgVersion))
166+
if err != nil {
167+
return "", err
168+
}
169+
// TODO: Revisit how pgbackrest_info.sh is used with pgMonitor.
170+
// pgMonitor queries expect a path to a script that runs pgBackRest
171+
// info and provides json output. In the queries yaml for pgBackRest
172+
// the default path is `/usr/bin/pgbackrest-info.sh`. We update
173+
// the path to point to the script in our database image.
174+
return strings.ReplaceAll(string(sql), "/usr/bin/pgbackrest-info.sh",
175+
"/opt/crunchy/bin/postgres/pgbackrest_info.sh"), nil
176+
}
177+
148178
// reconcileMonitoringSecret reconciles the secret containing authentication
149179
// for monitoring tools
150180
func (r *Reconciler) reconcileMonitoringSecret(

internal/controller/postgrescluster/pgmonitor_test.go

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import (
2020
"k8s.io/apimachinery/pkg/types"
2121
"sigs.k8s.io/controller-runtime/pkg/client"
2222

23+
"github.com/crunchydata/postgres-operator/internal/controller/runtime"
2324
"github.com/crunchydata/postgres-operator/internal/feature"
2425
"github.com/crunchydata/postgres-operator/internal/initialize"
2526
"github.com/crunchydata/postgres-operator/internal/naming"
2627
"github.com/crunchydata/postgres-operator/internal/testing/cmp"
28+
"github.com/crunchydata/postgres-operator/internal/testing/events"
2729
"github.com/crunchydata/postgres-operator/internal/testing/require"
2830
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
2931
)
@@ -551,8 +553,7 @@ func TestReconcilePGMonitorExporter(t *testing.T) {
551553
observed := &observedInstances{forCluster: instances}
552554

553555
called = false
554-
assert.NilError(t, reconciler.reconcilePGMonitorExporter(ctx,
555-
cluster, observed, nil))
556+
assert.NilError(t, reconciler.reconcilePGMonitorExporter(ctx, cluster, observed, nil))
556557
assert.Assert(t, called, "PodExec was not called.")
557558
assert.Assert(t, cluster.Status.Monitoring.ExporterConfiguration != "", "ExporterConfiguration was empty.")
558559
})
@@ -836,6 +837,147 @@ func TestReconcileExporterQueriesConfig(t *testing.T) {
836837
actual, err = reconciler.reconcileExporterQueriesConfig(ctx, cluster)
837838
assert.NilError(t, err)
838839
assert.Assert(t, actual.Data["defaultQueries.yml"] == existing.Data["defaultQueries.yml"], "Data does not align.")
840+
assert.Assert(t, actual.Data["defaultQueries.yml"] != "", "Data should not be empty.")
841+
})
842+
843+
t.Run("Pg>17", func(t *testing.T) {
844+
cluster.Spec.PostgresVersion = 18
845+
actual, err = reconciler.reconcileExporterQueriesConfig(ctx, cluster)
846+
assert.NilError(t, err)
847+
assert.Assert(t, actual.Data["defaultQueries.yml"] == "", "Data should be empty")
839848
})
840849
})
841850
}
851+
852+
// TestReconcileExporterSqlSetup checks that the setup script returned
853+
// by reconcileExporterSqlSetup is either empty or not depending on
854+
// which exporter is enabled and what the postgres version is.
855+
func TestReconcileExporterSqlSetup(t *testing.T) {
856+
ctx := context.Background()
857+
858+
monitoringSpec := &v1beta1.MonitoringSpec{
859+
PGMonitor: &v1beta1.PGMonitorSpec{
860+
Exporter: &v1beta1.ExporterSpec{
861+
Image: "image",
862+
},
863+
},
864+
}
865+
866+
instrumentationSpec := &v1beta1.InstrumentationSpec{
867+
Image: "image",
868+
}
869+
870+
testCases := []struct {
871+
tcName string
872+
postgresVersion int32
873+
exporterEnabled bool
874+
otelMetricsEnabled bool
875+
errorPresent bool
876+
setupEmpty bool
877+
expectedNumEvents int
878+
expectedEvent string
879+
}{{
880+
tcName: "ExporterEnabledOtelDisabled",
881+
postgresVersion: 17,
882+
exporterEnabled: true,
883+
otelMetricsEnabled: false,
884+
errorPresent: false,
885+
setupEmpty: false,
886+
expectedNumEvents: 0,
887+
expectedEvent: "",
888+
}, {
889+
tcName: "ExporterDisabledOtelEnabled",
890+
postgresVersion: 17,
891+
exporterEnabled: false,
892+
otelMetricsEnabled: true,
893+
errorPresent: false,
894+
setupEmpty: false,
895+
expectedNumEvents: 0,
896+
expectedEvent: "",
897+
}, {
898+
tcName: "BothEnabled",
899+
postgresVersion: 17,
900+
exporterEnabled: true,
901+
otelMetricsEnabled: true,
902+
errorPresent: false,
903+
setupEmpty: false,
904+
expectedNumEvents: 0,
905+
expectedEvent: "",
906+
}, {
907+
tcName: "ExporterEnabledOtelDisabledPostgres18",
908+
postgresVersion: 18,
909+
exporterEnabled: true,
910+
otelMetricsEnabled: false,
911+
errorPresent: false,
912+
setupEmpty: true,
913+
expectedNumEvents: 1,
914+
expectedEvent: "postgres_exporter not supported for pg18; use OTel for postgres 18 and later",
915+
}, {
916+
tcName: "ExporterDisabledOtelEnabledPostgres18",
917+
postgresVersion: 18,
918+
exporterEnabled: false,
919+
otelMetricsEnabled: true,
920+
errorPresent: false,
921+
setupEmpty: false,
922+
expectedNumEvents: 0,
923+
expectedEvent: "",
924+
}, {
925+
tcName: "BothEnabledPostgres18",
926+
postgresVersion: 18,
927+
exporterEnabled: true,
928+
otelMetricsEnabled: true,
929+
errorPresent: false,
930+
setupEmpty: false,
931+
expectedNumEvents: 0,
932+
expectedEvent: "",
933+
}, {
934+
tcName: "ExporterEnabledOtelDisabledBadPostgresVersion",
935+
postgresVersion: 1,
936+
exporterEnabled: true,
937+
otelMetricsEnabled: false,
938+
errorPresent: true,
939+
setupEmpty: true,
940+
expectedNumEvents: 0,
941+
expectedEvent: "",
942+
}}
943+
944+
for _, tc := range testCases {
945+
t.Run(tc.tcName, func(t *testing.T) {
946+
cluster := testCluster()
947+
cluster.Spec.PostgresVersion = tc.postgresVersion
948+
949+
recorder := events.NewRecorder(t, runtime.Scheme)
950+
r := &Reconciler{Recorder: recorder}
951+
952+
gate := feature.NewGate()
953+
assert.NilError(t, gate.SetFromMap(map[string]bool{
954+
feature.OpenTelemetryMetrics: tc.otelMetricsEnabled,
955+
}))
956+
ctx := feature.NewContext(ctx, gate)
957+
958+
if tc.otelMetricsEnabled {
959+
cluster.Spec.Instrumentation = instrumentationSpec
960+
}
961+
962+
if tc.exporterEnabled {
963+
cluster.Spec.Monitoring = monitoringSpec
964+
}
965+
966+
setup, err := r.reconcileExporterSqlSetup(ctx, cluster)
967+
if tc.errorPresent {
968+
assert.Assert(t, err != nil)
969+
} else {
970+
assert.NilError(t, err)
971+
}
972+
assert.Equal(t, setup == "", tc.setupEmpty)
973+
974+
assert.Equal(t, len(recorder.Events), tc.expectedNumEvents)
975+
if tc.expectedNumEvents == 1 {
976+
assert.Equal(t, recorder.Events[0].Regarding.Name, cluster.Name)
977+
assert.Equal(t, recorder.Events[0].Reason, "ExporterNotSupportedForPostgresVersion")
978+
assert.Equal(t, recorder.Events[0].Note, tc.expectedEvent)
979+
assert.Equal(t, recorder.Events[0].Type, corev1.EventTypeWarning)
980+
}
981+
})
982+
}
983+
}

internal/pgmonitor/exporter.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ func GenerateDefaultExporterQueries(ctx context.Context, cluster *v1beta1.Postgr
6666
queries += string(queriesContents) + "\n"
6767
}
6868

69+
// pgMonitor will not be adding support for postgres_exporter for postgres
70+
// versions past 17. If pg version is greater than 17, return an empty string.
71+
if cluster.Spec.PostgresVersion > 17 {
72+
return ""
73+
}
74+
6975
// Add general queries for specific postgres version
7076
queriesGeneral, err := os.ReadFile(fmt.Sprintf("%s/pg%d/queries_general.yml", queriesConfigDir, cluster.Spec.PostgresVersion))
7177
if err != nil {

internal/pgmonitor/exporter_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ func TestGenerateDefaultExporterQueries(t *testing.T) {
3838
assert.Assert(t, strings.Contains(queries, "ccp_pg_stat_statements_reset"),
3939
"Queries do not contain 'ccp_pg_stat_statements_reset' query when they should.")
4040
})
41+
42+
t.Run("PG>17", func(t *testing.T) {
43+
cluster.Spec.PostgresVersion = 18
44+
queries := GenerateDefaultExporterQueries(ctx, cluster)
45+
assert.Equal(t, queries, "")
46+
})
4147
}
4248

4349
func TestExporterStartCommand(t *testing.T) {

0 commit comments

Comments
 (0)