sqlhooks

repository·master·Indexed 20 days ago

https://github.com/qustavo/sqlhooks

A Go library for attaching hooks to any database/sql driver to instrument SQL statements for tasks such as query logging and execution time measurement. It provides a Hooks interface with Before and After methods, a Wrap function for driver registration, and a Compose function to combine multiple hooks.

Tokens
2.1K
Snippets
6
Records
6
Agent score
70%

What's inside sqlhooks

  1. How sqlhooks works with database/sql drivers

    master

    sqlhooks allows you to instrument SQL statements by attaching hooks to any database/sql driver. This enables logging queries or measuring execution time without modifying your application's core logic.

    To use it, you must:

    1. Implement the sqlhook.Hooks interface by defining Before and After methods.
    2. Register a wrapped driver using sqlhooks.Wrap(driver, hooks).
    3. Open the database using the new driver name you registered.

    The Hooks Interface

    • Before(ctx context.Context, query string, args ...interface{}) (context.Context, error): Executed before the query runs. You can use this to log the query or store a timestamp in the context.Context to measure duration.
    • After(ctx context.Context, query string, args ...interface{}) (context.Context, error): Executed after the query completes. You can retrieve data stored in the context by the Before hook to calculate elapsed time or perform post-query logging.
    // This example shows how to instrument sql queries in order to display the time that they consume
    package main
    
    import (
    	"context"
    	"database/sql"
    	"fmt"
    	"time"
    
    	"github.com/qustavo/sqlhooks/v2"
    	"github.com/mattn/go-sqlite3"
    )
    
    // Hooks satisfies the sqlhook.Hooks interface
    type Hooks struct {}
    
    // Before hook will print the query with it's args and return the context with the timestamp
    func (h *Hooks) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
    	fmt.Printf("> %s %q", query, args)
    	return context.WithValue(ctx, "begin", time.Now()), nil
    }
    
    // After hook will get the timestamp registered on the Before hook and print the elapsed time
    func (h *Hooks) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
    	begin := ctx.Value("begin").(time.Time)
    	fmt.Printf(". took: %s\n", time.Since(begin))
    	return ctx, nil
    }
    
    func main() {
    	// First, register the wrapper
    	sql.Register("sqlite3WithHooks", sqlhooks.Wrap(&sqlite3.SQLiteDriver{}, &Hooks{}))
    
    	// Connect to the registered wrapped driver
    	db, _ := sql.Open("sqlite3WithHooks", ":memory:")
    
    	// Do you're stuff
    	db.Exec("CREATE TABLE t (id INTEGER, text VARCHAR(16))")
    	db.Exec("INSERT into t (text) VALUES(?), (?)", "foo", "bar")
    	db.Query("SELECT id, text FROM t")
    }
  2. Install sqlhooks v2

    master

    To install sqlhooks version 2, use the following command. Note that this version requires Go >= 1.14.x.

    If you need to use older versions (v1), you can fetch them via Go modules or from gopkg.in using:

    go get github.com/qustavo/sqlhooks
    go get gopkg.in/qustavo/sqlhooks.v1
    go get github.com/qustavo/sqlhooks/v2
  3. Implement the Hooks interface to instrument SQL queries

    master

    To instrument database queries (e.g., for logging, tracing, or metrics), implement the Hooks interface. This interface allows you to intercept queries both before they are executed and after they complete.

    • Before(ctx, query, args...): Called before the query is sent to the driver. You can use this to modify the context or abort the query by returning an error.
    • After(ctx, query, args...): Called after the query has been executed. Use this to inspect results or perform post-query logic.

    If you also want to handle errors specifically, implement the OnErrorer interface.

    type MyHooks struct{}
    
    func (h *MyHooks) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
        // Logic before query
        return ctx, nil
    }
    
    func (h *MyHooks) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
        // Logic after query
        return ctx, nil
    }
    
    func (h *MyHooks) OnError(ctx context.Context, err error, query string, args ...interface{}) error {
        // Logic on error
        return nil
    }
  4. Wrap a database driver using Wrap()

    master

    Use the Wrap function to create a new instrumented driver.Driver. This wrapped driver will pass all query executions through your provided Hooks implementation. This is typically used during driver registration with sql.Register.

    import (
        "database/sql"
        "github.com/qustavo/qustavo/sqlhooks"
        "github.com/mattn/go-sqlite3" // Example vendor driver
    )
    
    func init() {
        // Wrap the existing sqlite3 driver with your custom hooks
        sql.Register("sqlite3-instrumented", sqlhooks.Wrap(&sqlite3.SQLiteDriver{}, &MyHooks{}))
    }
    
    func main() {
        db, err := sql.Open("sqlite3-instrumented", "file:memdb1?mode=memory")
        // ...
    }
  5. Compose multiple hooks using Compose()

    master

    The Compose function allows you to combine multiple Hooks into a single Hooks instance. When the resulting composed hook is executed, it runs every callback on every hook in the order they were provided as arguments.

    Key behaviors:

    • Execution Order: Hooks are executed in the order they are passed to Compose.
    • Error Resilience: Even if a previous hook returns an error, subsequent hooks in the chain will still be executed.
    • Error Aggregation: If multiple hooks return errors, the final return value will be of type MultipleErrors, allowing you to introspect all failures.
    // Example of composing multiple hooks
    hooks := sqlhooks.Compose(hook1, hook2, hook3)
    
    // The resulting 'hooks' can be used anywhere a single 'Hooks' is expected.
    ctx, err := hooks.Before(ctx, query, args...)
  6. Handle multiple errors with MultipleErrors

    master

    When using Compose, if more than one hook returns an error during a Before, After, or OnError lifecycle step, the error returned by the composed hook will be a MultipleErrors type. This type is a slice of errors that implements the error interface, allowing you to access the individual error instances.

    type MultipleErrors []error
    
    // You can type-assert to access individual errors
    if err != nil {
    	if multiErr, ok := err.(sqlhooks.MultipleErrors); ok {
    		for _, e := range multiErr {
    			fmt.Println("Individual error:", e)
    		}
    	}
    }