Skip to main content

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:

  • Use this for development or simple production diagnostics.
  • Disable parameter capture for sensitive fields by setting includeParameters: false.

API Reference

MethodDescription
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
queryLogReturns an immutable list of QueryLogEntry objects
loggingQueriesReturns true when logging is active

QueryLogEntry Fields

FieldDescription
type'query' or 'mutation'
sqlThe SQL statement with bindings interpolated
previewFull StatementPreview with raw SQL and parameter lists
durationExecution time
successtrue if no error was thrown
modelModel name (e.g., 'User')
tableTable name (e.g., 'users')
rowCountRows returned or affected
errorException object when success is false
parametersBind 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

FieldDescription
planQueryPlan executed against the driver
previewStatementPreview with SQL text
durationWall-clock Duration for the execution
rowsNumber of rows returned
error / stackTracePopulated when the driver threw
succeededtrue when no error occurred

MutationEvent Fields

FieldDescription
planMutationPlan (operation, rows, returning flag)
previewSQL preview for the mutation
durationExecution time
affectedRowsDriver-reported row count
error / stackTraceFailure context
succeededIndicates 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:

  • Use event.preview.sql / event.duration to label spans consistently.
  • Prefer a low-cardinality span name (e.g. db.query) and put SQL in attributes.

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 QueryContext is created to capture migrations, seed data, and runtime queries.

  • Protect secrets – Disable includeParameters or scrub entries inside onLog for columns that may contain PII.

  • Correlate with tracing – Use onQuery/onMutation to start tracing spans using the SQL preview and duration data.

  • Monitor slow queries – Register a listener that warns when event.duration exceeds 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');
}
});
}