loq_drift 0.1.0 copy "loq_drift: ^0.1.0" to clipboard
loq_drift: ^0.1.0 copied to clipboard

Structured query logging interceptor for Drift, powered by loq. Logs SQL statements, durations, row counts, batches, and transactions as structured records aligned with OpenTelemetry database semantic [...]

example/example.dart

// Showcase database exercising every loq_drift capability:
//
//   namespace                  : emit db.namespace on every event
//   skipLog                    : silence PRAGMA / health-check chatter
//   slowQueryThreshold         : flag and bump up slow queries
//   captureArgs                : include bound parameters (PII-aware)
//   fields                     : tag every successful event
//   errorFields                : mark retry-worthiness on failures
//   tableResolver              : populate db.collection.name
//   levelResolver              : custom per-event level logic
//   transaction lifecycle      : begin / commit / rollback at trace
//   database lifecycle         : open / close at trace
//
// Run with: `dart run example/example.dart`
//
// Requires the system sqlite3 library (most macOS/Linux distros ship it).
//
// Drives the raw `QueryExecutor` API so the example compiles without
// running `build_runner`. In a real app you'd hand the wrapped executor
// to your generated `GeneratedDatabase` subclass:
//
// ```dart
// final db = AppDatabase(
//   NativeDatabase.memory().interceptWith(LoqDriftInterceptor()),
// );
// ```

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:loq/loq.dart';
import 'package:loq_drift/loq_drift.dart';

/// Minimal `QueryExecutorUser` so the example can open the executor
/// without a generated database class.
class _NoSchema extends QueryExecutorUser {
  @override
  int get schemaVersion => 1;

  @override
  Future<void> beforeOpen(
    QueryExecutor executor,
    OpeningDetails details,
  ) async {}
}

/// Pull the primary table out of common Drift-generated SQL patterns.
String? _resolveTable(String sql) {
  final m = RegExp(
    r'\b(?:from|into|update)\s+"?(\w+)"?',
    caseSensitive: false,
  ).firstMatch(sql);
  return m?.group(1);
}

Future<void> main() async {
  // Swap ConsoleHandler for JsonHandler() to get one structured JSON
  // object per line (what you'd ship to Datadog/Elastic/Grafana).
  LogConfig.configure(
    handlers: [ConsoleHandler(minLevel: Level.trace)],
  );

  final interceptor = LoqDriftInterceptor(
    // ---- Setup ----
    logger: Logger('db'),
    namespace: ':memory:',

    // ---- Behavior ----
    skipLog: (sql) => sql.startsWith('PRAGMA'),
    slowQueryThreshold: const Duration(milliseconds: 50),

    // captureArgs leaks user-bound values into logs. Safe for dev; in
    // production combine with loq's redact() processor or leave off.
    captureArgs: true,

    // ---- Field hooks ----
    //
    // One hook for every success-path event. Pattern-match on `event`
    // to branch (DriftQueryEvent / DriftBatchEvent /
    // DriftTransactionEvent). Here we just tag every event the same.
    fields: (event) => {
      ...event.defaults,
      'tenant_id': 'acme',
    },
    errorFields: (event, error, stack) => {
      ...event.defaults,
      'db.error.retryable': error.toString().contains('locked'),
    },

    // ---- Resolvers ----
    tableResolver: _resolveTable,

    // Bump SELECTs that touch `users` to info for this demo.
    levelResolver: (event, error) {
      if (error == null &&
          event is DriftQueryEvent &&
          event.operation == 'SELECT' &&
          event.defaults['db.collection.name'] == 'users') {
        return Level.info;
      }
      return null;
    },
  );

  final executor = NativeDatabase.memory().interceptWith(interceptor);
  await executor.ensureOpen(_NoSchema());

  // Skipped by the skip predicate, so no log.
  await executor.runCustom('PRAGMA foreign_keys = ON');

  // Schema bootstrap. Logged as CREATE.
  await executor.runCustom(
    'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)',
  );

  // Single-row insert. sqlite reports the new row id via
  // `loq.db.last_insert_rowid` (`loq.db.affected_rows` on other dialects).
  await executor.runInsert(
    'INSERT INTO users (name, email) VALUES (?, ?)',
    ['Tibor', 'tibor@example.com'],
  );

  // Batch insert: one prepared statement, two argument groups.
  await executor.runBatched(
    BatchedStatements(
      const ['INSERT INTO users (name, email) VALUES (?, ?)'],
      [
        ArgumentsForBatchedStatement(0, ['Ada', 'ada@example.com']),
        ArgumentsForBatchedStatement(0, ['Grace', 'grace@example.com']),
      ],
    ),
  );

  // Read: bumped to info by levelResolver.
  await executor.runSelect('SELECT * FROM users', const []);

  // Transaction: begin, two updates, commit. Lifecycle events at trace;
  // inner queries at debug.
  final tx = executor.beginTransaction();
  await tx.ensureOpen(_NoSchema());
  await tx.runUpdate(
    'UPDATE users SET email = ? WHERE name = ?',
    ['tibor@scandit.com', 'Tibor'],
  );
  await tx.runUpdate(
    'UPDATE users SET email = ? WHERE name = ?',
    ['ada+oss@example.com', 'Ada'],
  );
  await tx.send();

  // Failing query: error log includes error.type, error.message,
  // stackTrace, plus the errorFields-supplied db.error.retryable.
  try {
    await executor.runSelect('SELECT * FROM does_not_exist', const []);
  } on Object catch (_) {
    // swallowed for the demo
  }

  await executor.close();
}
0
likes
150
points
80
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Structured query logging interceptor for Drift, powered by loq. Logs SQL statements, durations, row counts, batches, and transactions as structured records aligned with OpenTelemetry database semantic conventions.

Homepage
Repository (GitHub)
View/report issues

Topics

#logging #drift #sqlite #structured-logging

License

MIT (license)

Dependencies

drift, loq

More

Packages that depend on loq_drift