Skip to content

Commit a74b1a4

Browse files
committed
feat(flow-php/symfony-telemetry-bundle): align DBAL and PostgreSQL database instrumentatio
- transaction spans (grouped/per_operation/off) that nest query spans as children - DBAL query spans gain semconv names + full db.*/server.* attributes and bound parameters - DBAL emits db.client.operation.duration and db.client.response.returned_rows metrics - metrics tagged with low-cardinality attributes only (no query text, params or savepoint) - config: trace_transactions bool replaced by transaction_spans enum, drop dbal.log_sql, add collect_metrics/include_parameters - complete open transaction spans on close/destruct to avoid leaks
1 parent 281a182 commit a74b1a4

37 files changed

Lines changed: 2103 additions & 394 deletions

File tree

documentation/components/bridges/symfony-postgresql-bundle.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ flow_postgresql:
113113
service_id: "flow.telemetry" # Required: Telemetry service ID
114114
clock_service_id: null # Optional: PSR-20 clock service (defaults to SystemClock)
115115
trace_queries: true # Record query execution in traces
116-
trace_transactions: true # Record transaction boundaries
116+
transaction_spans: grouped # grouped | per_operation | off
117117
collect_metrics: true # Collect query metrics (duration, row count)
118118
log_queries: false # Log all queries
119119
max_query_length: 1000 # Truncate queries longer than this (chars)
@@ -905,7 +905,7 @@ flow_postgresql:
905905
telemetry:
906906
service_id: "flow.telemetry"
907907
trace_queries: true
908-
trace_transactions: true
908+
transaction_spans: grouped
909909
collect_metrics: true
910910
log_queries: false
911911
max_query_length: 1000

documentation/components/bridges/symfony-telemetry-bundle.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,8 +1388,12 @@ flow_telemetry:
13881388
instrumentation:
13891389
dbal:
13901390
enabled: true
1391-
log_sql: true # Include SQL in span attributes
1392-
max_sql_length: 1000 # Max SQL length (0 = no limit)
1391+
max_sql_length: 1000 # Max db.query.text length (0 = no limit)
1392+
collect_metrics: true # Emit db.client.operation.duration + db.client.response.returned_rows
1393+
include_parameters: false # Include bound statement parameters (security consideration)
1394+
max_parameters: 10 # Max parameters to include when include_parameters is enabled
1395+
max_parameter_length: 100 # Max length per included parameter value
1396+
transaction_spans: grouped # grouped (one span per transaction) | per_operation | off
13931397
exclude_connections:
13941398
- 'legacy'
13951399
- '/^test_.*/'
@@ -1416,7 +1420,7 @@ flow_telemetry:
14161420

14171421
A cache pool backed by `cache.adapter.doctrine_dbal` defers writes and flushes them from the pool's own
14181422
commit path at process shutdown — outside any request/message span. Each such write then surfaces as an
1419-
orphan `doctrine.dbal.*` trace (`statement.prepare`/`execute`, `transaction.begin`/`commit`), one per query.
1423+
orphan `doctrine.dbal.*` trace (a `BEGIN TRANSACTION` span wrapping `statement.prepare`/`execute`), one per query.
14201424

14211425
`flush_deferred: true` drains those deferred writes at request/command termination and after each consumed
14221426
message, inside a single `cache.flush` span, so they group under one trace instead of orphaning (and the pool
@@ -2001,7 +2005,6 @@ flow_telemetry:
20012005
trace: true
20022006
dbal:
20032007
enabled: true
2004-
log_sql: true
20052008
max_sql_length: 500
20062009
cache:
20072010
enabled: true

documentation/components/libs/postgresql/client-telemetry.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,16 @@ Configure telemetry behavior with `postgresql_telemetry_options()`:
4646
```php
4747
<?php
4848

49+
use Flow\PostgreSql\Client\Telemetry\TransactionSpanMode;
50+
4951
use function Flow\PostgreSql\DSL\{postgresql_telemetry_config, postgresql_telemetry_options};
5052

5153
$config = postgresql_telemetry_config(
5254
$telemetry,
5355
$clock,
5456
postgresql_telemetry_options(
5557
traceQueries: true, // Create spans for queries
56-
traceTransactions: true, // Create spans for transactions
58+
transactionSpans: TransactionSpanMode::GROUPED, // grouped | per_operation | off
5759
collectMetrics: true, // Record duration and row count histograms
5860
logQueries: false, // Log executed queries
5961
maxQueryLength: 1000, // Truncate query text (null = unlimited)
@@ -69,7 +71,7 @@ $config = postgresql_telemetry_config(
6971
| Option | Default | Description |
7072
|---------------------|---------|-----------------------------------------------------------------------------|
7173
| `traceQueries` | `true` | Create spans for each query operation |
72-
| `traceTransactions` | `true` | Create spans for transaction lifecycle (BEGIN, COMMIT, ROLLBACK) |
74+
| `transactionSpans` | `TransactionSpanMode::GROUPED` | `GROUPED` = one span per transaction; `PER_OPERATION` = a span per BEGIN/COMMIT/ROLLBACK; `OFF` = none |
7375
| `collectMetrics` | `true` | Record duration and row count histograms |
7476
| `logQueries` | `false` | Log executed queries via the telemetry logger |
7577
| `maxQueryLength` | `1000` | Maximum query text length in span attributes (`null` = unlimited) |
@@ -84,11 +86,13 @@ Options can be configured fluently:
8486
```php
8587
<?php
8688

89+
use Flow\PostgreSql\Client\Telemetry\TransactionSpanMode;
90+
8791
use function Flow\PostgreSql\DSL\postgresql_telemetry_options;
8892

8993
$options = postgresql_telemetry_options()
9094
->traceQueries(true)
91-
->traceTransactions(true)
95+
->transactionSpans(TransactionSpanMode::GROUPED)
9296
->collectMetrics(true)
9397
->logQueries(true)
9498
->maxQueryLength(500)
@@ -219,6 +223,8 @@ Number of rows returned by database operations.
219223
```php
220224
<?php
221225

226+
use Flow\PostgreSql\Client\Telemetry\TransactionSpanMode;
227+
222228
use function Flow\PostgreSql\DSL\{
223229
pgsql_client,
224230
pgsql_connection,
@@ -323,7 +329,7 @@ $client = traceable_postgresql_client(
323329
$clock,
324330
postgresql_telemetry_options(
325331
traceQueries: false,
326-
traceTransactions: false,
332+
transactionSpans: TransactionSpanMode::OFF,
327333
collectMetrics: true,
328334
),
329335
),

documentation/upgrading.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,44 @@ An excluded path now attaches the OpenTelemetry suppression key for the request'
8383
orphan root spans (e.g. the `/_wdt` toolbar fetch writing an audit row after its response). If you relied on
8484
those child spans being recorded for an excluded path, remove the path from `exclude_paths`.
8585

86+
### 7) `flow-php/postgresql`, `flow-php/symfony-postgresql-bundle` - `traceTransactions` bool replaced by
87+
`transactionSpans` mode
88+
89+
| Before | After |
90+
|----------------------------------------------------------------|--------------------------------------------------------------------------------|
91+
| `postgresql_telemetry_options(traceTransactions: true)` | `postgresql_telemetry_options(transactionSpans: TransactionSpanMode::GROUPED)` |
92+
| `postgresql_telemetry_options(traceTransactions: false)` | `postgresql_telemetry_options(transactionSpans: TransactionSpanMode::OFF)` |
93+
| `PostgreSqlTelemetryOptions` 2nd arg `bool $traceTransactions` | `TransactionSpanMode $transactionSpans` |
94+
| `$options->traceTransactions(false)` | `$options->transactionSpans(TransactionSpanMode::OFF)` |
95+
| `$options->traceTransactions` (property) | `$options->transactionSpans` (`TransactionSpanMode`) |
96+
| config `telemetry.trace_transactions: true` | config `telemetry.transaction_spans: grouped` |
97+
| config `telemetry.trace_transactions: false` | config `telemetry.transaction_spans: off` |
98+
99+
`TransactionSpanMode::PER_OPERATION` emits a short span per `BEGIN`/`COMMIT`/`ROLLBACK`; `GROUPED` (default) keeps
100+
the single long-lived transaction span.
101+
102+
The `db.client.operation.duration` metric now uses only low-cardinality dimensions: queries are tagged with
103+
`db.system.name`, `db.namespace`, `db.operation.name`, `db.collection.name`; transactions with `db.system.name`,
104+
`db.namespace`, `db.operation.name` (`begin`/`commit`/`rollback`), `db.transaction.nesting_level`. `db.query.text`,
105+
`db.query.parameter.*`, `server.address` and `db.transaction.savepoint` are no longer metric dimensions.
106+
107+
### 8) `flow-php/symfony-telemetry-bundle` - DBAL spans, metrics and config aligned with the PostgreSQL client
108+
109+
| Before | After |
110+
|------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
111+
| `doctrine.dbal.transaction.begin`/`.commit`/`.rollback` (always) | one `BEGIN TRANSACTION` span per transaction (`grouped`) |
112+
| `doctrine.dbal.connection.exec`/`.query` span names | semconv `{db.operation.name} {db.collection.name}` (e.g. `SELECT users`) |
113+
| `doctrine.dbal.statement.execute`/`.prepare` span names | semconv `{db.operation.name} {db.collection.name}` |
114+
| config `instrumentation.dbal.log_sql` | removed — `db.query.text` is always recorded (bounded by `max_sql_length`) |
115+
|| config `instrumentation.dbal.transaction_spans`: `grouped` (default)/`per_operation`/`off` |
116+
|| config `instrumentation.dbal.collect_metrics` (default `true`) |
117+
|| config `instrumentation.dbal.include_parameters`/`max_parameters`/`max_parameter_length` |
118+
119+
Query spans now carry `db.system.name`, `db.namespace`, `server.address`, `server.port`, `db.operation.name`,
120+
`db.collection.name`, `db.response.returned_rows` and (on error) `db.response.status_code`, and the instrumentation
121+
emits `db.client.operation.duration` and `db.client.response.returned_rows` metrics. If you matched DBAL spans by
122+
their `doctrine.dbal.*` names, switch to the semantic names above.
123+
86124
---
87125

88126
## Upgrading from 0.40.x to 0.41.x

src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/FlowPostgreSqlBundle.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Flow\PostgreSql\Client\Telemetry\PostgreSqlTelemetryConfig;
3232
use Flow\PostgreSql\Client\Telemetry\PostgreSqlTelemetryOptions;
3333
use Flow\PostgreSql\Client\Telemetry\TraceableClient;
34+
use Flow\PostgreSql\Client\Telemetry\TransactionSpanMode;
3435
use Flow\PostgreSql\Migrations\Configuration as MigrationsConfiguration;
3536
use Flow\PostgreSql\Migrations\Executor\MigrationExecutor;
3637
use Flow\PostgreSql\Migrations\Generator\DiffMigrationGenerator;
@@ -188,8 +189,10 @@ public function configure(DefinitionConfigurator $definition): void
188189
->booleanNode('trace_queries')
189190
->defaultTrue()
190191
->end()
191-
->booleanNode('trace_transactions')
192-
->defaultTrue()
192+
->enumNode('transaction_spans')
193+
->info('How transactions are traced: "grouped" (one span holding the queries), "per_operation" (a short span per BEGIN/COMMIT/ROLLBACK), or "off" (no transaction spans)')
194+
->values(['grouped', 'per_operation', 'off'])
195+
->defaultValue('grouped')
193196
->end()
194197
->booleanNode('collect_metrics')
195198
->defaultTrue()
@@ -495,7 +498,7 @@ public function configure(DefinitionConfigurator $definition): void
495498
}
496499

497500
/**
498-
* @param array{connections: array<string, array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, lazy: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}, profiler?: bool}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array<string, array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool}>}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, connection: ?string, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool, drop_if_exists: bool, context?: array<string, mixed>, exclude?: list<array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string}>}, catalog_providers: list<array{catalog_provider_id: ?string, catalog: ?array<string, mixed>}>, profiler?: array{enabled?: bool|null, include_parameters?: bool, migrations?: bool}} $config
501+
* @param array{connections: array<string, array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, lazy: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, transaction_spans: 'grouped'|'per_operation'|'off', collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}, profiler?: bool}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array<string, array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool}>}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, connection: ?string, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool, drop_if_exists: bool, context?: array<string, mixed>, exclude?: list<array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string}>}, catalog_providers: list<array{catalog_provider_id: ?string, catalog: ?array<string, mixed>}>, profiler?: array{enabled?: bool|null, include_parameters?: bool, migrations?: bool}} $config
499502
*/
500503
#[Override]
501504
public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void
@@ -750,7 +753,7 @@ private function registerCatalogProviders(array $catalogProviders, ContainerBuil
750753
}
751754

752755
/**
753-
* @param array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, lazy: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}, profiler?: bool} $connectionConfig
756+
* @param array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, lazy: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, transaction_spans: 'grouped'|'per_operation'|'off', collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}, profiler?: bool} $connectionConfig
754757
*/
755758
private function registerConnection(
756759
string $name,
@@ -1141,13 +1144,13 @@ private function registerStaticConnection(string $name, ContainerBuilder $contai
11411144
}
11421145

11431146
/**
1144-
* @param array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int} $telemetryConfig
1147+
* @param array{service_id: string, clock_service_id: ?string, trace_queries: bool, transaction_spans: 'grouped'|'per_operation'|'off', collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int} $telemetryConfig
11451148
*/
11461149
private function registerTelemetry(string $name, array $telemetryConfig, ContainerBuilder $container): void
11471150
{
11481151
$optionsDef = new Definition(PostgreSqlTelemetryOptions::class, [
11491152
$telemetryConfig['trace_queries'],
1150-
$telemetryConfig['trace_transactions'],
1153+
TransactionSpanMode::from($telemetryConfig['transaction_spans']),
11511154
$telemetryConfig['collect_metrics'],
11521155
$telemetryConfig['log_queries'],
11531156
$telemetryConfig['max_query_length'],

src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/FlowPostgreSqlExtensionTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Flow\PostgreSql\Client\Context;
2626
use Flow\PostgreSql\Client\Telemetry\PostgreSqlTelemetryOptions;
2727
use Flow\PostgreSql\Client\Telemetry\TraceableClient;
28+
use Flow\PostgreSql\Client\Telemetry\TransactionSpanMode;
2829
use Flow\PostgreSql\Migrations\Configuration as MigrationsConfiguration;
2930
use Flow\PostgreSql\Migrations\Executor\MigrationExecutor;
3031
use Flow\PostgreSql\Migrations\Generator\DiffMigrationGenerator;
@@ -1721,7 +1722,7 @@ public function test_telemetry_options_propagated_to_config(): void
17211722
static::assertTrue($options->includeParameters);
17221723
static::assertSame(500, $options->maxQueryLength);
17231724
static::assertTrue($options->traceQueries);
1724-
static::assertTrue($options->traceTransactions);
1725+
static::assertSame(TransactionSpanMode::GROUPED, $options->transactionSpans);
17251726
static::assertTrue($options->collectMetrics);
17261727
}
17271728
}

src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ public function test_telemetry_with_custom_options(): void
725725
'service_id' => 'my.telemetry',
726726
'clock_service_id' => 'my.clock',
727727
'trace_queries' => false,
728-
'trace_transactions' => false,
728+
'transaction_spans' => 'off',
729729
'collect_metrics' => false,
730730
'log_queries' => true,
731731
'max_query_length' => 500,
@@ -741,7 +741,7 @@ public function test_telemetry_with_custom_options(): void
741741
static::assertSame('my.telemetry', $telemetry['service_id']);
742742
static::assertSame('my.clock', $telemetry['clock_service_id']);
743743
static::assertFalse($telemetry['trace_queries']);
744-
static::assertFalse($telemetry['trace_transactions']);
744+
static::assertSame('off', $telemetry['transaction_spans']);
745745
static::assertFalse($telemetry['collect_metrics']);
746746
static::assertTrue($telemetry['log_queries']);
747747
static::assertSame(500, $telemetry['max_query_length']);
@@ -767,7 +767,7 @@ public function test_telemetry_with_defaults(): void
767767
static::assertSame('flow.telemetry', $telemetry['service_id']);
768768
static::assertNull($telemetry['clock_service_id']);
769769
static::assertTrue($telemetry['trace_queries']);
770-
static::assertTrue($telemetry['trace_transactions']);
770+
static::assertSame('grouped', $telemetry['transaction_spans']);
771771
static::assertTrue($telemetry['collect_metrics']);
772772
static::assertFalse($telemetry['log_queries']);
773773
static::assertSame(1000, $telemetry['max_query_length']);

0 commit comments

Comments
 (0)