Observability
The ORM exposes structured hooks for every query and mutation so you can ship metrics, traces, and logs without patching driver code.
Query Logging
The simplest way to observe queries is via the built-in query log on OrmConnection or DataSource:
- Notes
- Code
- Use this for development or simple production diagnostics.
- Disable parameter capture for sensitive fields by setting
includeParameters: false.
Future<void> queryLoggingExample() async {
final dataSource = DataSource(
DataSourceOptions(
name: 'primary',
driver: SqliteDriverAdapter.inMemory(),
entities: generatedOrmModelDefinitions,
logging: true, // Enable query logging
),
);
await dataSource.init();
// Execute queries...
await dataSource.query<$User>().get();
// Review the log
for (final entry in dataSource.queryLog) {
print('SQL: ${entry.sql}');
print('Bindings: ${entry.bindings}');
print('Duration: ${entry.duration}');
}
// Clear when done
dataSource.clearQueryLog();
dataSource.disableQueryLog();
}
API Reference
| Method | Description |
|---|---|
enableQueryLog({includeParameters, clear}) | Enables logging; includeParameters controls whether bind values are captured (default: true); clear resets the log (default: true) |
disableQueryLog({clear}) | Disables logging; optionally clears entries |
clearQueryLog() | Removes all accumulated entries |
queryLog | Returns an immutable list of QueryLogEntry objects |
loggingQueries | Returns true when logging is active |
QueryLogEntry Fields
| Field | Description |
|---|---|
type | 'query' or 'mutation' |
sql | The SQL statement with bindings interpolated |
preview | Full StatementPreview with raw SQL and parameter lists |
duration | Execution time |
success | true if no error was thrown |
model | Model name (e.g., 'User') |
table | Table name (e.g., 'users') |
rowCount | Rows returned or affected |
error | Exception object when success is false |
parameters | Bind values (empty when includeParameters is false) |
toMap() | JSON-serializable representation |
Listening to Log Events
Use onQueryLogged to receive entries as they are recorded:
void onQueryLoggedExample(OrmConnection connection) {
connection.onQueryLogged((entry) {
print('SQL ${entry.type}: ${entry.sql}');
});
}
Query & Mutation Events
Register listeners on the QueryContext:
Future<void> queryEventsExample(DataSource dataSource) async {
// Listen for query events
dataSource.onQuery((event) {
print('Query: ${event.sql}');
print('Duration: ${event.duration}ms');
if (event.duration.inMilliseconds > 100) {
print('SLOW QUERY: ${event.sql}');
}
});
// Execute queries
await dataSource.query<$User>().get();
}
QueryEvent Fields
| Field | Description |
|---|---|
plan | QueryPlan executed against the driver |
preview | StatementPreview with SQL text |
duration | Wall-clock Duration for the execution |
rows | Number of rows returned |
error / stackTrace | Populated when the driver threw |
succeeded | true when no error occurred |
MutationEvent Fields
| Field | Description |
|---|---|
plan | MutationPlan (operation, rows, returning flag) |
preview | SQL preview for the mutation |
duration | Execution time |
affectedRows | Driver-reported row count |
error / stackTrace | Failure context |
succeeded | Indicates success |
StructuredQueryLogger
Attach a structured logger for JSON-friendly output:
class StructuredQueryLogger {
void log(QueryLogEntry entry) {
final logEntry = {
'sql': entry.sql,
'bindings': entry.bindings,
'duration_ms': entry.duration.inMilliseconds,
'timestamp': DateTime.now().toIso8601String(),
};
print(logEntry);
}
}
Future<void> structuredLoggerExample(DataSource dataSource) async {
final logger = StructuredQueryLogger();
dataSource.onQuery((event) {
logger.log(event);
});
}
Each entry contains:
{
"type": "query",
"timestamp": "2025-01-01T12:00:00.000Z",
"model": "User",
"table": "users",
"sql": "SELECT \"id\" FROM \"users\" WHERE \"email\" = ?",
"parameters": ["alice@example.com"],
"duration_ms": 1.23,
"success": true,
"env": "prod"
}
For failed queries, the logger adds error_type, error_message, and (optionally) stack_trace.
Printing Helper
For development or when piping logs to stdout:
void printingHelperExample(QueryContext context) {
// For development or when piping logs to stdout:
// StructuredQueryLogger.printing(pretty: true).attach(context);
}
SQL Preview Without Execution
Use Query.toSql() to inspect queries before running them:
Future<void> sqlPreviewExample(DataSource dataSource) async {
// Preview SQL without executing
final query = dataSource
.query<$User>()
.whereEquals('active', true)
.orderBy('name');
final sql = query.toSql();
print('Generated SQL: $sql');
// Then execute if needed
final users = await query.get();
}
Mutation previews are available via repository helpers (previewInsert, previewUpdateMany, etc.) and context.describeMutation().
Connection Instrumentation
OrmConnection provides additional hooks for fine-grained control.
Before Hooks
Intercept queries or mutations before they execute:
Future<void> beforeHooksExample(DataSource dataSource) async {
// Log before queries execute
dataSource.beforeQuery((sql, bindings) {
print('About to execute: $sql');
print('With parameters: $bindings');
});
// Track query counts
var queryCount = 0;
dataSource.beforeQuery((sql, bindings) {
queryCount++;
});
await dataSource.query<$User>().get();
print('Total queries: $queryCount');
}
beforeExecuting
Called just before SQL is sent to the driver, with full statement context:
void beforeExecutingExample(OrmConnection connection) {
final unregister = connection.beforeExecuting((statement) {
print('[SQL] ${statement.sqlWithBindings}');
print('Type: ${statement.type}'); // query or mutation
print('Connection: ${statement.connectionName}');
});
// Later, unregister
unregister();
}
Slow Query Detection
void slowQueryDetectionExample(OrmConnection connection) {
connection.whenQueryingForLongerThan(Duration(milliseconds: 100), (event) {
print('Slow query: ${event.statement.sql}');
print('Duration: ${event.duration.inMilliseconds}ms');
});
}
Pretend Mode
Capture SQL without executing against the database:
Future<void> pretendModeExample(DataSource ds, User user, Post post) async {
final captured = await ds.pretend(() async {
await ds.repo<$User>().insert(user);
await ds.repo<$Post>().insert(post);
});
for (final entry in captured) {
print('Would execute: ${entry.sql}');
}
// No actual database changes made
}
During pretend mode, connection.pretending returns true.
Integration with Tracing
Use event hooks to create tracing spans:
- Notes
- Code
- Use
event.preview.sql/event.durationto label spans consistently. - Prefer a low-cardinality span name (e.g.
db.query) and put SQL in attributes.
void tracingIntegrationExample(QueryContext context, Tracer tracer) {
context.onQuery((event) {
final span = tracer.startSpan('db.query')
..setAttribute('db.statement', event.preview.sql)
..setAttribute('db.system', 'sqlite');
if (event.succeeded) {
span.end();
} else {
span.recordException(event.error!, event.stackTrace);
span.setStatus(SpanStatus.error);
span.end();
}
});
}
// Placeholder tracer interface
abstract class Tracer {
Span startSpan(String name);
}
abstract class Span {
void setAttribute(String key, String value);
void recordException(Object error, StackTrace? stackTrace);
void setStatus(SpanStatus status);
void end();
}
enum SpanStatus { ok, error }
Metrics Integration
Send query metrics to your monitoring service:
void metricsIntegrationExample(QueryContext context, Metrics metrics) {
context.onQuery((event) {
metrics.histogram('db.query.duration', event.duration.inMicroseconds);
metrics.increment('db.query.count');
if (!event.succeeded) {
metrics.increment('db.query.errors');
}
});
context.onMutation((event) {
metrics.histogram('db.mutation.duration', event.duration.inMicroseconds);
metrics.increment('db.mutation.affected_rows', event.affectedRows);
});
}
// Placeholder metrics interface
abstract class Metrics {
void histogram(String name, int value);
void increment(String name, [int value = 1]);
}
Best Practices
-
Attach early – Add event listeners as soon as the
QueryContextis created to capture migrations, seed data, and runtime queries. -
Protect secrets – Disable
includeParametersor scrub entries insideonLogfor columns that may contain PII. -
Correlate with tracing – Use
onQuery/onMutationto start tracing spans using the SQL preview and duration data. -
Monitor slow queries – Register a listener that warns when
event.durationexceeds your SLO.
void slowQueryMonitoringExample(QueryContext context) {
context.onQuery((event) {
if (event.duration > Duration(milliseconds: 100)) {
print('Slow query detected: ${event.preview.sql}');
print('Duration: ${event.duration.inMilliseconds}ms');
}
});
}