Skip to content

Commit f43a670

Browse files
authored
observe mysql/postgres column type changes (#4488)
add metric to count column type change events for MySQL and postgres
1 parent ff4139e commit f43a670

4 files changed

Lines changed: 80 additions & 11 deletions

File tree

flow/connectors/mysql/cdc.go

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/pingcap/tidb/pkg/parser/ast"
2222
_ "github.com/pingcap/tidb/pkg/types/parser_driver"
2323
"go.opentelemetry.io/otel/attribute"
24+
"go.opentelemetry.io/otel/metric"
2425
"go.opentelemetry.io/otel/trace"
2526
"golang.org/x/text/encoding"
2627
"google.golang.org/protobuf/proto"
@@ -590,7 +591,8 @@ func (c *MySqlConnector) PullRecords(
590591
for _, stmt := range stmts {
591592
if alterTableStmt, ok := stmt.(*ast.AlterTableStmt); ok {
592593
if err := c.processAlterTableQuery(
593-
ctx, catalogPool, req, alterTableStmt, string(ev.Schema), binlogRowMetadataSupported, req.InternalVersion); err != nil {
594+
ctx, catalogPool, otelManager, req, alterTableStmt,
595+
string(ev.Schema), binlogRowMetadataSupported, req.InternalVersion); err != nil {
594596
return fmt.Errorf("failed to process ALTER TABLE query: %w", err)
595597
}
596598
}
@@ -669,7 +671,7 @@ func (c *MySqlConnector) PullRecords(
669671
if ev.Table.ColumnName != nil {
670672
var err error
671673
fields, err = c.processTableMapEventSchema(
672-
ctx, catalogPool, req, ev.Table,
674+
ctx, catalogPool, otelManager, req, ev.Table,
673675
sourceTableName, destinationTableName, schema, exclusion,
674676
)
675677
if err != nil {
@@ -828,6 +830,7 @@ func (c *MySqlConnector) PullRecords(
828830
}
829831

830832
func (c *MySqlConnector) processAlterTableQuery(ctx context.Context, catalogPool shared.CatalogPool,
833+
otelManager *otel_metrics.OtelManager,
831834
req *model.PullRecordsRequest[model.RecordItems], stmt *ast.AlterTableStmt, stmtSchema string,
832835
binlogRowMetadataSupported bool, mirrorVersion uint32,
833836
) error {
@@ -855,6 +858,13 @@ func (c *MySqlConnector) processAlterTableQuery(ctx context.Context, catalogPool
855858
NullableEnabled: currentSchema != nil && currentSchema.NullableEnabled,
856859
}
857860

861+
existingColTypes := make(map[string]string)
862+
if currentSchema != nil {
863+
for _, col := range currentSchema.Columns {
864+
existingColTypes[col.Name] = col.Type
865+
}
866+
}
867+
858868
hasPositionShiftingDdlChanges := false
859869

860870
for _, spec := range stmt.Specs {
@@ -869,18 +879,28 @@ func (c *MySqlConnector) processAlterTableQuery(ctx context.Context, catalogPool
869879
continue
870880
}
871881

882+
qkind, err := QkindFromMysqlColumnType(col.Tp.InfoSchemaStr(), binlogRowMetadataSupported, mirrorVersion)
883+
if err != nil {
884+
return err
885+
}
886+
887+
if oldType, exists := existingColTypes[col.Name.OrigColName()]; exists && oldType != string(qkind) {
888+
c.logger.Warn("column type change detected via ALTER TABLE, not propagating",
889+
slog.String("table", sourceTableName),
890+
slog.String("column", col.Name.OrigColName()),
891+
slog.String("from", oldType),
892+
slog.String("to", string(qkind)))
893+
c.recordColumnTypeChange(ctx, otelManager, types.QValueKind(oldType), qkind,
894+
otel_metrics.SourceEventTypeDDL)
895+
}
896+
872897
if spec.Position != nil && spec.Position.Tp != ast.ColumnPositionNone {
873898
hasPositionShiftingDdlChanges = true
874899
c.logger.Warn("column added with position specifier (FIRST/AFTER)",
875900
slog.String("columnName", col.Name.String()),
876901
slog.String("tableName", sourceTableName))
877902
}
878903

879-
qkind, err := QkindFromMysqlColumnType(col.Tp.InfoSchemaStr(), binlogRowMetadataSupported, mirrorVersion)
880-
if err != nil {
881-
return err
882-
}
883-
884904
nullable := true
885905
for _, option := range col.Options {
886906
if option.Tp == ast.ColumnOptionNotNull {
@@ -956,12 +976,25 @@ func parseIncidentEvent(data []byte) (uint16, string) {
956976
return incident, string(data[3:end])
957977
}
958978

979+
func (c *MySqlConnector) recordColumnTypeChange(
980+
ctx context.Context, otelManager *otel_metrics.OtelManager, from types.QValueKind, to types.QValueKind,
981+
eventType string,
982+
) {
983+
otelManager.Metrics.ColumnTypeChangesCounter.Add(ctx, 1, metric.WithAttributeSet(attribute.NewSet(
984+
attribute.String(otel_metrics.SourcePeerType, "mysql"),
985+
attribute.String(otel_metrics.TypeChangeFromKey, string(from)),
986+
attribute.String(otel_metrics.TypeChangeToKey, string(to)),
987+
attribute.String(otel_metrics.SourceEventTypeKey, eventType),
988+
)))
989+
}
990+
959991
// processTableMapEventSchema compares the TABLE_MAP_EVENT schema against the cached schema
960992
// and returns a TableSchemaDelta if new columns are detected (e.g., after gh-ost migration).
961993
// It also returns a slice mapping binlog column index to FieldDescription for efficient row processing.
962994
func (c *MySqlConnector) processTableMapEventSchema(
963995
ctx context.Context,
964996
catalogPool shared.CatalogPool,
997+
otelManager *otel_metrics.OtelManager,
965998
req *model.PullRecordsRequest[model.RecordItems],
966999
tableMap *replication.TableMapEvent,
9671000
sourceTableName string,
@@ -989,14 +1022,26 @@ func (c *MySqlConnector) processTableMapEventSchema(
9891022
continue
9901023
}
9911024

1025+
var charset uint16
1026+
if collation, ok := collationMap[idx]; ok {
1027+
charset = uint16(collation)
1028+
}
1029+
9921030
if fd, exists := existingCols[colName]; exists {
9931031
newFds[idx] = fd
1032+
1033+
if qkind, err := qkindFromMysqlType(
1034+
tableMap.ColumnType[idx], unsignedMap[idx], charset, req.InternalVersion,
1035+
); err == nil && qkind != types.QValueKind(fd.Type) {
1036+
c.logger.Warn("column type change detected from TABLE_MAP_EVENT, not propagating",
1037+
slog.String("table", sourceTableName),
1038+
slog.String("column", colName),
1039+
slog.String("from", fd.Type),
1040+
slog.String("to", string(qkind)))
1041+
c.recordColumnTypeChange(ctx, otelManager, types.QValueKind(fd.Type), qkind, otel_metrics.SourceEventTypeEventMetadata)
1042+
}
9941043
} else {
9951044
// New column detected - get type from TABLE_MAP_EVENT
996-
var charset uint16
997-
if collation, ok := collationMap[idx]; ok {
998-
charset = uint16(collation)
999-
}
10001045
mytype := tableMap.ColumnType[idx]
10011046
qkind, err := qkindFromMysqlType(mytype, unsignedMap[idx], charset, req.InternalVersion)
10021047
if err != nil {

flow/connectors/postgres/cdc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,12 @@ func processRelationMessage[Items model.Items](
13051305
} else if prevRelMap[column.Name] != currRelMap[column.Name] {
13061306
p.logger.Warn(fmt.Sprintf("Detected column %s with type changed from %s to %s in table %s, but not propagating",
13071307
column.Name, prevRelMap[column.Name], currRelMap[column.Name], schemaDelta.SrcTableName))
1308+
p.otelManager.Metrics.ColumnTypeChangesCounter.Add(ctx, 1, metric.WithAttributeSet(attribute.NewSet(
1309+
attribute.String(otel_metrics.SourcePeerType, "postgres"),
1310+
attribute.String(otel_metrics.TypeChangeFromKey, prevRelMap[column.Name]),
1311+
attribute.String(otel_metrics.TypeChangeToKey, currRelMap[column.Name]),
1312+
attribute.String(otel_metrics.SourceEventTypeKey, otel_metrics.SourceEventTypeEventMetadata),
1313+
)))
13081314
}
13091315
}
13101316
for _, column := range prevSchema.Columns {

flow/otel_metrics/attributes.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ const (
4646
EndBatchIDKey = "endBatchID"
4747
ResumeTokenKey = "resumeToken"
4848
GtidKey = "gtid"
49+
TypeChangeFromKey = "from"
50+
TypeChangeToKey = "to"
51+
SourceEventTypeKey = "sourceEventType"
4952
)
5053

5154
const (
@@ -63,3 +66,8 @@ const (
6366
InstanceStatusUnknown = "unknown"
6467
InstanceStatusReady = "ready"
6568
)
69+
70+
const (
71+
SourceEventTypeDDL = "ddl"
72+
SourceEventTypeEventMetadata = "eventMetadata"
73+
)

flow/otel_metrics/otel_manager.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ const (
8181
CodeNotificationCounterName = "code_notification"
8282
ServerWalEndLagGaugeName = "wal_end_lag"
8383
UsedMySQLCharsetsName = "used_mysql_charsets"
84+
ColumnTypeChangesName = "column_type_changes"
8485
)
8586

8687
type Metrics struct {
@@ -136,6 +137,7 @@ type Metrics struct {
136137
UnchangedToastValuesCounter metric.Int64Counter
137138
ServerWalEndLagGauge metric.Int64Gauge
138139
UsedMySQLCharsetsCounter metric.Int64Counter
140+
ColumnTypeChangesCounter metric.Int64Counter
139141
}
140142

141143
type SlotMetricGauges struct {
@@ -582,6 +584,14 @@ func (om *OtelManager) setupMetrics(ctx context.Context) error {
582584
return err
583585
}
584586

587+
if om.Metrics.ColumnTypeChangesCounter, err = om.GetOrInitInt64Counter(BuildMetricName(ColumnTypeChangesName),
588+
metric.WithDescription(
589+
"Counter of column type changes detected on the CDC path, with `source` label holding the source peer type, "+
590+
"`from`/`to` labels holding the source/target type and `sourceEventType` holding the source of event(ddl, eventMetadata)"),
591+
); err != nil {
592+
return err
593+
}
594+
585595
if CodeNotificationCounter, err = om.GetOrInitInt64Counter(BuildMetricName(CodeNotificationCounterName),
586596
metric.WithDescription("One-off notifications with unique `message` attribute, triggers generic non-paging alert"),
587597
); err != nil {

0 commit comments

Comments
 (0)