Skip to content

Commit bdbaae5

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 fc31149 commit bdbaae5

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

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)