Skip to content

Commit 9bc3b5d

Browse files
raj14243joao-r-reis
authored andcommitted
Add PreparedMetadata and IsPrepared fields to ObservedQuery and ObservedBatch
ObservedQuery and ObservedBatch did not expose the keyspace/table targeted by a query, requiring consumers (metrics collectors, loggers, tracers) to parse the CQL statement string. This change introduces a PreparedMetadata struct (Keyspace, Table) on ObservedQuery and a parallel []PreparedMetadata slice on ObservedBatch, populated from prepared statement metadata returned by Cassandra. An IsPrepared bool on ObservedQuery and a parallel []bool on ObservedBatch explicitly report when the metadata is valid; statements that take the unprepared path (e.g. DDL) report IsPrepared=false and zero PreparedMetadata. The information was already available internally via preparedMetadata (used for routing), so no additional round-trips or CQL parsing are needed. Updated TestObserve (including a negative case for an unprepared DDL statement) and TestBatchObserve to verify the new fields. Patch by Raj Ummadisetty; reviewed by João Reis, Bohdan Siryk for CASSGO-119
1 parent 40db71c commit 9bc3b5d

5 files changed

Lines changed: 179 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Security-model discoverability (CASSANDRA-21464)
1313
- Improve host_source locking and ring refresh concurrency (CASSGO-121)
14+
- Add PreparedMetadata (Keyspace, Table) and IsPrepared fields to ObservedQuery, and parallel PreparedMetadata / IsPrepared slices to ObservedBatch, for statement-level observability without CQL parsing (CASSGO-119)
1415

1516
## [2.1.2]
1617

cassandra_test.go

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -172,22 +172,31 @@ func TestObserve(t *testing.T) {
172172
}
173173

174174
var (
175-
observedErr error
176-
observedKeyspace string
177-
observedStmt string
175+
observedErr error
176+
observedKeyspace string
177+
observedPreparedKeyspace string
178+
observedStmt string
179+
observedPreparedTable string
180+
observedIsPrepared bool
178181
)
179182

180183
const keyspace = "gocql_test"
181184

182185
resetObserved := func() {
183186
observedErr = errors.New("placeholder only") // used to distinguish err=nil cases
184187
observedKeyspace = ""
188+
observedPreparedKeyspace = ""
185189
observedStmt = ""
190+
observedPreparedTable = ""
191+
observedIsPrepared = false
186192
}
187193

188194
observer := funcQueryObserver(func(ctx context.Context, o ObservedQuery) {
189195
observedKeyspace = o.Keyspace
196+
observedPreparedKeyspace = o.PreparedMetadata.Keyspace
190197
observedStmt = o.Statement
198+
observedPreparedTable = o.PreparedMetadata.Table
199+
observedIsPrepared = o.IsPrepared
191200
observedErr = o.Err
192201
})
193202

@@ -202,6 +211,12 @@ func TestObserve(t *testing.T) {
202211
t.Fatal("select: unexpected observed keyspace", observedKeyspace)
203212
} else if observedStmt != `SELECT id FROM observe WHERE id = ?` {
204213
t.Fatal("select: unexpected observed stmt", observedStmt)
214+
} else if !observedIsPrepared {
215+
t.Fatal("select: expected observed IsPrepared to be true")
216+
} else if observedPreparedKeyspace != keyspace {
217+
t.Fatal("select: unexpected observed prepared keyspace", observedPreparedKeyspace)
218+
} else if observedPreparedTable != "observe" {
219+
t.Fatal("select: unexpected observed prepared table", observedPreparedTable)
205220
}
206221

207222
resetObserved()
@@ -213,6 +228,12 @@ func TestObserve(t *testing.T) {
213228
t.Fatal("insert: unexpected observed keyspace", observedKeyspace)
214229
} else if observedStmt != `INSERT INTO observe (id) VALUES (?)` {
215230
t.Fatal("insert: unexpected observed stmt", observedStmt)
231+
} else if !observedIsPrepared {
232+
t.Fatal("insert: expected observed IsPrepared to be true")
233+
} else if observedPreparedKeyspace != keyspace {
234+
t.Fatal("insert: unexpected observed prepared keyspace", observedPreparedKeyspace)
235+
} else if observedPreparedTable != "observe" {
236+
t.Fatal("insert: unexpected observed prepared table", observedPreparedTable)
216237
}
217238

218239
resetObserved()
@@ -227,6 +248,12 @@ func TestObserve(t *testing.T) {
227248
t.Fatal("select: unexpected observed keyspace", observedKeyspace)
228249
} else if observedStmt != `SELECT id FROM observe WHERE id = ?` {
229250
t.Fatal("select: unexpected observed stmt", observedStmt)
251+
} else if !observedIsPrepared {
252+
t.Fatal("select: expected observed IsPrepared to be true")
253+
} else if observedPreparedKeyspace != keyspace {
254+
t.Fatal("select: unexpected observed prepared keyspace", observedPreparedKeyspace)
255+
} else if observedPreparedTable != "observe" {
256+
t.Fatal("select: unexpected observed prepared table", observedPreparedTable)
230257
}
231258

232259
// also works from session observer
@@ -240,6 +267,12 @@ func TestObserve(t *testing.T) {
240267
t.Fatal("select: unexpected observed keyspace", observedKeyspace)
241268
} else if observedStmt != `SELECT id FROM observe WHERE id = ?` {
242269
t.Fatal("select: unexpected observed stmt", observedStmt)
270+
} else if !observedIsPrepared {
271+
t.Fatal("select: expected observed IsPrepared to be true")
272+
} else if observedPreparedKeyspace != keyspace {
273+
t.Fatal("select: unexpected observed prepared keyspace", observedPreparedKeyspace)
274+
} else if observedPreparedTable != "observe" {
275+
t.Fatal("select: unexpected observed prepared table", observedPreparedTable)
243276
}
244277

245278
// reports errors when the query is poorly formed
@@ -254,6 +287,24 @@ func TestObserve(t *testing.T) {
254287
} else if observedStmt != `SELECT id FROM unknown_table WHERE id = ?` {
255288
t.Fatal("select: unexpected observed stmt", observedStmt)
256289
}
290+
291+
// statements that cannot be prepared (e.g. DDL) report IsPrepared=false and
292+
// a zero PreparedMetadata, so the contract is explicit for observers.
293+
resetObserved()
294+
const ddlStmt = `CREATE TABLE IF NOT EXISTS gocql_test.observe_unprepared (id int primary key)`
295+
if err := session.Query(ddlStmt).Observer(observer).Exec(); err != nil {
296+
t.Fatal("ddl:", err)
297+
} else if observedErr != nil {
298+
t.Fatal("ddl:", observedErr)
299+
} else if observedStmt != ddlStmt {
300+
t.Fatal("ddl: unexpected observed stmt", observedStmt)
301+
} else if observedIsPrepared {
302+
t.Fatal("ddl: expected observed IsPrepared to be false for unprepared statement")
303+
} else if observedPreparedKeyspace != "" {
304+
t.Fatal("ddl: expected zero PreparedMetadata.Keyspace, got", observedPreparedKeyspace)
305+
} else if observedPreparedTable != "" {
306+
t.Fatal("ddl: expected zero PreparedMetadata.Table, got", observedPreparedTable)
307+
}
257308
}
258309

259310
func TestObserve_Pagination(t *testing.T) {
@@ -2134,10 +2185,12 @@ func TestBatchObserve(t *testing.T) {
21342185
}
21352186

21362187
type observation struct {
2137-
observedErr error
2138-
observedKeyspace string
2139-
observedStmts []string
2140-
observedValues [][]interface{}
2188+
observedErr error
2189+
observedKeyspace string
2190+
observedPreparedMetadata []PreparedMetadata
2191+
observedStmts []string
2192+
observedIsPrepared []bool
2193+
observedValues [][]interface{}
21412194
}
21422195

21432196
var observedBatch *observation
@@ -2149,10 +2202,12 @@ func TestBatchObserve(t *testing.T) {
21492202
}
21502203

21512204
observedBatch = &observation{
2152-
observedKeyspace: o.Keyspace,
2153-
observedStmts: o.Statements,
2154-
observedErr: o.Err,
2155-
observedValues: o.Values,
2205+
observedKeyspace: o.Keyspace,
2206+
observedPreparedMetadata: o.PreparedMetadata,
2207+
observedStmts: o.Statements,
2208+
observedIsPrepared: o.IsPrepared,
2209+
observedErr: o.Err,
2210+
observedValues: o.Values,
21562211
}
21572212
}))
21582213
for i := 0; i < 100; i++ {
@@ -2175,11 +2230,29 @@ func TestBatchObserve(t *testing.T) {
21752230
if observedBatch.observedKeyspace != "gocql_test" {
21762231
t.Fatalf("expecting keyspace 'gocql_test', got %q", observedBatch.observedKeyspace)
21772232
}
2233+
if len(observedBatch.observedPreparedMetadata) != 100 {
2234+
t.Fatal("expecting 100 observed prepared metadata entries, got", len(observedBatch.observedPreparedMetadata))
2235+
}
2236+
if len(observedBatch.observedIsPrepared) != 100 {
2237+
t.Fatal("expecting 100 observed IsPrepared flags, got", len(observedBatch.observedIsPrepared))
2238+
}
21782239
for i, stmt := range observedBatch.observedStmts {
21792240
if stmt != fmt.Sprintf(`INSERT INTO batch_observe_table (id,other) VALUES (?,%d)`, i) {
21802241
t.Fatal("unexpected query", stmt)
21812242
}
21822243

2244+
if !observedBatch.observedIsPrepared[i] {
2245+
t.Fatalf("expected observed IsPrepared at index %d to be true", i)
2246+
}
2247+
2248+
if observedBatch.observedPreparedMetadata[i].Keyspace != "gocql_test" {
2249+
t.Fatalf("unexpected observed prepared keyspace at index %d: %q", i, observedBatch.observedPreparedMetadata[i].Keyspace)
2250+
}
2251+
2252+
if observedBatch.observedPreparedMetadata[i].Table != "batch_observe_table" {
2253+
t.Fatalf("unexpected observed prepared table at index %d: %q", i, observedBatch.observedPreparedMetadata[i].Table)
2254+
}
2255+
21832256
assertDeepEqual(t, "observed value", []interface{}{i}, observedBatch.observedValues[i])
21842257
}
21852258
}

conn.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,7 @@ func (c *Conn) executeQuery(ctx context.Context, q *internalQuery) *Iter {
16611661
q.routingInfo.keyspace = usedKeyspace
16621662
}
16631663
q.routingInfo.table = info.request.table
1664+
q.routingInfo.prepared = true
16641665
q.routingInfo.mu.Unlock()
16651666
} else {
16661667
frame = &writeQueryFrame{
@@ -1856,6 +1857,12 @@ func (c *Conn) executeBatch(ctx context.Context, b *internalBatch) *Iter {
18561857

18571858
stmts := make(map[string]string, len(b.batchOpts.entries))
18581859

1860+
if b.batchOpts.observer != nil {
1861+
b.entryKeyspaces = make([]string, n)
1862+
b.entryTables = make([]string, n)
1863+
b.entryPrepared = make([]bool, n)
1864+
}
1865+
18591866
for i := 0; i < n; i++ {
18601867
entry := &b.batchOpts.entries[i]
18611868
batchStmt := &req.statements[i]
@@ -1867,6 +1874,12 @@ func (c *Conn) executeBatch(ctx context.Context, b *internalBatch) *Iter {
18671874
return iter
18681875
}
18691876

1877+
if b.entryTables != nil {
1878+
b.entryKeyspaces[i] = info.request.keyspace
1879+
b.entryTables[i] = info.request.table
1880+
b.entryPrepared[i] = true
1881+
}
1882+
18701883
var values []interface{}
18711884
if entry.binding == nil {
18721885
values = entry.Args

query_executor.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,20 @@ func (q *internalQuery) attempt(keyspace string, end, start time.Time, iter *Ite
385385
q.qryOpts.observer.ObserveQuery(q.qryOpts.context, ObservedQuery{
386386
Keyspace: keyspace,
387387
Statement: q.qryOpts.stmt,
388-
Values: q.qryOpts.values,
389-
Start: start,
390-
End: end,
391-
Rows: iter.numRows,
392-
Host: host,
393-
Metrics: metricsForHost,
394-
Err: iter.err,
395-
Attempt: attempt,
396-
Query: q.originalQuery,
388+
PreparedMetadata: PreparedMetadata{
389+
Keyspace: q.routingInfo.getKeyspace(),
390+
Table: q.routingInfo.getTable(),
391+
},
392+
IsPrepared: q.routingInfo.isPrepared(),
393+
Values: q.qryOpts.values,
394+
Start: start,
395+
End: end,
396+
Rows: iter.numRows,
397+
Host: host,
398+
Metrics: metricsForHost,
399+
Err: iter.err,
400+
Attempt: attempt,
401+
Query: q.originalQuery,
397402
})
398403
}
399404
}
@@ -432,6 +437,7 @@ func (q *internalQuery) GetRoutingKey() ([]byte, error) {
432437
q.routingInfo.mu.Lock()
433438
q.routingInfo.keyspace = meta.Keyspace
434439
q.routingInfo.table = meta.Table
440+
q.routingInfo.prepared = true
435441
q.routingInfo.mu.Unlock()
436442
}
437443
return createRoutingKey(meta, q.qryOpts.values)
@@ -562,6 +568,19 @@ type internalBatch struct {
562568
session *Session
563569
metrics *queryMetrics
564570
hostMetricsManager hostMetricsManager
571+
572+
// entryKeyspaces holds the keyspace for each batch entry,
573+
// populated from prepared statement metadata during execution.
574+
entryKeyspaces []string
575+
576+
// entryTables holds the table name for each batch entry,
577+
// populated from prepared statement metadata during execution.
578+
entryTables []string
579+
580+
// entryPrepared[i] reports whether the i-th batch entry was prepared by
581+
// the driver, and therefore whether entryKeyspaces[i]/entryTables[i] hold
582+
// valid prepared statement metadata.
583+
entryPrepared []bool
565584
}
566585

567586
func newInternalBatch(batch *Batch, ctx context.Context) *internalBatch {
@@ -597,20 +616,30 @@ func (b *internalBatch) attempt(keyspace string, end, start time.Time, iter *Ite
597616

598617
metricsForHost := b.hostMetricsManager.attempt(latency, host)
599618

600-
statements := make([]string, len(b.batchOpts.entries))
601-
values := make([][]interface{}, len(b.batchOpts.entries))
619+
n := len(b.batchOpts.entries)
620+
statements := make([]string, n)
621+
values := make([][]interface{}, n)
622+
preparedMetadata := make([]PreparedMetadata, n)
602623

603624
for i, entry := range b.batchOpts.entries {
604625
statements[i] = entry.Stmt
605626
values[i] = entry.Args
627+
if i < len(b.entryKeyspaces) {
628+
preparedMetadata[i] = PreparedMetadata{
629+
Keyspace: b.entryKeyspaces[i],
630+
Table: b.entryTables[i],
631+
}
632+
}
606633
}
607634

608635
b.batchOpts.observer.ObserveBatch(b.batchOpts.context, ObservedBatch{
609-
Keyspace: keyspace,
610-
Statements: statements,
611-
Values: values,
612-
Start: start,
613-
End: end,
636+
Keyspace: keyspace,
637+
Statements: statements,
638+
PreparedMetadata: preparedMetadata,
639+
IsPrepared: b.entryPrepared,
640+
Values: values,
641+
Start: start,
642+
End: end,
614643
// Rows not used in batch observations // TODO - might be able to support it when using BatchCAS
615644
Host: host,
616645
Metrics: metricsForHost,
@@ -652,6 +681,7 @@ func (b *internalBatch) GetRoutingKey() ([]byte, error) {
652681
b.routingInfo.mu.Lock()
653682
b.routingInfo.keyspace = meta.Keyspace
654683
b.routingInfo.table = meta.Table
684+
b.routingInfo.prepared = true
655685
b.routingInfo.mu.Unlock()
656686
}
657687

session.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,8 @@ type queryRoutingInfo struct {
10861086
keyspace string
10871087

10881088
table string
1089+
1090+
prepared bool
10891091
}
10901092

10911093
func (qr *queryRoutingInfo) getKeyspace() string {
@@ -1100,6 +1102,12 @@ func (qr *queryRoutingInfo) getTable() string {
11001102
return qr.table
11011103
}
11021104

1105+
func (qr *queryRoutingInfo) isPrepared() bool {
1106+
qr.mu.RLock()
1107+
defer qr.mu.RUnlock()
1108+
return qr.prepared
1109+
}
1110+
11031111
func (q *Query) defaultsFromSession() {
11041112
s := q.session
11051113

@@ -2370,10 +2378,28 @@ func (s *Session) GetHosts() []*HostInfo {
23702378
return s.ring.allHosts()
23712379
}
23722380

2381+
// PreparedMetadata holds metadata extracted from a prepared statement
2382+
// (returned by Cassandra during statement preparation). It is exposed via
2383+
// ObservedQuery and ObservedBatch when the driver has prepared the statement.
2384+
type PreparedMetadata struct {
2385+
Keyspace string
2386+
Table string
2387+
}
2388+
23732389
type ObservedQuery struct {
23742390
Keyspace string
23752391
Statement string
23762392

2393+
// PreparedMetadata holds keyspace/table information from prepared statement
2394+
// metadata returned by Cassandra. Only valid when IsPrepared is true; the
2395+
// zero value is reported otherwise (for example, for DDL statements or
2396+
// statements not prepared by the driver).
2397+
PreparedMetadata PreparedMetadata
2398+
2399+
// IsPrepared reports whether the statement was prepared by the driver and
2400+
// therefore whether PreparedMetadata holds valid information.
2401+
IsPrepared bool
2402+
23772403
// Values holds a slice of bound values for the query.
23782404
// Do not modify the values here, they are shared with multiple goroutines.
23792405
Values []interface{}
@@ -2418,6 +2444,15 @@ type ObservedBatch struct {
24182444
Keyspace string
24192445
Statements []string
24202446

2447+
// PreparedMetadata holds prepared statement metadata for each batch statement.
2448+
// PreparedMetadata[i] corresponds to Statements[i]. Each entry is only valid
2449+
// when IsPrepared[i] is true; the zero value is reported otherwise.
2450+
PreparedMetadata []PreparedMetadata
2451+
2452+
// IsPrepared reports, for each batch statement, whether the statement was
2453+
// prepared by the driver. IsPrepared[i] corresponds to Statements[i].
2454+
IsPrepared []bool
2455+
24212456
// Values holds a slice of bound values for each statement.
24222457
// Values[i] are bound values passed to Statements[i].
24232458
// Do not modify the values here, they are shared with multiple goroutines.

0 commit comments

Comments
 (0)