├── .github └── workflows │ └── reports.yml ├── .gitignore ├── .golangci.yml ├── LICENSE.txt ├── README.md ├── connection.go ├── connection_test.go ├── connector.go ├── connector_test.go ├── database.go ├── database_test.go ├── go.mod ├── go.sum ├── logadapter ├── logrusadapter │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── logger.go │ └── logger_test.go ├── onelogadapter │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── logger.go │ ├── logger_test.go │ └── onelog.jpg ├── zapadapter │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── logger.go │ ├── logger_test.go │ └── zap.jpg └── zerologadapter │ ├── README.md │ ├── console.jpg │ ├── go.mod │ ├── go.sum │ ├── logger.go │ ├── logger_test.go │ └── zerolog.jpg ├── logger.go ├── logger_test.go ├── options.go ├── options_test.go ├── result.go ├── result_test.go ├── rows.go ├── rows_test.go ├── sonar-project.properties ├── statement.go ├── statement_test.go ├── test.sh ├── transaction.go └── transaction_test.go /.github/workflows/reports.yml: -------------------------------------------------------------------------------- 1 | name: Reports 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | permissions: 9 | contents: read 10 | jobs: 11 | reports: 12 | name: Test and Report 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go: [ '1.19', '1.18', '1.17' ] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: ${{ matrix.go }} 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v3 25 | with: 26 | version: v1.50.1 27 | - name: Run Unit Tests 28 | run: ./test.sh 29 | - name: Send coverage 30 | uses: shogo82148/actions-goveralls@v1 31 | with: 32 | path-to-profile: coverage.out 33 | flag-name: Go-${{ matrix.go }} 34 | - name: SonarCloud Scan 35 | uses: SonarSource/sonarcloud-github-action@master 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### NetBeans template 3 | nbproject/private/ 4 | build/ 5 | nbbuild/ 6 | dist/ 7 | nbdist/ 8 | vendor/ 9 | nbactions.xml 10 | .nb-gradle/ 11 | .vscode/ 12 | .scannerwork/ 13 | .DS_Store 14 | ### JetBrains template 15 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 16 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 17 | 18 | # User-specific stuff: 19 | .idea/workspace.xml 20 | .idea/tasks.xml 21 | .idea/dictionaries 22 | .idea/vcs.xml 23 | .idea/jsLibraryMappings.xml 24 | 25 | # Sensitive or high-churn files: 26 | .idea/dataSources.ids 27 | .idea/dataSources.xml 28 | .idea/dataSources.local.xml 29 | .idea/sqlDataSources.xml 30 | .idea/dynamic.xml 31 | .idea/uiDesigner.xml 32 | 33 | # Gradle: 34 | .idea/gradle.xml 35 | .idea/libraries 36 | 37 | # Mongo Explorer plugin: 38 | .idea/mongoSettings.xml 39 | 40 | ## File-based project format: 41 | *.iws 42 | 43 | ## Plugin-specific files: 44 | 45 | # IntelliJ 46 | /out/ 47 | 48 | # mpeltonen/sbt-idea plugin 49 | .idea_modules/ 50 | 51 | # JIRA plugin 52 | atlassian-ide-plugin.xml 53 | 54 | # Crashlytics plugin (for Android Studio and IntelliJ) 55 | com_crashlytics_export_strings.xml 56 | crashlytics.properties 57 | crashlytics-build.properties 58 | fabric.properties 59 | ### Go template 60 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 61 | *.o 62 | *.a 63 | *.so 64 | 65 | # Folders 66 | _obj 67 | _test 68 | 69 | # Architecture specific extensions/prefixes 70 | *.[568vq] 71 | [568vq].out 72 | 73 | *.cgo1.go 74 | *.cgo2.c 75 | _cgo_defun.c 76 | _cgo_gotypes.go 77 | _cgo_export.* 78 | 79 | _testmain.go 80 | 81 | *.exe 82 | *.test 83 | *.prof 84 | ### SublimeText template 85 | # cache files for sublime text 86 | *.tmlanguage.cache 87 | *.tmPreferences.cache 88 | *.stTheme.cache 89 | 90 | # workspace files are user-specific 91 | *.sublime-workspace 92 | 93 | # project files should be checked into the repository, unless a significant 94 | # proportion of contributors will probably not be using SublimeText 95 | # *.sublime-project 96 | 97 | # sftp configuration file 98 | sftp-config.json 99 | 100 | # Package control specific files 101 | Package Control.last-run 102 | Package Control.ca-list 103 | Package Control.ca-bundle 104 | Package Control.system-ca-bundle 105 | Package Control.cache/ 106 | Package Control.ca-certs/ 107 | bh_unicode_properties.cache 108 | 109 | # Sublime-github package stores a github token in this file 110 | # https://packagecontrol.io/packages/sublime-github 111 | GitHub.sublime-settings 112 | 113 | config.json 114 | config.yaml 115 | *.iml 116 | *.bak 117 | .idea 118 | *.ini 119 | *.env 120 | coverage.* 121 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | govet: 3 | check-shadowing: true 4 | golint: 5 | min-confidence: 0 6 | gocyclo: 7 | min-complexity: 15 8 | maligned: 9 | suggest-new: true 10 | goconst: 11 | min-len: 2 12 | min-occurrences: 2 13 | misspell: 14 | locale: US 15 | lll: 16 | line-length: 120 17 | gocritic: 18 | enabled-tags: 19 | - diagnostic 20 | - experimental 21 | - opinionated 22 | - performance 23 | - style 24 | disabled-checks: 25 | - hugeParam 26 | - wrapperFunc 27 | - dupImport # https://github.com/go-critic/go-critic/issues/845 28 | - ifElseChain 29 | - octalLiteral 30 | funlen: 31 | lines: 100 32 | statements: 50 33 | 34 | linters: # don't use --enable-all 35 | disable-all: true 36 | enable: 37 | - bodyclose 38 | - deadcode 39 | - depguard 40 | - dogsled 41 | - dupl 42 | - errcheck 43 | - funlen 44 | - gochecknoinits 45 | - goconst 46 | - gocritic 47 | - gocyclo 48 | - gofmt 49 | - goimports 50 | - golint 51 | - gosec 52 | - gosimple 53 | - govet 54 | - ineffassign 55 | - interfacer 56 | - lll 57 | - misspell 58 | - nakedret 59 | - scopelint 60 | - staticcheck 61 | - structcheck 62 | - stylecheck 63 | - typecheck 64 | - unconvert 65 | - unparam 66 | - unused 67 | - varcheck 68 | - whitespace 69 | - maligned 70 | 71 | run: 72 | skip-dirs: 73 | - vendor/ 74 | skip-files: 75 | - ./*_test.go 76 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2020 Sarjono Mukti Aji 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLDB-Logger 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/simukti/sqldb-logger/badge.svg)](https://coveralls.io/github/simukti/sqldb-logger) [![Go Report Card](https://goreportcard.com/badge/github.com/simukti/sqldb-logger)](https://goreportcard.com/report/github.com/simukti/sqldb-logger) [![Sonar Violations (long format)](https://img.shields.io/sonar/violations/simukti_sqldb-logger?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=simukti_sqldb-logger) [![Sonar Tech Debt](https://img.shields.io/sonar/tech_debt/simukti_sqldb-logger?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=simukti_sqldb-logger) [![Sonar Quality Gate](https://img.shields.io/sonar/quality_gate/simukti_sqldb-logger?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/dashboard?id=simukti_sqldb-logger) [![Documentation](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/simukti/sqldb-logger) [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/simukti/sqldb-logger/master/LICENSE.txt) 4 | 5 | A logger for Go SQL database driver without modify existing `*sql.DB` stdlib usage. 6 | 7 | ![shameless console output sample](./logadapter/zerologadapter/console.jpg?raw=true "go sql database logger output") 8 | _Colored console writer output above only for sample/development_ 9 | 10 | ## FEATURES 11 | 12 | - Leveled, detailed and [configurable](./options.go) logging. 13 | - Keep using (or re-use existing) `*sql.DB` as is. 14 | - Bring your own logger backend via simple log interface. 15 | - Trackable log output: 16 | - Every call has its own unique ID. 17 | - Prepared statement and execution will have same ID. 18 | - On execution/result error, it will include the query, arguments, params, and related IDs. 19 | 20 | ## INSTALL 21 | 22 | ```bash 23 | go get -u -v github.com/simukti/sqldb-logger 24 | ``` 25 | 26 | _Version pinning using dependency manager such as [Mod](https://github.com/golang/go/wiki/Modules) or [Dep](https://github.com/golang/dep) is highly recommended._ 27 | 28 | ## USAGE 29 | 30 | As a start, `Logger` is just a simple interface: 31 | 32 | ```go 33 | type Logger interface { 34 | Log(ctx context.Context, level Level, msg string, data map[string]interface{}) 35 | } 36 | ``` 37 | 38 | There are 4 included basic implementation that uses well-known JSON structured logger for quickstart: 39 | 40 | - [Zerolog adapter](logadapter/zerologadapter): Using [rs/zerolog](https://github.com/rs/zerolog) as its logger. 41 | - [Onelog adapter](logadapter/onelogadapter): Using [francoispqt/onelog](https://github.com/francoispqt/onelog) as its logger. 42 | - [Zap adapter](logadapter/zapadapter): Using [uber-go/zap](https://github.com/uber-go/zap) as its logger. 43 | - [Logrus adapter](logadapter/logrusadapter): Using [sirupsen/logrus](https://github.com/sirupsen/logrus) as its logger. 44 | 45 | _Note: [those adapters](./logadapter) does not use given `context`, you need to modify it and adjust with your needs._ 46 | _(example: add http request id/whatever value from context to query log when you call `QueryerContext` and`ExecerContext` methods)_ 47 | 48 | Then for that logger to works, you need to integrate with a compatible driver which will be used by `*sql.DB`. 49 | 50 | ### INTEGRATE WITH EXISTING SQL DB DRIVER 51 | 52 | Re-use from existing `*sql.DB` driver, this is the simplest way: 53 | 54 | For example, from: 55 | 56 | ```go 57 | dsn := "username:passwd@tcp(mysqlserver:3306)/dbname?parseTime=true" 58 | db, err := sql.Open("mysql", dsn) // db is *sql.DB 59 | db.Ping() // to check connectivity and DSN correctness 60 | ``` 61 | 62 | To: 63 | 64 | ```go 65 | // import sqldblogger "github.com/simukti/sqldb-logger" 66 | // import "github.com/simukti/sqldb-logger/logadapter/zerologadapter" 67 | dsn := "username:passwd@tcp(mysqlserver:3306)/dbname?parseTime=true" 68 | db, err := sql.Open("mysql", dsn) // db is *sql.DB 69 | // handle err 70 | loggerAdapter := zerologadapter.New(zerolog.New(os.Stdout)) 71 | db = sqldblogger.OpenDriver(dsn, db.Driver(), loggerAdapter/*, using_default_options*/) // db is STILL *sql.DB 72 | db.Ping() // to check connectivity and DSN correctness 73 | ``` 74 | 75 | That's it, all `*sql.DB` interaction now logged. 76 | 77 | ### INTEGRATE WITH SQL DRIVER STRUCT 78 | 79 | It is also possible to integrate with following public empty struct driver directly: 80 | 81 | #### MySQL ([go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)) 82 | 83 | ```go 84 | db := sqldblogger.OpenDriver(dsn, &mysql.MySQLDriver{}, loggerAdapter /*, ...options */) 85 | ``` 86 | 87 | #### PostgreSQL ([lib/pq](https://github.com/lib/pq)) 88 | 89 | ```go 90 | db := sqldblogger.OpenDriver(dsn, &pq.Driver{}, loggerAdapter /*, ...options */) 91 | ``` 92 | 93 | #### SQLite3 ([mattn/go-sqlite3](https://github.com/mattn/go-sqlite3)) 94 | 95 | ```go 96 | db := sqldblogger.OpenDriver(dsn, &sqlite3.SQLiteDriver{}, loggerAdapter /*, ...options */) 97 | ``` 98 | 99 | _Following struct drivers **maybe** compatible:_ 100 | 101 | #### SQL Server ([denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb)) 102 | 103 | ```go 104 | db := sqldblogger.OpenDriver(dsn, &mssql.Driver{}, loggerAdapter /*, ...options */) 105 | ``` 106 | 107 | #### Oracle ([mattn/go-oci8](https://github.com/mattn/go-oci8)) 108 | 109 | ```go 110 | db := sqldblogger.OpenDriver(dsn, oci8.OCI8Driver, loggerAdapter /*, ...options */) 111 | ``` 112 | 113 | ## LOGGER OPTIONS 114 | 115 | When using `sqldblogger.OpenDriver(dsn, driver, logger, opt...)` without 4th variadic argument, it will use [default options](./options.go#L37-L59). 116 | 117 | Here is sample of `OpenDriver()` using all available options and use non-default value: 118 | 119 | ```go 120 | db = sqldblogger.OpenDriver( 121 | dsn, 122 | db.Driver(), 123 | loggerAdapter, 124 | // AVAILABLE OPTIONS 125 | sqldblogger.WithErrorFieldname("sql_error"), // default: error 126 | sqldblogger.WithDurationFieldname("query_duration"), // default: duration 127 | sqldblogger.WithTimeFieldname("log_time"), // default: time 128 | sqldblogger.WithSQLQueryFieldname("sql_query"), // default: query 129 | sqldblogger.WithSQLArgsFieldname("sql_args"), // default: args 130 | sqldblogger.WithMinimumLevel(sqldblogger.LevelTrace), // default: LevelDebug 131 | sqldblogger.WithLogArguments(false), // default: true 132 | sqldblogger.WithDurationUnit(sqldblogger.DurationNanosecond), // default: DurationMillisecond 133 | sqldblogger.WithTimeFormat(sqldblogger.TimeFormatRFC3339), // default: TimeFormatUnix 134 | sqldblogger.WithLogDriverErrorSkip(true), // default: false 135 | sqldblogger.WithSQLQueryAsMessage(true), // default: false 136 | sqldblogger.WithUIDGenerator(sqldblogger.UIDGenerator), // default: *defaultUID 137 | sqldblogger.WithConnectionIDFieldname("con_id"), // default: conn_id 138 | sqldblogger.WithStatementIDFieldname("stm_id"), // default: stmt_id 139 | sqldblogger.WithTransactionIDFieldname("trx_id"), // default: tx_id 140 | sqldblogger.WithWrapResult(false), // default: true 141 | sqldblogger.WithIncludeStartTime(true), // default: false 142 | sqldblogger.WithStartTimeFieldname("start_time"), // default: start 143 | sqldblogger.WithPreparerLevel(sqldblogger.LevelDebug), // default: LevelInfo 144 | sqldblogger.WithQueryerLevel(sqldblogger.LevelDebug), // default: LevelInfo 145 | sqldblogger.WithExecerLevel(sqldblogger.LevelDebug), // default: LevelInfo 146 | ) 147 | ``` 148 | 149 | [Click here](https://pkg.go.dev/github.com/simukti/sqldb-logger#Option) for options documentation. 150 | 151 | ## MOTIVATION 152 | 153 | I want to: 154 | 155 | - Keep using `*sql.DB`. 156 | - Have configurable output field. 157 | - Leverage structured logging. 158 | - Fetch and log `context.Context` value if needed. 159 | - Re-use [pgx log interface](https://github.com/jackc/pgx/blob/f3a3ee1a0e5c8fc8991928bcd06fdbcd1ee9d05c/logger.go#L46-L49). 160 | 161 | I haven't found Go `*sql.DB` logger with that features, so why not created myself? 162 | 163 | ## REFERENCES 164 | 165 | - [Stdlib sql.DB](https://github.com/golang/go/blob/master/src/database/sql/sql.go) 166 | - [SQL driver interfaces](https://github.com/golang/go/blob/master/src/database/sql/driver/driver.go) 167 | - [SQL driver implementation](https://github.com/golang/go/wiki/SQLDrivers) 168 | 169 | ## CONTRIBUTE 170 | 171 | If you found a bug, typo, wrong test, idea, help with existing issue, or anything constructive. 172 | 173 | Don't hesitate to create an issue or pull request. 174 | 175 | ## CREDITS 176 | 177 | - [pgx](https://github.com/jackc/pgx) for awesome PostgreSQL driver. 178 | 179 | ## LICENSE 180 | 181 | [MIT](./LICENSE.txt) -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "time" 7 | ) 8 | 9 | // connection is a database connection wrapper which implements following interfaces: 10 | // - driver.Conn 11 | // - driver.ConnBeginTx 12 | // - driver.ConnPrepareContext 13 | // - driver.Pinger 14 | // - driver.Execer 15 | // - driver.ExecerContext 16 | // - driver.Queryer 17 | // - driver.QueryerContext 18 | // - driver.SessionResetter 19 | // - driver.NamedValueChecker 20 | type connection struct { 21 | driver.Conn 22 | id string 23 | logger *logger 24 | } 25 | 26 | // Begin implements driver.Conn 27 | func (c *connection) Begin() (driver.Tx, error) { 28 | lvl, start, id := LevelDebug, time.Now(), c.logger.opt.uidGenerator.UniqueID() 29 | logs := append(c.logData(), c.logger.withUID(c.logger.opt.txIDFieldname, id)) 30 | connTx, err := c.Conn.Begin() // nolint // disable static check on deprecated driver method 31 | 32 | if err != nil { 33 | lvl = LevelError 34 | } 35 | 36 | c.logger.log(context.Background(), lvl, "Begin", start, err, logs...) 37 | 38 | return c.transaction(connTx, err, id) 39 | } 40 | 41 | // Prepare implements driver.Conn 42 | func (c *connection) Prepare(query string) (driver.Stmt, error) { 43 | lvl, start, id := c.logger.opt.preparerLevel, time.Now(), c.logger.opt.uidGenerator.UniqueID() 44 | logs := append(c.logData(), c.logger.withQuery(query), c.logger.withUID(c.logger.opt.stmtIDFieldname, id)) 45 | driverStmt, err := c.Conn.Prepare(query) 46 | 47 | if err != nil { 48 | lvl = LevelError 49 | } 50 | 51 | c.logger.log(context.Background(), lvl, "Prepare", start, err, logs...) 52 | 53 | return c.statement(driverStmt, err, id, query) 54 | } 55 | 56 | // Prepare implements driver.Conn 57 | func (c *connection) Close() error { 58 | lvl, start := LevelDebug, time.Now() 59 | err := c.Conn.Close() 60 | 61 | if err != nil { 62 | lvl = LevelError 63 | } 64 | 65 | c.logger.log(context.Background(), lvl, "Close", start, err, c.logData()...) 66 | 67 | return err 68 | } 69 | 70 | // BeginTx implements driver.ConnBeginTx 71 | func (c *connection) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { 72 | drvTx, ok := c.Conn.(driver.ConnBeginTx) 73 | if !ok { 74 | return nil, driver.ErrSkip 75 | } 76 | 77 | lvl, start, id := LevelDebug, time.Now(), c.logger.opt.uidGenerator.UniqueID() 78 | logs := append(c.logData(), c.logger.withUID(c.logger.opt.txIDFieldname, id)) 79 | connTx, err := drvTx.BeginTx(ctx, opts) 80 | 81 | if err != nil { 82 | lvl = LevelError 83 | } 84 | 85 | c.logger.log(ctx, lvl, "BeginTx", start, err, logs...) 86 | 87 | return c.transaction(connTx, err, id) 88 | } 89 | 90 | // PrepareContext implements driver.ConnPrepareContext 91 | func (c *connection) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { 92 | driverPrep, ok := c.Conn.(driver.ConnPrepareContext) 93 | if !ok { 94 | return nil, driver.ErrSkip 95 | } 96 | 97 | lvl, start, id := c.logger.opt.preparerLevel, time.Now(), c.logger.opt.uidGenerator.UniqueID() 98 | logs := append(c.logData(), c.logger.withQuery(query), c.logger.withUID(c.logger.opt.stmtIDFieldname, id)) 99 | driverStmt, err := driverPrep.PrepareContext(ctx, query) 100 | 101 | if err != nil { 102 | lvl = LevelError 103 | } 104 | 105 | c.logger.log(ctx, lvl, "PrepareContext", start, err, logs...) 106 | 107 | return c.statement(driverStmt, err, id, query) 108 | } 109 | 110 | // Ping implements driver.Pinger 111 | func (c *connection) Ping(ctx context.Context) error { 112 | driverPinger, ok := c.Conn.(driver.Pinger) 113 | if !ok { 114 | return driver.ErrSkip 115 | } 116 | 117 | lvl, start := LevelDebug, time.Now() 118 | err := driverPinger.Ping(ctx) 119 | 120 | if err != nil { 121 | lvl = LevelError 122 | } 123 | 124 | c.logger.log(ctx, lvl, "Ping", start, err, c.logData()...) 125 | 126 | return err 127 | } 128 | 129 | // Exec implements driver.Execer 130 | // Deprecated: use ExecContext() instead 131 | func (c *connection) Exec(query string, args []driver.Value) (driver.Result, error) { 132 | driverExecer, ok := c.Conn.(driver.Execer) // nolint // disable static check on deprecated driver method 133 | if !ok { 134 | return nil, driver.ErrSkip 135 | } 136 | 137 | logs := append(c.logData(), c.logger.withQuery(query), c.logger.withArgs(args)) 138 | lvl, start := c.logger.opt.execerLevel, time.Now() 139 | res, err := driverExecer.Exec(query, args) 140 | 141 | if err != nil { 142 | lvl = LevelError 143 | } 144 | 145 | c.logger.log(context.Background(), lvl, "Exec", start, err, logs...) 146 | 147 | return c.result(res, err, query, args) 148 | } 149 | 150 | // ExecContext implements driver.ExecerContext 151 | func (c *connection) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { 152 | driverExecerContext, ok := c.Conn.(driver.ExecerContext) 153 | if !ok { 154 | return nil, driver.ErrSkip 155 | } 156 | 157 | logArgs := namedValuesToValues(args) 158 | logs := append(c.logData(), c.logger.withQuery(query), c.logger.withArgs(logArgs)) 159 | lvl, start := c.logger.opt.execerLevel, time.Now() 160 | res, err := driverExecerContext.ExecContext(ctx, query, args) 161 | 162 | if err != nil { 163 | lvl = LevelError 164 | } 165 | 166 | c.logger.log(ctx, lvl, "ExecContext", start, err, logs...) 167 | 168 | return c.result(res, err, query, logArgs) 169 | } 170 | 171 | // Query implements driver.Queryer 172 | // Deprecated: use QueryContext() instead 173 | func (c *connection) Query(query string, args []driver.Value) (driver.Rows, error) { 174 | driverQueryer, ok := c.Conn.(driver.Queryer) // nolint // disable static check on deprecated driver method 175 | if !ok { 176 | return nil, driver.ErrSkip 177 | } 178 | 179 | logs := append(c.logData(), c.logger.withQuery(query), c.logger.withArgs(args)) 180 | lvl, start := c.logger.opt.queryerLevel, time.Now() 181 | res, err := driverQueryer.Query(query, args) 182 | 183 | if err != nil { 184 | lvl = LevelError 185 | } 186 | 187 | c.logger.log(context.Background(), lvl, "Query", start, err, logs...) 188 | 189 | return c.rows(res, err, query, args) 190 | } 191 | 192 | // QueryContext implements driver.QueryerContext 193 | func (c *connection) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { 194 | driverQueryerContext, ok := c.Conn.(driver.QueryerContext) 195 | if !ok { 196 | return nil, driver.ErrSkip 197 | } 198 | 199 | logArgs := namedValuesToValues(args) 200 | logs := append(c.logData(), c.logger.withQuery(query), c.logger.withArgs(logArgs)) 201 | lvl, start := c.logger.opt.queryerLevel, time.Now() 202 | res, err := driverQueryerContext.QueryContext(ctx, query, args) 203 | 204 | if err != nil { 205 | lvl = LevelError 206 | } 207 | 208 | c.logger.log(ctx, lvl, "QueryContext", start, err, logs...) 209 | 210 | return c.rows(res, err, query, logArgs) 211 | } 212 | 213 | // ResetSession implements driver.SessionResetter 214 | func (c *connection) ResetSession(ctx context.Context) error { 215 | resetter, ok := c.Conn.(driver.SessionResetter) 216 | if !ok { 217 | return driver.ErrSkip 218 | } 219 | 220 | lvl, start := LevelTrace, time.Now() 221 | err := resetter.ResetSession(ctx) 222 | 223 | if err != nil { 224 | lvl = LevelError 225 | } 226 | 227 | c.logger.log(context.Background(), lvl, "ResetSession", start, err, c.logData()...) 228 | 229 | return err 230 | } 231 | 232 | // CheckNamedValue implements driver.NamedValueChecker 233 | func (c *connection) CheckNamedValue(nm *driver.NamedValue) error { 234 | checker, ok := c.Conn.(driver.NamedValueChecker) 235 | if !ok { 236 | return driver.ErrSkip 237 | } 238 | 239 | lvl, start := LevelTrace, time.Now() 240 | err := checker.CheckNamedValue(nm) 241 | 242 | if err != nil { 243 | lvl = LevelError 244 | } 245 | 246 | c.logger.log(context.Background(), lvl, "CheckNamedValue", start, err, c.logData()...) 247 | 248 | return err 249 | } 250 | 251 | func (c *connection) transaction(tx driver.Tx, err error, id string) (driver.Tx, error) { 252 | if err != nil { 253 | return tx, err 254 | } 255 | 256 | return &transaction{Tx: tx, logger: c.logger, connID: c.id, id: id}, nil 257 | } 258 | 259 | func (c *connection) statement(stmt driver.Stmt, err error, id, query string) (driver.Stmt, error) { 260 | if err != nil { 261 | return stmt, err 262 | } 263 | 264 | return &statement{Stmt: stmt, query: query, logger: c.logger, connID: c.id, id: id}, nil 265 | } 266 | 267 | func (c *connection) rows(res driver.Rows, err error, query string, args []driver.Value) (driver.Rows, error) { 268 | if !c.logger.opt.wrapResult || err != nil { 269 | return res, err 270 | } 271 | 272 | return &rows{Rows: res, logger: c.logger, connID: c.id, query: query, args: args}, nil 273 | } 274 | 275 | func (c *connection) result(res driver.Result, err error, query string, args []driver.Value) (driver.Result, error) { 276 | if !c.logger.opt.wrapResult || err != nil { 277 | return res, err 278 | } 279 | 280 | return &result{Result: res, logger: c.logger, connID: c.id, query: query, args: args}, nil 281 | } 282 | 283 | // logData default log data for connection. 284 | func (c *connection) logData() []dataFunc { 285 | return []dataFunc{ 286 | c.logger.withUID(c.logger.opt.connIDFieldname, c.id), 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /connection_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | var ( 14 | testOpts = &options{} 15 | bufLogger = &bufferTestLogger{} 16 | testLogger *logger 17 | ) 18 | 19 | func init() { 20 | setDefaultOptions(testOpts) 21 | testOpts.minimumLogLevel = LevelDebug 22 | testLogger = &logger{ 23 | logger: bufLogger, 24 | opt: testOpts, 25 | } 26 | } 27 | 28 | func TestConnection_Begin(t *testing.T) { 29 | t.Run("Error", func(t *testing.T) { 30 | driverConnMock := &driverConnMock{} 31 | var txMock *transactionMock 32 | driverConnMock.On("Begin").Return(txMock, driver.ErrBadConn) 33 | 34 | id := testLogger.opt.uidGenerator.UniqueID() 35 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: id} 36 | _, err := conn.Begin() 37 | assert.Error(t, err) 38 | 39 | var output bufLog 40 | err = json.Unmarshal(bufLogger.Bytes(), &output) 41 | assert.NoError(t, err) 42 | assert.Equal(t, LevelError.String(), output.Level) 43 | assert.Equal(t, id, output.Data[testOpts.connIDFieldname]) 44 | }) 45 | 46 | t.Run("Success", func(t *testing.T) { 47 | driverConnMock := &driverConnMock{} 48 | txMock := &transactionMock{} 49 | driverConnMock.On("Begin").Return(txMock, nil) 50 | 51 | id := testLogger.opt.uidGenerator.UniqueID() 52 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: id} 53 | tx, err := conn.Begin() 54 | assert.NoError(t, err) 55 | assert.Implements(t, (*driver.Tx)(nil), tx) 56 | 57 | var output bufLog 58 | err = json.Unmarshal(bufLogger.Bytes(), &output) 59 | assert.NoError(t, err) 60 | assert.Equal(t, "Begin", output.Message) 61 | assert.Equal(t, LevelDebug.String(), output.Level) 62 | assert.Equal(t, id, output.Data[testOpts.connIDFieldname]) 63 | }) 64 | } 65 | 66 | func TestConnection_Prepare(t *testing.T) { 67 | t.Run("Error", func(t *testing.T) { 68 | driverConnMock := &driverConnMock{} 69 | var stmtMock *statementMock 70 | driverConnMock.On("Prepare", mock.Anything).Return(stmtMock, driver.ErrBadConn) 71 | q := "SELECT * FROM tt WHERE id = ?" 72 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 73 | _, err := conn.Prepare(q) 74 | assert.Error(t, err) 75 | 76 | var output bufLog 77 | err = json.Unmarshal(bufLogger.Bytes(), &output) 78 | assert.NoError(t, err) 79 | assert.Equal(t, "Prepare", output.Message) 80 | assert.Equal(t, LevelError.String(), output.Level) 81 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 82 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 83 | }) 84 | 85 | t.Run("Success", func(t *testing.T) { 86 | driverConnMock := &driverConnMock{} 87 | stmtMock := &statementMock{} 88 | driverConnMock.On("Prepare", mock.Anything).Return(stmtMock, nil) 89 | q := "SELECT * FROM tt WHERE id = ?" 90 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 91 | stmt, err := conn.Prepare(q) 92 | assert.NoError(t, err) 93 | assert.Implements(t, (*driver.Stmt)(nil), stmt) 94 | 95 | var output bufLog 96 | err = json.Unmarshal(bufLogger.Bytes(), &output) 97 | assert.NoError(t, err) 98 | assert.Equal(t, "Prepare", output.Message) 99 | assert.Equal(t, LevelInfo.String(), output.Level) 100 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 101 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 102 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 103 | }) 104 | 105 | t.Run("Success with custom level", func(t *testing.T) { 106 | driverConnMock := &driverConnMock{} 107 | stmtMock := &statementMock{} 108 | driverConnMock.On("Prepare", mock.Anything).Return(stmtMock, nil) 109 | q := "SELECT * FROM tt WHERE id = ?" 110 | 111 | custOpt := *testOpts 112 | WithPreparerLevel(LevelDebug)(&custOpt) 113 | custLogger := *testLogger 114 | custLogger.opt = &custOpt 115 | 116 | conn := &connection{Conn: driverConnMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID()} 117 | stmt, err := conn.Prepare(q) 118 | assert.NoError(t, err) 119 | assert.Implements(t, (*driver.Stmt)(nil), stmt) 120 | 121 | var output bufLog 122 | err = json.Unmarshal(bufLogger.Bytes(), &output) 123 | assert.NoError(t, err) 124 | assert.Equal(t, "Prepare", output.Message) 125 | assert.Equal(t, LevelDebug.String(), output.Level) 126 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 127 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 128 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 129 | }) 130 | } 131 | 132 | func TestConnection_Close(t *testing.T) { 133 | t.Run("Error", func(t *testing.T) { 134 | driverConnMock := &driverConnMock{} 135 | driverConnMock.On("Close").Return(driver.ErrBadConn) 136 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 137 | err := conn.Close() 138 | assert.Error(t, err) 139 | 140 | var output bufLog 141 | err = json.Unmarshal(bufLogger.Bytes(), &output) 142 | assert.NoError(t, err) 143 | assert.Equal(t, "Close", output.Message) 144 | assert.Equal(t, LevelError.String(), output.Level) 145 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 146 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 147 | }) 148 | 149 | t.Run("Success", func(t *testing.T) { 150 | driverConnMock := &driverConnMock{} 151 | driverConnMock.On("Close").Return(nil) 152 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 153 | err := conn.Close() 154 | assert.NoError(t, err) 155 | 156 | var output bufLog 157 | err = json.Unmarshal(bufLogger.Bytes(), &output) 158 | assert.NoError(t, err) 159 | assert.Equal(t, "Close", output.Message) 160 | assert.Equal(t, LevelDebug.String(), output.Level) 161 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 162 | }) 163 | } 164 | 165 | func TestConnection_BeginTx(t *testing.T) { 166 | t.Run("Non driver.ConnBeginTx", func(t *testing.T) { 167 | driverConnMock := &driverConnMock{} 168 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 169 | _, err := conn.BeginTx(context.TODO(), driver.TxOptions{ 170 | Isolation: 1, 171 | ReadOnly: true, 172 | }) 173 | assert.Error(t, err) 174 | assert.Equal(t, driver.ErrSkip, err) 175 | }) 176 | 177 | t.Run("With driver.ConnBeginTx Error", func(t *testing.T) { 178 | driverConnMock := &driverConnWithContextMock{} 179 | var txMock *transactionMock 180 | driverConnMock.On("BeginTx", mock.Anything, mock.Anything).Return(txMock, driver.ErrBadConn) 181 | 182 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 183 | _, err := conn.BeginTx(context.TODO(), driver.TxOptions{ 184 | Isolation: 1, 185 | ReadOnly: true, 186 | }) 187 | assert.Error(t, err) 188 | 189 | var output bufLog 190 | err = json.Unmarshal(bufLogger.Bytes(), &output) 191 | assert.NoError(t, err) 192 | assert.Equal(t, "BeginTx", output.Message) 193 | assert.Equal(t, LevelError.String(), output.Level) 194 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 195 | assert.NotEmpty(t, output.Data[testOpts.txIDFieldname]) 196 | }) 197 | 198 | t.Run("With driver.ConnBeginTx Success", func(t *testing.T) { 199 | driverConnMock := &driverConnWithContextMock{} 200 | txMock := &transactionMock{} 201 | driverConnMock.On("BeginTx", mock.Anything, mock.Anything).Return(txMock, nil) 202 | 203 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 204 | tx, err := conn.BeginTx(context.TODO(), driver.TxOptions{ 205 | Isolation: 1, 206 | ReadOnly: true, 207 | }) 208 | assert.NoError(t, err) 209 | assert.Implements(t, (*driver.Tx)(nil), tx) 210 | 211 | var output bufLog 212 | err = json.Unmarshal(bufLogger.Bytes(), &output) 213 | assert.NoError(t, err) 214 | assert.Equal(t, "BeginTx", output.Message) 215 | assert.Equal(t, LevelDebug.String(), output.Level) 216 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 217 | assert.NotEmpty(t, output.Data[testOpts.txIDFieldname]) 218 | }) 219 | } 220 | 221 | func TestConnection_PrepareContext(t *testing.T) { 222 | t.Run("Non driver.ConnPrepareContext", func(t *testing.T) { 223 | driverConnMock := &driverConnMock{} 224 | 225 | q := "SELECT * FROM tt WHERE id = ?" 226 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 227 | _, err := conn.PrepareContext(context.TODO(), q) 228 | assert.Error(t, err) 229 | assert.Equal(t, driver.ErrSkip, err) 230 | }) 231 | 232 | t.Run("With driver.ConnPrepareContext Error", func(t *testing.T) { 233 | driverConnMock := &driverConnWithContextMock{} 234 | var stmtMock *statementMock 235 | driverConnMock.On("PrepareContext", mock.Anything).Return(stmtMock, driver.ErrBadConn) 236 | 237 | q := "SELECT * FROM tt WHERE id = ?" 238 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 239 | _, err := conn.PrepareContext(context.TODO(), q) 240 | assert.Error(t, err) 241 | 242 | var output bufLog 243 | err = json.Unmarshal(bufLogger.Bytes(), &output) 244 | assert.NoError(t, err) 245 | assert.Equal(t, "PrepareContext", output.Message) 246 | assert.Equal(t, LevelError.String(), output.Level) 247 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 248 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 249 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 250 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 251 | }) 252 | 253 | t.Run("With driver.ConnBeginTx Success", func(t *testing.T) { 254 | driverConnMock := &driverConnWithContextMock{} 255 | stmtMock := &statementMock{} 256 | driverConnMock.On("PrepareContext", mock.Anything).Return(stmtMock, nil) 257 | 258 | q := "SELECT * FROM tt WHERE id = ?" 259 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 260 | stmt, err := conn.PrepareContext(context.TODO(), q) 261 | assert.NoError(t, err) 262 | assert.Implements(t, (*driver.Stmt)(nil), stmt) 263 | 264 | var output bufLog 265 | err = json.Unmarshal(bufLogger.Bytes(), &output) 266 | assert.NoError(t, err) 267 | assert.Equal(t, "PrepareContext", output.Message) 268 | assert.Equal(t, LevelInfo.String(), output.Level) 269 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 270 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 271 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 272 | }) 273 | 274 | t.Run("With driver.ConnBeginTx Success and custom preparer level", func(t *testing.T) { 275 | driverConnMock := &driverConnWithContextMock{} 276 | stmtMock := &statementMock{} 277 | driverConnMock.On("PrepareContext", mock.Anything).Return(stmtMock, nil) 278 | 279 | custOpt := *testOpts 280 | WithPreparerLevel(LevelDebug)(&custOpt) 281 | custLogger := *testLogger 282 | custLogger.opt = &custOpt 283 | 284 | q := "SELECT * FROM tt WHERE id = ?" 285 | conn := &connection{Conn: driverConnMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID()} 286 | stmt, err := conn.PrepareContext(context.TODO(), q) 287 | assert.NoError(t, err) 288 | assert.Implements(t, (*driver.Stmt)(nil), stmt) 289 | 290 | var output bufLog 291 | err = json.Unmarshal(bufLogger.Bytes(), &output) 292 | assert.NoError(t, err) 293 | assert.Equal(t, "PrepareContext", output.Message) 294 | assert.Equal(t, LevelDebug.String(), output.Level) 295 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 296 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 297 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 298 | }) 299 | } 300 | 301 | func TestConnection_Ping(t *testing.T) { 302 | t.Run("Non driver.Pinger", func(t *testing.T) { 303 | driverConnMock := &driverConnMock{} 304 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 305 | err := conn.Ping(context.TODO()) 306 | assert.Error(t, err) 307 | assert.Equal(t, driver.ErrSkip, err) 308 | }) 309 | 310 | t.Run("driver.Pinger With Error", func(t *testing.T) { 311 | driverConnMock := &driverConnPingerMock{} 312 | driverConnMock.On("Ping", mock.Anything).Return(driver.ErrBadConn) 313 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 314 | err := conn.Ping(context.TODO()) 315 | assert.Error(t, err) 316 | 317 | var output bufLog 318 | err = json.Unmarshal(bufLogger.Bytes(), &output) 319 | assert.NoError(t, err) 320 | assert.Equal(t, "Ping", output.Message) 321 | assert.Equal(t, LevelError.String(), output.Level) 322 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 323 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 324 | }) 325 | 326 | t.Run("driver.Pinger Success", func(t *testing.T) { 327 | driverConnMock := &driverConnPingerMock{} 328 | driverConnMock.On("Ping", mock.Anything).Return(nil) 329 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 330 | err := conn.Ping(context.TODO()) 331 | assert.NoError(t, err) 332 | 333 | var output bufLog 334 | err = json.Unmarshal(bufLogger.Bytes(), &output) 335 | assert.NoError(t, err) 336 | assert.Equal(t, "Ping", output.Message) 337 | assert.Equal(t, LevelDebug.String(), output.Level) 338 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 339 | }) 340 | } 341 | 342 | func TestConnection_Exec(t *testing.T) { 343 | t.Run("Non driver.Execer Will Return Error", func(t *testing.T) { 344 | driverConnMock := &driverConnMock{} 345 | 346 | q := "SELECT * FROM tt WHERE id = ?" 347 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 348 | res, err := conn.Exec(q, []driver.Value{1}) 349 | assert.Nil(t, res) 350 | assert.Error(t, err) 351 | assert.Equal(t, interface{}(driver.ErrSkip), err) 352 | }) 353 | 354 | t.Run("driver.Execer Return Error", func(t *testing.T) { 355 | driverConnMock := &driverConnExecerMock{} 356 | resultMock := driver.ResultNoRows 357 | driverConnMock.On("Exec", mock.Anything, mock.Anything).Return(resultMock, driver.ErrBadConn) 358 | 359 | q := "SELECT * FROM tt WHERE id = ?" 360 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 361 | _, err := conn.Exec(q, []driver.Value{1}) 362 | assert.Error(t, err) 363 | assert.Equal(t, interface{}(driver.ErrBadConn), err) 364 | 365 | var output bufLog 366 | err = json.Unmarshal(bufLogger.Bytes(), &output) 367 | assert.NoError(t, err) 368 | assert.Equal(t, "Exec", output.Message) 369 | assert.Equal(t, LevelError.String(), output.Level) 370 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 371 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 372 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 373 | }) 374 | 375 | t.Run("driver.Execer Success", func(t *testing.T) { 376 | driverConnMock := &driverConnExecerMock{} 377 | resultMock := driver.ResultNoRows 378 | driverConnMock.On("Exec", mock.Anything, mock.Anything).Return(resultMock, nil) 379 | 380 | q := "SELECT * FROM tt WHERE id = ?" 381 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 382 | _, err := conn.Exec(q, []driver.Value{"testid"}) 383 | assert.NoError(t, err) 384 | 385 | var output bufLog 386 | err = json.Unmarshal(bufLogger.Bytes(), &output) 387 | assert.NoError(t, err) 388 | assert.Equal(t, "Exec", output.Message) 389 | assert.Equal(t, LevelInfo.String(), output.Level) 390 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 391 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 392 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 393 | }) 394 | 395 | t.Run("driver.Execer Success With Custom Level", func(t *testing.T) { 396 | driverConnMock := &driverConnExecerMock{} 397 | resultMock := driver.ResultNoRows 398 | driverConnMock.On("Exec", mock.Anything, mock.Anything).Return(resultMock, nil) 399 | 400 | q := "SELECT * FROM tt WHERE id = ?" 401 | custOpt := *testOpts 402 | WithExecerLevel(LevelDebug)(&custOpt) 403 | custLogger := *testLogger 404 | custLogger.opt = &custOpt 405 | 406 | conn := &connection{Conn: driverConnMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID()} 407 | _, err := conn.Exec(q, []driver.Value{"testid"}) 408 | assert.NoError(t, err) 409 | 410 | var output bufLog 411 | err = json.Unmarshal(bufLogger.Bytes(), &output) 412 | assert.NoError(t, err) 413 | assert.Equal(t, "Exec", output.Message) 414 | assert.Equal(t, LevelDebug.String(), output.Level) 415 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 416 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 417 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 418 | }) 419 | } 420 | 421 | func TestConnection_ExecContext(t *testing.T) { 422 | t.Run("Non driver.ExecerContext Return Error args", func(t *testing.T) { 423 | driverConnMock := &driverConnExecerMock{} 424 | q := "SELECT * FROM tt WHERE id = ?" 425 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 426 | _, err := conn.ExecContext(context.TODO(), q, []driver.NamedValue{ 427 | {Name: "errrrr", Ordinal: 0, Value: 1}, 428 | }) 429 | assert.Error(t, err) 430 | }) 431 | 432 | t.Run("driver.ExecerContext Return Error", func(t *testing.T) { 433 | driverConnMock := &driverConnExecerContextMock{} 434 | resultMock := driver.ResultNoRows 435 | driverConnMock.On("ExecContext", mock.Anything, mock.Anything, mock.Anything).Return(resultMock, driver.ErrBadConn) 436 | 437 | q := "SELECT * FROM tt WHERE id = ?" 438 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 439 | _, err := conn.ExecContext(context.TODO(), q, []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 440 | assert.Error(t, err) 441 | 442 | var output bufLog 443 | err = json.Unmarshal(bufLogger.Bytes(), &output) 444 | assert.NoError(t, err) 445 | assert.Equal(t, "ExecContext", output.Message) 446 | assert.Equal(t, LevelError.String(), output.Level) 447 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 448 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 449 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 450 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 451 | }) 452 | 453 | t.Run("driver.ExecerContext Success", func(t *testing.T) { 454 | driverConnMock := &driverConnExecerContextMock{} 455 | resultMock := driver.ResultNoRows 456 | driverConnMock.On("ExecContext", mock.Anything, mock.Anything, mock.Anything).Return(resultMock, nil) 457 | 458 | q := "SELECT * FROM tt WHERE id = ?" 459 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 460 | _, err := conn.ExecContext(context.TODO(), q, []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 461 | assert.NoError(t, err) 462 | 463 | var output bufLog 464 | err = json.Unmarshal(bufLogger.Bytes(), &output) 465 | assert.NoError(t, err) 466 | assert.Equal(t, "ExecContext", output.Message) 467 | assert.Equal(t, LevelInfo.String(), output.Level) 468 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 469 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 470 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 471 | }) 472 | 473 | t.Run("driver.ExecerContext Success", func(t *testing.T) { 474 | driverConnMock := &driverConnExecerContextMock{} 475 | resultMock := driver.ResultNoRows 476 | driverConnMock.On("ExecContext", mock.Anything, mock.Anything, mock.Anything).Return(resultMock, nil) 477 | 478 | q := "SELECT * FROM tt WHERE id = ?" 479 | custOpt := *testOpts 480 | WithExecerLevel(LevelDebug)(&custOpt) 481 | custLogger := *testLogger 482 | custLogger.opt = &custOpt 483 | 484 | conn := &connection{Conn: driverConnMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID()} 485 | _, err := conn.ExecContext(context.TODO(), q, []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 486 | assert.NoError(t, err) 487 | 488 | var output bufLog 489 | err = json.Unmarshal(bufLogger.Bytes(), &output) 490 | assert.NoError(t, err) 491 | assert.Equal(t, "ExecContext", output.Message) 492 | assert.Equal(t, LevelDebug.String(), output.Level) 493 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 494 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 495 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 496 | }) 497 | } 498 | 499 | func TestConnection_Query(t *testing.T) { 500 | t.Run("Non driver.Queryer Will Return Error", func(t *testing.T) { 501 | driverConnMock := &driverConnMock{} 502 | 503 | q := "SELECT * FROM tt WHERE id = ?" 504 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 505 | res, err := conn.Query(q, []driver.Value{1}) 506 | assert.Nil(t, res) 507 | assert.Error(t, err) 508 | assert.Equal(t, driver.ErrSkip, err) 509 | }) 510 | 511 | t.Run("driver.Queryer Return Error", func(t *testing.T) { 512 | driverConnMock := &driverConnQueryerMock{} 513 | resultMock := &rowsMock{} 514 | driverConnMock.On("Query", mock.Anything, mock.Anything).Return(resultMock, driver.ErrBadConn) 515 | 516 | q := "SELECT * FROM tt WHERE id = ?" 517 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 518 | _, err := conn.Query(q, []driver.Value{"testid"}) 519 | assert.Error(t, err) 520 | assert.Equal(t, interface{}(driver.ErrBadConn), err) 521 | 522 | var output bufLog 523 | err = json.Unmarshal(bufLogger.Bytes(), &output) 524 | assert.NoError(t, err) 525 | assert.Equal(t, "Query", output.Message) 526 | assert.Equal(t, LevelError.String(), output.Level) 527 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 528 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 529 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 530 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 531 | }) 532 | 533 | t.Run("driver.Queryer Success", func(t *testing.T) { 534 | driverConnMock := &driverConnQueryerMock{} 535 | resultMock := &rowsMock{} 536 | driverConnMock.On("Query", mock.Anything, mock.Anything).Return(resultMock, nil) 537 | 538 | q := "SELECT * FROM tt WHERE id = ?" 539 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 540 | _, err := conn.Query(q, []driver.Value{"testid"}) 541 | assert.NoError(t, err) 542 | 543 | var output bufLog 544 | err = json.Unmarshal(bufLogger.Bytes(), &output) 545 | assert.NoError(t, err) 546 | assert.Equal(t, "Query", output.Message) 547 | assert.Equal(t, LevelInfo.String(), output.Level) 548 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 549 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 550 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 551 | }) 552 | 553 | t.Run("driver.Queryer Success With Custom Level", func(t *testing.T) { 554 | driverConnMock := &driverConnQueryerMock{} 555 | resultMock := &rowsMock{} 556 | driverConnMock.On("Query", mock.Anything, mock.Anything).Return(resultMock, nil) 557 | 558 | q := "SELECT * FROM tt WHERE id = ?" 559 | custOpt := *testOpts 560 | WithQueryerLevel(LevelDebug)(&custOpt) 561 | custLogger := *testLogger 562 | custLogger.opt = &custOpt 563 | 564 | conn := &connection{Conn: driverConnMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID()} 565 | _, err := conn.Query(q, []driver.Value{"testid"}) 566 | assert.NoError(t, err) 567 | 568 | var output bufLog 569 | err = json.Unmarshal(bufLogger.Bytes(), &output) 570 | assert.NoError(t, err) 571 | assert.Equal(t, "Query", output.Message) 572 | assert.Equal(t, LevelDebug.String(), output.Level) 573 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 574 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 575 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 576 | }) 577 | } 578 | 579 | func TestConnection_QueryContext(t *testing.T) { 580 | t.Run("Non driver.QueryerContext Return Error args", func(t *testing.T) { 581 | driverConnMock := &driverConnQueryerMock{} 582 | q := "SELECT * FROM tt WHERE id = ?" 583 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 584 | _, err := conn.QueryContext(context.TODO(), q, []driver.NamedValue{ 585 | {Name: "errrrr", Ordinal: 0, Value: 1}, 586 | }) 587 | assert.Error(t, err) 588 | assert.Equal(t, driver.ErrSkip, err) 589 | }) 590 | 591 | t.Run("driver.QueryerContext Return Error", func(t *testing.T) { 592 | driverConnMock := &driverConnQueryerContextMock{} 593 | resultMock := &rowsMock{} 594 | driverConnMock.On("QueryContext", mock.Anything, mock.Anything, mock.Anything).Return(resultMock, driver.ErrBadConn) 595 | 596 | q := "SELECT * FROM tt WHERE id = ?" 597 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 598 | _, err := conn.QueryContext(context.TODO(), q, []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 599 | assert.Error(t, err) 600 | 601 | var output bufLog 602 | err = json.Unmarshal(bufLogger.Bytes(), &output) 603 | assert.NoError(t, err) 604 | assert.Equal(t, "QueryContext", output.Message) 605 | assert.Equal(t, LevelError.String(), output.Level) 606 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 607 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 608 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 609 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 610 | }) 611 | 612 | t.Run("driver.QueryerContext Success", func(t *testing.T) { 613 | driverConnMock := &driverConnQueryerContextMock{} 614 | resultMock := &rowsMock{} 615 | driverConnMock.On("QueryContext", mock.Anything, mock.Anything, mock.Anything).Return(resultMock, nil) 616 | 617 | q := "SELECT * FROM tt WHERE id = ?" 618 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 619 | _, err := conn.QueryContext(context.TODO(), q, []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 620 | assert.NoError(t, err) 621 | 622 | var output bufLog 623 | err = json.Unmarshal(bufLogger.Bytes(), &output) 624 | assert.NoError(t, err) 625 | assert.Equal(t, "QueryContext", output.Message) 626 | assert.Equal(t, LevelInfo.String(), output.Level) 627 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 628 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 629 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 630 | }) 631 | 632 | t.Run("driver.QueryerContext Success With Custom Level", func(t *testing.T) { 633 | driverConnMock := &driverConnQueryerContextMock{} 634 | resultMock := &rowsMock{} 635 | driverConnMock.On("QueryContext", mock.Anything, mock.Anything, mock.Anything).Return(resultMock, nil) 636 | 637 | q := "SELECT * FROM tt WHERE id = ?" 638 | custOpt := *testOpts 639 | WithQueryerLevel(LevelDebug)(&custOpt) 640 | custLogger := *testLogger 641 | custLogger.opt = &custOpt 642 | 643 | conn := &connection{Conn: driverConnMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID()} 644 | _, err := conn.QueryContext(context.TODO(), q, []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 645 | assert.NoError(t, err) 646 | 647 | var output bufLog 648 | err = json.Unmarshal(bufLogger.Bytes(), &output) 649 | assert.NoError(t, err) 650 | assert.Equal(t, "QueryContext", output.Message) 651 | assert.Equal(t, LevelDebug.String(), output.Level) 652 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 653 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 654 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 655 | }) 656 | } 657 | 658 | func TestConnection_ResetSession(t *testing.T) { 659 | t.Run("Non driver.SessionResetter", func(t *testing.T) { 660 | driverConnMock := &driverConnMock{} 661 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 662 | err := conn.ResetSession(context.TODO()) 663 | assert.Error(t, err) 664 | assert.Error(t, driver.ErrSkip, err) 665 | }) 666 | 667 | t.Run("driver.SessionResetter Return Error", func(t *testing.T) { 668 | driverConnMock := &driverConnResetterMock{} 669 | driverConnMock.On("ResetSession", mock.Anything).Return(driver.ErrBadConn) 670 | 671 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 672 | err := conn.ResetSession(context.TODO()) 673 | assert.Error(t, err) 674 | 675 | var output bufLog 676 | err = json.Unmarshal(bufLogger.Bytes(), &output) 677 | assert.NoError(t, err) 678 | assert.Equal(t, "ResetSession", output.Message) 679 | assert.Equal(t, LevelError.String(), output.Level) 680 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 681 | }) 682 | } 683 | 684 | func TestConnection_CheckNamedValue(t *testing.T) { 685 | t.Run("Non driver.NamedValueChecker", func(t *testing.T) { 686 | driverConnMock := &driverConnMock{} 687 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 688 | err := conn.CheckNamedValue(&driver.NamedValue{ 689 | Name: "", 690 | Ordinal: 0, 691 | Value: "testid", 692 | }) 693 | assert.Error(t, err) 694 | assert.Equal(t, driver.ErrSkip, err) 695 | }) 696 | 697 | t.Run("driver.NamedValueChecker Return Error", func(t *testing.T) { 698 | driverConnMock := &driverConnNameValueCheckerMock{} 699 | driverConnMock.On("CheckNamedValue", mock.Anything).Return(driver.ErrBadConn) 700 | 701 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 702 | err := conn.CheckNamedValue(&driver.NamedValue{ 703 | Name: "", 704 | Ordinal: 0, 705 | Value: "testid", 706 | }) 707 | assert.Error(t, err) 708 | 709 | var output bufLog 710 | err = json.Unmarshal(bufLogger.Bytes(), &output) 711 | assert.NoError(t, err) 712 | assert.Equal(t, "CheckNamedValue", output.Message) 713 | assert.Equal(t, LevelError.String(), output.Level) 714 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 715 | assert.Equal(t, conn.id, output.Data[testOpts.connIDFieldname]) 716 | }) 717 | } 718 | 719 | type driverConnMock struct { 720 | mock.Mock 721 | } 722 | 723 | func (m *driverConnMock) Prepare(query string) (driver.Stmt, error) { 724 | args := m.Called(query) 725 | 726 | return args.Get(0).(driver.Stmt), args.Error(1) 727 | } 728 | func (m *driverConnMock) Close() error { return m.Called().Error(0) } 729 | func (m *driverConnMock) Begin() (driver.Tx, error) { 730 | return m.Called().Get(0).(driver.Tx), m.Called().Error(1) 731 | } 732 | 733 | type driverConnExecerMock struct { 734 | driverConnMock 735 | } 736 | 737 | func (m *driverConnExecerMock) Exec(query string, args []driver.Value) (driver.Result, error) { 738 | arg := m.Called(query, args) 739 | 740 | return arg.Get(0).(driver.Result), arg.Error(1) 741 | } 742 | 743 | type driverConnExecerContextMock struct { 744 | driverConnExecerMock 745 | } 746 | 747 | func (m *driverConnExecerContextMock) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { 748 | arg := m.Called(ctx, query, args) 749 | 750 | return arg.Get(0).(driver.Result), arg.Error(1) 751 | } 752 | 753 | type driverConnQueryerMock struct { 754 | driverConnMock 755 | } 756 | 757 | func (m *driverConnQueryerMock) Query(query string, args []driver.Value) (driver.Rows, error) { 758 | arg := m.Called(query, args) 759 | 760 | return arg.Get(0).(driver.Rows), arg.Error(1) 761 | } 762 | 763 | type driverConnQueryerContextMock struct { 764 | driverConnExecerMock 765 | } 766 | 767 | func (m *driverConnQueryerContextMock) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { 768 | arg := m.Called(ctx, query, args) 769 | 770 | return arg.Get(0).(driver.Rows), arg.Error(1) 771 | } 772 | 773 | type driverConnWithContextMock struct { 774 | driverConnMock 775 | } 776 | 777 | func (m *driverConnWithContextMock) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { 778 | args := m.Called(ctx, opts) 779 | 780 | return args.Get(0).(driver.Tx), args.Error(1) 781 | } 782 | 783 | func (m *driverConnWithContextMock) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { 784 | args := m.Called(query) 785 | 786 | return args.Get(0).(driver.Stmt), args.Error(1) 787 | } 788 | 789 | type driverConnPingerMock struct { 790 | driverConnMock 791 | } 792 | 793 | func (m *driverConnPingerMock) Ping(ctx context.Context) error { return m.Called().Error(0) } 794 | 795 | type driverConnResetterMock struct { 796 | driverConnMock 797 | } 798 | 799 | func (m *driverConnResetterMock) ResetSession(ctx context.Context) error { 800 | return m.Called(ctx).Error(0) 801 | } 802 | 803 | type driverConnNameValueCheckerMock struct { 804 | driverConnMock 805 | } 806 | 807 | func (m *driverConnNameValueCheckerMock) CheckNamedValue(nm *driver.NamedValue) error { 808 | return m.Called(nm).Error(0) 809 | } 810 | -------------------------------------------------------------------------------- /connector.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "time" 7 | ) 8 | 9 | // connector is a wrapped connector to a given driver and should implements: 10 | // - driver.Connector 11 | type connector struct { 12 | dsn string 13 | driver driver.Driver 14 | logger *logger 15 | } 16 | 17 | // Connect implement driver.Connector which will open new db connection if none exist 18 | func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { 19 | start, id := time.Now(), c.logger.opt.uidGenerator.UniqueID() 20 | logID := c.logger.withUID(c.logger.opt.connIDFieldname, id) 21 | conn, err := c.driver.Open(c.dsn) 22 | 23 | if err != nil { 24 | c.logger.log(ctx, LevelError, "Connect", start, err, logID) 25 | return nil, err 26 | } 27 | 28 | c.logger.log(ctx, LevelDebug, "Connect", start, err, logID) 29 | 30 | return &connection{Conn: conn, logger: c.logger, id: id}, nil 31 | } 32 | 33 | // Driver implement driver.Connector 34 | func (c *connector) Driver() driver.Driver { return c.driver } 35 | -------------------------------------------------------------------------------- /connector_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func TestConnector_Connect(t *testing.T) { 14 | t.Run("Connect Error", func(t *testing.T) { 15 | mockDriver := &driverMock{} 16 | mockDriver.On("Open", mock.Anything).Return(&driverConnMock{}, driver.ErrBadConn) 17 | 18 | con := &connector{dsn: "test", driver: mockDriver, logger: testLogger} 19 | _, err := con.Connect(context.TODO()) 20 | assert.Error(t, err) 21 | 22 | var output bufLog 23 | err = json.Unmarshal(bufLogger.Bytes(), &output) 24 | assert.NoError(t, err) 25 | assert.Equal(t, "Connect", output.Message) 26 | assert.Equal(t, LevelError.String(), output.Level) 27 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 28 | }) 29 | 30 | t.Run("Connect Success", func(t *testing.T) { 31 | mockDriver := &driverMock{} 32 | mockDriver.On("Open", mock.Anything).Return(&driverConnMock{}, nil) 33 | 34 | con := &connector{dsn: "test", driver: mockDriver, logger: testLogger} 35 | _, err := con.Connect(context.TODO()) 36 | assert.NoError(t, err) 37 | 38 | var output bufLog 39 | err = json.Unmarshal(bufLogger.Bytes(), &output) 40 | assert.NoError(t, err) 41 | assert.Equal(t, "Connect", output.Message) 42 | assert.Equal(t, LevelDebug.String(), output.Level) 43 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 44 | }) 45 | } 46 | 47 | func TestConnector_Driver(t *testing.T) { 48 | mockDriver := &driverMock{} 49 | con := &connector{dsn: "test", driver: mockDriver, logger: testLogger} 50 | drv := con.Driver() 51 | 52 | assert.Equal(t, mockDriver, drv) 53 | } 54 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | // Package sqldblogger act as thin and transparent logger without having to change existing *sql.DB usage. 2 | package sqldblogger 3 | 4 | import ( 5 | "database/sql" 6 | "database/sql/driver" 7 | ) 8 | 9 | // OpenDriver wrap given driver with logger and return *sql.DB. 10 | func OpenDriver(dsn string, drv driver.Driver, lg Logger, opt ...Option) *sql.DB { 11 | opts := &options{} 12 | setDefaultOptions(opts) 13 | 14 | for _, o := range opt { 15 | o(opts) 16 | } 17 | 18 | conn := &connector{ 19 | dsn: dsn, 20 | driver: drv, 21 | logger: &logger{logger: lg, opt: opts}, 22 | } 23 | 24 | return sql.OpenDB(conn) 25 | } 26 | -------------------------------------------------------------------------------- /database_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func init() { 14 | sql.Register("mock", &driverMock{}) 15 | } 16 | 17 | func TestOpenDriver(t *testing.T) { 18 | t.Run("Without Options", func(t *testing.T) { 19 | mockDriver := &driverMock{} 20 | mockDriver.On("Open", mock.Anything).Return(&driverConnMock{}, nil) 21 | 22 | db := OpenDriver("test", mockDriver, bufLogger) 23 | _, ok := interface{}(db).(*sql.DB) 24 | assert.True(t, ok) 25 | }) 26 | 27 | t.Run("With Options", func(t *testing.T) { 28 | mockDriver := &driverMock{} 29 | mockDriver.On("Open", mock.Anything).Return(&driverConnMock{}, driver.ErrBadConn) 30 | 31 | db := OpenDriver("test", mockDriver, bufLogger, WithErrorFieldname("errtest"), WithMinimumLevel(LevelDebug)) 32 | _, ok := interface{}(db).(*sql.DB) 33 | assert.True(t, ok) 34 | err := db.Ping() 35 | assert.Error(t, err) 36 | 37 | var output bufLog 38 | err = json.Unmarshal(bufLogger.Bytes(), &output) 39 | assert.NoError(t, err) 40 | assert.Equal(t, "Connect", output.Message) 41 | assert.Equal(t, LevelError.String(), output.Level) 42 | assert.Contains(t, output.Data, "errtest") 43 | }) 44 | } 45 | 46 | type driverMock struct { 47 | mock.Mock 48 | } 49 | 50 | func (m *driverMock) Open(name string) (driver.Conn, error) { 51 | arg := m.Called(name) 52 | 53 | return arg.Get(0).(driver.Conn), arg.Error(1) 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simukti/sqldb-logger 2 | 3 | go 1.17 4 | 5 | require github.com/stretchr/testify v1.8.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/kr/pretty v0.1.0 // indirect 10 | github.com/pmezard/go-difflib v1.0.0 // indirect 11 | github.com/stretchr/objx v0.5.0 // indirect 12 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 5 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 13 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 14 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 15 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 17 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 18 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /logadapter/logrusadapter/README.md: -------------------------------------------------------------------------------- 1 | ```go 2 | logger := logrus.New() 3 | logger.Level = logrus.DebugLevel // miminum level 4 | logger.Formatter = &logrus.JSONFormatter{} // logrus automatically add time field 5 | // other logrus variable setup 6 | // populate log pre-fields here before set to OpenDriver 7 | db := sqldblogger.OpenDriver( 8 | dsn, 9 | &mysql.MySQLDriver{}, 10 | logrusadapter.New(logger), 11 | // optional config... 12 | ) 13 | ``` -------------------------------------------------------------------------------- /logadapter/logrusadapter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simukti/sqldb-logger/logadapter/logrusadapter 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea 7 | github.com/sirupsen/logrus v1.9.0 8 | github.com/stretchr/testify v1.8.1 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | golang.org/x/sys v0.4.0 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /logadapter/logrusadapter/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 10 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea h1:MygiYxbZHQAGOsZmrIiytjLhPLwww1xcdXzPORrOrLM= 14 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE= 15 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 16 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 19 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 20 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 25 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 26 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 28 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 31 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | -------------------------------------------------------------------------------- /logadapter/logrusadapter/logger.go: -------------------------------------------------------------------------------- 1 | package logrusadapter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | sqldblogger "github.com/simukti/sqldb-logger" 9 | ) 10 | 11 | type logrusAdapter struct { 12 | logger *logrus.Logger 13 | } 14 | 15 | // New set logrus logger as backend as an example on how it process log from sqldblogger.Log(). 16 | func New(logger *logrus.Logger) sqldblogger.Logger { 17 | return &logrusAdapter{logger: logger} 18 | } 19 | 20 | // Log implement sqldblogger.Logger and log it as is. 21 | // To use context.Context values, please copy this file and adjust to your needs. 22 | func (l *logrusAdapter) Log(ctx context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) { 23 | // logrus will rename "time" field to "fields.time" and provide their own time value (RFC3339) 24 | // see: https://github.com/sirupsen/logrus#entries 25 | entry := l.logger.WithContext(ctx).WithFields(data) 26 | 27 | switch level { 28 | case sqldblogger.LevelError: 29 | entry.Error(msg) 30 | case sqldblogger.LevelInfo: 31 | entry.Info(msg) 32 | case sqldblogger.LevelDebug: 33 | entry.Debug(msg) 34 | case sqldblogger.LevelTrace: 35 | entry.Trace(msg) 36 | default: 37 | entry.Debug(msg) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /logadapter/logrusadapter/logger_test.go: -------------------------------------------------------------------------------- 1 | package logrusadapter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/assert" 13 | 14 | sqldblogger "github.com/simukti/sqldb-logger" 15 | ) 16 | 17 | type logContent struct { 18 | Level string `json:"level"` 19 | Time int64 `json:"fields.time"` 20 | Duration float64 `json:"duration"` 21 | Query string `json:"query"` 22 | Args []interface{} `json:"args"` 23 | Error string `json:"error"` 24 | } 25 | 26 | func TestLogrusAdapter_Log(t *testing.T) { 27 | now := time.Now() 28 | wr := &bytes.Buffer{} 29 | lr := logrus.New() 30 | lr.Out = wr 31 | lr.Level = logrus.TraceLevel 32 | lr.Formatter = &logrus.JSONFormatter{} 33 | logger := New(lr) 34 | 35 | lvls := map[sqldblogger.Level]string{ 36 | sqldblogger.LevelError: "error", 37 | sqldblogger.LevelInfo: "info", 38 | sqldblogger.LevelDebug: "debug", 39 | sqldblogger.LevelTrace: "trace", 40 | sqldblogger.Level(99): "debug", // unknown 41 | } 42 | 43 | for lvl, lvlStr := range lvls { 44 | data := map[string]interface{}{ 45 | "time": now.Unix(), 46 | "duration": time.Since(now).Nanoseconds(), 47 | "query": "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", 48 | "args": []interface{}{1}, 49 | } 50 | 51 | if lvl == sqldblogger.LevelError { 52 | data["error"] = fmt.Errorf("dummy error").Error() 53 | } 54 | 55 | logger.Log(context.TODO(), lvl, "query", data) 56 | 57 | var content logContent 58 | 59 | err := json.Unmarshal(wr.Bytes(), &content) 60 | assert.NoError(t, err) 61 | assert.Equal(t, now.Unix(), content.Time) 62 | assert.True(t, content.Duration > 0) 63 | assert.Equal(t, lvlStr, content.Level) 64 | assert.Equal(t, "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", content.Query) 65 | if lvl == sqldblogger.LevelError { 66 | assert.Equal(t, "dummy error", content.Error) 67 | } 68 | wr.Reset() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /logadapter/onelogadapter/README.md: -------------------------------------------------------------------------------- 1 | ## SQLDB-LOGGER ONELOG ADAPTER 2 | 3 | ![stdout sample](./onelog.jpg?raw=true "stdout output") 4 | 5 | ```go 6 | logger := onelog.New(os.Stdout, onelog.ALL) 7 | // populate log pre-fields here before set to OpenDriver 8 | db := sqldblogger.OpenDriver( 9 | dsn, 10 | &mysql.MySQLDriver{}, 11 | onelogadapter.New(logger), 12 | // optional config... 13 | ) 14 | ``` -------------------------------------------------------------------------------- /logadapter/onelogadapter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simukti/sqldb-logger/logadapter/onelogadapter 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/francoispqt/onelog v0.0.0-20190306043706-8c2bb31b10a4 7 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea 8 | github.com/stretchr/testify v1.8.1 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/francoispqt/gojay v1.2.13 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /logadapter/onelogadapter/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 5 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 6 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 7 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 8 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 9 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 14 | github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 15 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 16 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 21 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 22 | github.com/francoispqt/gojay v0.0.0-20181220093123-f2cc13a668ca/go.mod h1:H8Wgri1Asi1VevY3ySdpIK5+KCpqzToVswNq8g2xZj4= 23 | github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= 24 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 25 | github.com/francoispqt/onelog v0.0.0-20190306043706-8c2bb31b10a4 h1:N9eG+1y9e3tnNPXKjssLMa8MumIBDWWoJQWM7htGWUc= 26 | github.com/francoispqt/onelog v0.0.0-20190306043706-8c2bb31b10a4/go.mod h1:v1Il1fkBpjiYPpEJcGxqgrPUPcHuTC7eHh9zBV3CLBE= 27 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 28 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 29 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 30 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 31 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 32 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 33 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 34 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 35 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 39 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 40 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 41 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 42 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 43 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 44 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 45 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 46 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 47 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 48 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 49 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 50 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 51 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 52 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 53 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 54 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 57 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 58 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 59 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 60 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 61 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 62 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 65 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 66 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 67 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 68 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 69 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 70 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 71 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 72 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 73 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 74 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 75 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 76 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 77 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 78 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 79 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 80 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 81 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 82 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 83 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 84 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 85 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 86 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 87 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 88 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 89 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 90 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 91 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 92 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 93 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 94 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 95 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 96 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 97 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 98 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 99 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea h1:MygiYxbZHQAGOsZmrIiytjLhPLwww1xcdXzPORrOrLM= 100 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE= 101 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 102 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 103 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 104 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 105 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 106 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 107 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 108 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 109 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 110 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 111 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 112 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 113 | github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= 114 | github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= 115 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 116 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 117 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 118 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 119 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 120 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 121 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 122 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 123 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 124 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 125 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 126 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 129 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 130 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 132 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 133 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 134 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 135 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 136 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 137 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 138 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 146 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 148 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 149 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 150 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 151 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 152 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 153 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 154 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 155 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 156 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 157 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 158 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 159 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 160 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 161 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 162 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 163 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 164 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 165 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 166 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 167 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 168 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 169 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 170 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 171 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 173 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 174 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 175 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 178 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 179 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 180 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 181 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 182 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 183 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 184 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 185 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 186 | -------------------------------------------------------------------------------- /logadapter/onelogadapter/logger.go: -------------------------------------------------------------------------------- 1 | package onelogadapter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/francoispqt/onelog" 7 | 8 | sqldblogger "github.com/simukti/sqldb-logger" 9 | ) 10 | 11 | type onelogAdapter struct { 12 | logger *onelog.Logger 13 | } 14 | 15 | // New set onelog logger as backend as an example on how it process log from sqldblogger.Log(). 16 | func New(logger *onelog.Logger) sqldblogger.Logger { 17 | return &onelogAdapter{logger: logger} 18 | } 19 | 20 | // Log implement sqldblogger.Logger and log it as is. 21 | // To use context.Context values, please copy this file and adjust to your needs. 22 | func (oa *onelogAdapter) Log(_ context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) { 23 | var chain onelog.ChainEntry 24 | 25 | switch level { 26 | case sqldblogger.LevelError: 27 | chain = oa.logger.ErrorWith(msg) 28 | case sqldblogger.LevelInfo: 29 | chain = oa.logger.InfoWith(msg) 30 | case sqldblogger.LevelDebug: 31 | chain = oa.logger.DebugWith(msg) 32 | default: 33 | // trace will use onelog debug 34 | chain = oa.logger.DebugWith(msg) 35 | } 36 | 37 | for k, v := range data { 38 | chain.Any(k, v) 39 | } 40 | 41 | chain.Write() 42 | } 43 | -------------------------------------------------------------------------------- /logadapter/onelogadapter/logger_test.go: -------------------------------------------------------------------------------- 1 | package onelogadapter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/francoispqt/onelog" 12 | "github.com/stretchr/testify/assert" 13 | 14 | sqldblogger "github.com/simukti/sqldb-logger" 15 | ) 16 | 17 | type logContent struct { 18 | Level string `json:"level"` 19 | Time int64 `json:"time"` 20 | Duration float64 `json:"duration"` 21 | Query string `json:"query"` 22 | Args []interface{} `json:"args"` 23 | Error string `json:"error"` 24 | } 25 | 26 | func TestOnelogAdapter_Log(t *testing.T) { 27 | now := time.Now() 28 | wr := &bytes.Buffer{} 29 | logger := New(onelog.New(wr, onelog.ALL)) 30 | 31 | lvls := map[sqldblogger.Level]string{ 32 | sqldblogger.LevelError: "error", 33 | sqldblogger.LevelInfo: "info", 34 | sqldblogger.LevelDebug: "debug", 35 | sqldblogger.LevelTrace: "debug", 36 | sqldblogger.Level(99): "debug", // unknown 37 | } 38 | 39 | for lvl, lvlStr := range lvls { 40 | data := map[string]interface{}{ 41 | "time": now.Unix(), 42 | "duration": time.Since(now).Nanoseconds(), 43 | "query": "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", 44 | "args": []interface{}{1}, 45 | } 46 | 47 | if lvl == sqldblogger.LevelError { 48 | data["error"] = fmt.Errorf("dummy error").Error() 49 | } 50 | 51 | logger.Log(context.TODO(), lvl, "query", data) 52 | 53 | var content logContent 54 | 55 | err := json.Unmarshal(wr.Bytes(), &content) 56 | assert.NoError(t, err) 57 | assert.Equal(t, now.Unix(), content.Time) 58 | assert.True(t, content.Duration > 0) 59 | assert.Equal(t, lvlStr, content.Level) 60 | assert.Equal(t, "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", content.Query) 61 | if lvl == sqldblogger.LevelError { 62 | assert.Equal(t, "dummy error", content.Error) 63 | } 64 | wr.Reset() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /logadapter/onelogadapter/onelog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simukti/sqldb-logger/646c1a075551789f4694dd17081086d6ed940ee8/logadapter/onelogadapter/onelog.jpg -------------------------------------------------------------------------------- /logadapter/zapadapter/README.md: -------------------------------------------------------------------------------- 1 | ## SQLDB-LOGGER ZAP ADAPTER 2 | 3 | ![stdout sample](./zap.jpg?raw=true "stdout output") 4 | 5 | ```go 6 | zapCfg := zap.NewProductionConfig() 7 | zapCfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel) // whatever minimum level 8 | zapCfg.DisableCaller = true 9 | logger, _ := zapCfg.Build() 10 | // populate log pre-fields here before set to OpenDriver 11 | db := sqldblogger.OpenDriver( 12 | dsn, 13 | &mysql.MySQLDriver{}, 14 | zapadapter.New(logger), 15 | // optional config... 16 | ) 17 | ``` -------------------------------------------------------------------------------- /logadapter/zapadapter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simukti/sqldb-logger/logadapter/zapadapter 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea 7 | github.com/stretchr/testify v1.8.1 8 | go.uber.org/zap v1.24.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | go.uber.org/atomic v1.10.0 // indirect 16 | go.uber.org/multierr v1.9.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /logadapter/zapadapter/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 8 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 13 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 14 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea h1:MygiYxbZHQAGOsZmrIiytjLhPLwww1xcdXzPORrOrLM= 18 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 22 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 23 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 24 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 25 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 26 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 27 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 28 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 29 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 30 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 31 | go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 32 | go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 33 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 34 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 35 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 36 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 37 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 38 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 39 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 40 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 41 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 42 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 43 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 44 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 47 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 48 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 51 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 56 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 57 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 58 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 59 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 60 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 61 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 62 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 63 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 64 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 67 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 68 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 69 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 70 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 71 | -------------------------------------------------------------------------------- /logadapter/zapadapter/logger.go: -------------------------------------------------------------------------------- 1 | package zapadapter 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/zap" 7 | 8 | sqldblogger "github.com/simukti/sqldb-logger" 9 | ) 10 | 11 | type zapAdapter struct { 12 | logger *zap.Logger 13 | } 14 | 15 | // New set zap logger as backend as an example on how it process log from sqldblogger.Log(). 16 | func New(logger *zap.Logger) sqldblogger.Logger { 17 | return &zapAdapter{logger: logger} 18 | } 19 | 20 | // Log implement sqldblogger.Logger and log it as is. 21 | // To use context.Context values, please copy this file and adjust to your needs. 22 | func (zp *zapAdapter) Log(_ context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) { 23 | fields := make([]zap.Field, len(data)) 24 | i := 0 25 | 26 | for k, v := range data { 27 | fields[i] = zap.Any(k, v) 28 | i++ 29 | } 30 | 31 | switch level { 32 | case sqldblogger.LevelError: 33 | zp.logger.Error(msg, fields...) 34 | case sqldblogger.LevelInfo: 35 | zp.logger.Info(msg, fields...) 36 | case sqldblogger.LevelDebug: 37 | zp.logger.Debug(msg, fields...) 38 | default: 39 | // trace will use zap debug 40 | zp.logger.Debug(msg, fields...) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /logadapter/zapadapter/logger_test.go: -------------------------------------------------------------------------------- 1 | package zapadapter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | 15 | sqldblogger "github.com/simukti/sqldb-logger" 16 | ) 17 | 18 | type logContent struct { 19 | Level string `json:"level"` 20 | Time int64 `json:"time"` 21 | Duration float64 `json:"duration"` 22 | Query string `json:"query"` 23 | Args []interface{} `json:"args"` 24 | Error string `json:"error"` 25 | } 26 | 27 | func TestZapAdapter_Log(t *testing.T) { 28 | now := time.Now() 29 | wr := &bytes.Buffer{} 30 | syn := zapcore.AddSync(wr) 31 | enc := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) 32 | logger := New(zap.New(zapcore.NewCore(enc, syn, zap.NewAtomicLevelAt(zap.DebugLevel)))) 33 | 34 | lvls := map[sqldblogger.Level]string{ 35 | sqldblogger.LevelError: "error", 36 | sqldblogger.LevelInfo: "info", 37 | sqldblogger.LevelDebug: "debug", 38 | sqldblogger.LevelTrace: "debug", 39 | sqldblogger.Level(99): "debug", // unknown 40 | } 41 | 42 | for lvl, lvlStr := range lvls { 43 | data := map[string]interface{}{ 44 | "time": now.Unix(), 45 | "duration": time.Since(now).Nanoseconds(), 46 | "query": "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", 47 | "args": []interface{}{1}, 48 | } 49 | 50 | if lvl == sqldblogger.LevelError { 51 | data["error"] = fmt.Errorf("dummy error").Error() 52 | } 53 | 54 | logger.Log(context.TODO(), lvl, "query", data) 55 | 56 | var content logContent 57 | 58 | err := json.Unmarshal(wr.Bytes(), &content) 59 | assert.NoError(t, err) 60 | assert.Equal(t, now.Unix(), content.Time) 61 | assert.True(t, content.Duration > 0) 62 | assert.Equal(t, lvlStr, content.Level) 63 | assert.Equal(t, "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", content.Query) 64 | if lvl == sqldblogger.LevelError { 65 | assert.Equal(t, "dummy error", content.Error) 66 | } 67 | wr.Reset() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /logadapter/zapadapter/zap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simukti/sqldb-logger/646c1a075551789f4694dd17081086d6ed940ee8/logadapter/zapadapter/zap.jpg -------------------------------------------------------------------------------- /logadapter/zerologadapter/README.md: -------------------------------------------------------------------------------- 1 | ## SQLDB-LOGGER ZEROLOG ADAPTER 2 | 3 | ![stdout sample](./zerolog.jpg?raw=true "stdout output") 4 | 5 | ```go 6 | logger := zerolog.New(os.Stdout) 7 | // populate log pre-fields here before set to 8 | db := sqldblogger.OpenDriver( 9 | dsn, 10 | &mysql.MySQLDriver{}, 11 | zerologadapter.New(logger), 12 | // optional config... 13 | ) 14 | ``` -------------------------------------------------------------------------------- /logadapter/zerologadapter/console.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simukti/sqldb-logger/646c1a075551789f4694dd17081086d6ed940ee8/logadapter/zerologadapter/console.jpg -------------------------------------------------------------------------------- /logadapter/zerologadapter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simukti/sqldb-logger/logadapter/zerologadapter 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/rs/zerolog v1.28.0 7 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea 8 | github.com/stretchr/testify v1.8.1 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.17 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/sys v0.4.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /logadapter/zerologadapter/go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 7 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 8 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 13 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 14 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 15 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 16 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 17 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 18 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 19 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 24 | github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= 25 | github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 26 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea h1:MygiYxbZHQAGOsZmrIiytjLhPLwww1xcdXzPORrOrLM= 27 | github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 30 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 31 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 32 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 33 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 34 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 35 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 36 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 40 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 43 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 46 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 47 | -------------------------------------------------------------------------------- /logadapter/zerologadapter/logger.go: -------------------------------------------------------------------------------- 1 | package zerologadapter 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rs/zerolog" 7 | 8 | sqldblogger "github.com/simukti/sqldb-logger" 9 | ) 10 | 11 | type zerologAdapter struct { 12 | logger zerolog.Logger 13 | } 14 | 15 | // New set zerolog logger as backend as an example on how it process log from sqldblogger.Log(). 16 | func New(logger zerolog.Logger) sqldblogger.Logger { 17 | return &zerologAdapter{logger: logger} 18 | } 19 | 20 | // Log implement sqldblogger.Logger and log it as is. 21 | // To use context.Context values, please copy this file and adjust to your needs. 22 | func (zl *zerologAdapter) Log(_ context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) { 23 | var lvl zerolog.Level 24 | 25 | switch level { 26 | case sqldblogger.LevelError: 27 | lvl = zerolog.ErrorLevel 28 | case sqldblogger.LevelInfo: 29 | lvl = zerolog.InfoLevel 30 | case sqldblogger.LevelDebug: 31 | lvl = zerolog.DebugLevel 32 | case sqldblogger.LevelTrace: 33 | lvl = zerolog.TraceLevel 34 | default: 35 | lvl = zerolog.DebugLevel 36 | } 37 | 38 | zl.logger.WithLevel(lvl).Fields(data).Msg(msg) 39 | } 40 | -------------------------------------------------------------------------------- /logadapter/zerologadapter/logger_test.go: -------------------------------------------------------------------------------- 1 | package zerologadapter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/rs/zerolog" 12 | "github.com/stretchr/testify/assert" 13 | 14 | sqldblogger "github.com/simukti/sqldb-logger" 15 | ) 16 | 17 | type logContent struct { 18 | Level string `json:"level"` 19 | Time int64 `json:"time"` 20 | Duration float64 `json:"duration"` 21 | Query string `json:"query"` 22 | Args []interface{} `json:"args"` 23 | Error string `json:"error"` 24 | } 25 | 26 | func TestZerologAdapter_Log(t *testing.T) { 27 | now := time.Now() 28 | wr := &bytes.Buffer{} 29 | lg := New(zerolog.New(wr)) 30 | lvls := map[sqldblogger.Level]string{ 31 | sqldblogger.LevelError: "error", 32 | sqldblogger.LevelInfo: "info", 33 | sqldblogger.LevelDebug: "debug", 34 | sqldblogger.LevelTrace: "trace", 35 | sqldblogger.Level(99): "debug", // unknown 36 | } 37 | 38 | for lvl, lvlStr := range lvls { 39 | data := map[string]interface{}{ 40 | "time": now.Unix(), 41 | "duration": time.Since(now).Nanoseconds(), 42 | "query": "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", 43 | "args": []interface{}{1}, 44 | } 45 | 46 | if lvl == sqldblogger.LevelError { 47 | data["error"] = fmt.Errorf("dummy error").Error() 48 | } 49 | 50 | lg.Log(context.TODO(), lvl, "query", data) 51 | 52 | var content logContent 53 | 54 | err := json.Unmarshal(wr.Bytes(), &content) 55 | assert.NoError(t, err) 56 | assert.Equal(t, now.Unix(), content.Time) 57 | assert.True(t, content.Duration > 0) 58 | assert.Equal(t, lvlStr, content.Level) 59 | assert.Equal(t, "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1", content.Query) 60 | if lvl == sqldblogger.LevelError { 61 | assert.Equal(t, "dummy error", content.Error) 62 | } 63 | 64 | wr.Reset() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /logadapter/zerologadapter/zerolog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simukti/sqldb-logger/646c1a075551789f4694dd17081086d6ed940ee8/logadapter/zerologadapter/zerolog.jpg -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // Level is a log level which filterable by minimum level option. 12 | type Level uint8 13 | 14 | const ( 15 | // LevelTrace is the lowest level and the most detailed. 16 | // Use this if you want to know interaction flow from prepare, statement, execution to result/rows. 17 | LevelTrace Level = iota 18 | // LevelDebug is used by non Queryer(Context) and Execer(Context) call like Ping() and Connect(). 19 | LevelDebug 20 | // LevelInfo is used by Queryer, Execer, Preparer, and Stmt. 21 | LevelInfo 22 | // LevelError is used on actual driver error or when driver not implement some optional sql/driver interface. 23 | LevelError 24 | ) 25 | 26 | // String implement Stringer to convert type Level to string. 27 | // nolint // disable goconst check 28 | func (l Level) String() string { 29 | switch l { 30 | case LevelTrace: 31 | return "trace" 32 | case LevelDebug: 33 | return "debug" 34 | case LevelInfo: 35 | return "info" 36 | case LevelError: 37 | return "error" 38 | default: 39 | return fmt.Sprintf("(invalid level): %d", l) 40 | } 41 | } 42 | 43 | // Logger interface copied from: 44 | // https://github.com/jackc/pgx/blob/f3a3ee1a0e5c8fc8991928bcd06fdbcd1ee9d05c/logger.go#L46-L49 45 | type Logger interface { 46 | Log(ctx context.Context, level Level, msg string, data map[string]interface{}) 47 | } 48 | 49 | // logger internal logger wrapper 50 | type logger struct { 51 | logger Logger 52 | opt *options 53 | } 54 | 55 | // dataFunc for extra data to be added to log 56 | type dataFunc func() (string, interface{}) 57 | 58 | // withUID used to set unique id per call scope. 59 | func (l *logger) withUID(k, v string) dataFunc { 60 | return func() (string, interface{}) { 61 | if v == "" { 62 | return k, nil 63 | } 64 | 65 | return k, v 66 | } 67 | } 68 | 69 | func (l *logger) withQuery(query string) dataFunc { 70 | return func() (string, interface{}) { 71 | return l.opt.sqlQueryFieldname, query 72 | } 73 | } 74 | 75 | func (l *logger) withArgs(args []driver.Value) dataFunc { 76 | return func() (string, interface{}) { 77 | if !l.opt.logArgs { 78 | return l.opt.sqlArgsFieldname, nil 79 | } 80 | 81 | return l.withKeyArgs(l.opt.sqlArgsFieldname, args)() 82 | } 83 | } 84 | 85 | func (l *logger) withKeyArgs(key string, args []driver.Value) dataFunc { 86 | return func() (string, interface{}) { 87 | if len(args) == 0 { 88 | return key, nil 89 | } 90 | 91 | return key, parseArgs(args) 92 | } 93 | } 94 | 95 | func (l *logger) log(ctx context.Context, lvl Level, msg string, start time.Time, err error, datas ...dataFunc) { 96 | if lvl < l.opt.minimumLogLevel { 97 | return 98 | } 99 | 100 | if !l.opt.logDriverErrSkip && err == driver.ErrSkip { 101 | return 102 | } 103 | 104 | data := map[string]interface{}{ 105 | l.opt.timeFieldname: l.opt.timeFormat.format(time.Now()), 106 | l.opt.durationFieldname: l.opt.durationUnit.format(time.Since(start)), 107 | } 108 | 109 | if l.opt.includeStartTime { 110 | data[l.opt.startTimeFieldname] = l.opt.timeFormat.format(start) 111 | } 112 | 113 | if lvl == LevelError && err != nil { 114 | data[l.opt.errorFieldname] = err.Error() 115 | } 116 | 117 | for _, d := range datas { 118 | k, v := d() 119 | 120 | if k == l.opt.sqlArgsFieldname && !l.opt.logArgs { 121 | continue 122 | } 123 | 124 | // don't log nil value 125 | if v == nil { 126 | continue 127 | } 128 | 129 | if k == l.opt.sqlQueryFieldname && l.opt.sqlQueryAsMsg { 130 | msg = v.(string) 131 | continue 132 | } 133 | 134 | data[k] = v 135 | } 136 | 137 | l.logger.Log(ctx, lvl, msg, data) 138 | } 139 | 140 | // maxArgValueLen []byte and string more than this length will be truncated. 141 | const maxArgValueLen int = 64 142 | 143 | // parseArgs will trim argument value if it is []byte or string more than maxArgValueLen. 144 | // Copied from https://github.com/jackc/pgx/blob/f3a3ee1a0e5c8fc8991928bcd06fdbcd1ee9d05c/logger.go#L79 145 | // and modified accordingly. 146 | func parseArgs(argsVal []driver.Value) []interface{} { 147 | args := make([]interface{}, len(argsVal)) 148 | 149 | for k, a := range argsVal { 150 | switch v := a.(type) { 151 | case []byte: 152 | if len(v) < maxArgValueLen { 153 | a = string(v) 154 | } else { 155 | a = string(v[:maxArgValueLen]) + " (" + strconv.Itoa(len(v)-maxArgValueLen) + " bytes truncated)" 156 | } 157 | case string: 158 | if len(v) > maxArgValueLen { 159 | a = v[:maxArgValueLen] + " (" + strconv.Itoa(len(v)-maxArgValueLen) + " bytes truncated)" 160 | } 161 | } 162 | 163 | args[k] = a 164 | } 165 | 166 | return args 167 | } 168 | 169 | // namedValuesToValues is type conversion ONLY for logging arguments. 170 | func namedValuesToValues(args []driver.NamedValue) []driver.Value { 171 | argsVal := make([]driver.Value, len(args)) 172 | 173 | for k, v := range args { 174 | argsVal[k] = v.Value 175 | } 176 | 177 | return argsVal 178 | } 179 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql/driver" 7 | "encoding/json" 8 | "fmt" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestLevel_String(t *testing.T) { 16 | tt := map[Level]string{ 17 | LevelError: "error", 18 | LevelInfo: "info", 19 | LevelDebug: "debug", 20 | LevelTrace: "trace", 21 | Level(99): "(invalid level): 99", 22 | } 23 | 24 | for l, s := range tt { 25 | assert.Equal(t, l.String(), s) 26 | } 27 | } 28 | 29 | func TestNullLogger_Log(t *testing.T) { 30 | lg := &bufferTestLogger{} 31 | lg.Log(context.TODO(), LevelInfo, "msg", nil) 32 | assert.Implements(t, (*Logger)(nil), lg) 33 | } 34 | 35 | func TestWithQuery(t *testing.T) { 36 | cfg := &options{} 37 | setDefaultOptions(cfg) 38 | l := &logger{opt: cfg} 39 | k, v := l.withQuery("query")() 40 | assert.Equal(t, cfg.sqlQueryFieldname, k) 41 | assert.Equal(t, "query", fmt.Sprint(v)) 42 | } 43 | 44 | func TestWithArgs(t *testing.T) { 45 | cfg := &options{} 46 | setDefaultOptions(cfg) 47 | l := &logger{opt: cfg} 48 | 49 | t.Run("Non Empty Args", func(t *testing.T) { 50 | k, v := l.withArgs([]driver.Value{1})() 51 | assert.Equal(t, cfg.sqlArgsFieldname, k) 52 | assert.Equal(t, []interface{}{1}, v) 53 | }) 54 | 55 | t.Run("Non Empty Named Args", func(t *testing.T) { 56 | k, v := l.withArgs(namedValuesToValues([]driver.NamedValue{ 57 | {Name: "test", Ordinal: 1, Value: 9}, 58 | }))() 59 | assert.Equal(t, cfg.sqlArgsFieldname, k) 60 | assert.Equal(t, []interface{}{9}, v) 61 | }) 62 | 63 | t.Run("Empty Args", func(t *testing.T) { 64 | k, v := l.withArgs([]driver.Value{})() 65 | assert.Equal(t, cfg.sqlArgsFieldname, k) 66 | assert.Equal(t, nil, v) 67 | }) 68 | } 69 | 70 | func TestLogInternalWithMinimumLevel(t *testing.T) { 71 | tt := []struct { 72 | minLevel, givenLevel Level 73 | msg, expect string 74 | err error 75 | }{ 76 | { 77 | minLevel: LevelTrace, 78 | givenLevel: LevelTrace, 79 | msg: "msg trace", 80 | expect: "msg trace", 81 | }, 82 | { 83 | minLevel: LevelTrace, 84 | givenLevel: LevelDebug, 85 | msg: "msg debug", 86 | expect: "msg debug", 87 | }, 88 | { 89 | minLevel: LevelDebug, 90 | givenLevel: LevelDebug, 91 | msg: "msg debug", 92 | expect: "msg debug", 93 | }, 94 | { 95 | minLevel: LevelInfo, 96 | givenLevel: LevelDebug, 97 | msg: "msg debug", 98 | expect: "", 99 | }, 100 | { 101 | minLevel: LevelError, 102 | givenLevel: LevelInfo, 103 | msg: "msg info", 104 | expect: "", 105 | }, 106 | { 107 | minLevel: LevelInfo, 108 | givenLevel: LevelInfo, 109 | msg: "msg info", 110 | expect: "msg info", 111 | }, 112 | { 113 | minLevel: LevelError, 114 | givenLevel: LevelError, 115 | msg: "msg error", 116 | expect: "msg error", 117 | err: fmt.Errorf("dummy error"), 118 | }, 119 | { 120 | minLevel: LevelError, 121 | givenLevel: LevelInfo, 122 | msg: "msg info", 123 | expect: "", 124 | }, 125 | } 126 | for _, tc := range tt { 127 | cfg := &options{} 128 | setDefaultOptions(cfg) 129 | WithMinimumLevel(tc.minLevel)(cfg) 130 | bl := &bufferTestLogger{} 131 | l := &logger{opt: cfg, logger: bl} 132 | l.log(context.TODO(), tc.givenLevel, tc.msg, time.Now(), tc.err) 133 | if tc.expect == "" { 134 | assert.Equal(t, bl.String(), tc.expect) 135 | } else { 136 | assert.Contains(t, bl.String(), tc.expect) 137 | } 138 | if tc.givenLevel == LevelError && tc.err != nil { 139 | assert.Contains(t, bl.String(), tc.err.Error()) 140 | } 141 | bl.Reset() 142 | } 143 | } 144 | 145 | func TestLogInternal(t *testing.T) { 146 | cfg := &options{} 147 | setDefaultOptions(cfg) 148 | bl := &bufferTestLogger{} 149 | l := &logger{opt: cfg, logger: bl} 150 | l.log(context.TODO(), LevelInfo, "msg", time.Now(), nil) 151 | 152 | var content bufLog 153 | err := json.Unmarshal(bl.Bytes(), &content) 154 | assert.NoError(t, err) 155 | assert.Contains(t, content.Data, cfg.timeFieldname) 156 | assert.Contains(t, content.Data, cfg.durationFieldname) 157 | assert.Equal(t, LevelInfo.String(), content.Level) 158 | bl.Reset() 159 | } 160 | 161 | func TestLogInternalWithData(t *testing.T) { 162 | cfg := &options{} 163 | setDefaultOptions(cfg) 164 | bl := &bufferTestLogger{} 165 | l := &logger{opt: cfg, logger: bl} 166 | l.log(context.TODO(), LevelInfo, "msg", time.Now(), nil, l.withQuery("query")) 167 | 168 | var content bufLog 169 | err := json.Unmarshal(bl.Bytes(), &content) 170 | assert.NoError(t, err) 171 | assert.Contains(t, content.Data, cfg.timeFieldname) 172 | assert.Contains(t, content.Data, cfg.durationFieldname) 173 | assert.Contains(t, content.Data, cfg.sqlQueryFieldname) 174 | assert.Equal(t, LevelInfo.String(), content.Level) 175 | assert.Equal(t, "msg", content.Message) 176 | assert.Equal(t, "query", content.Data[cfg.sqlQueryFieldname]) 177 | bl.Reset() 178 | } 179 | 180 | func TestLogInternalErrorLevel(t *testing.T) { 181 | cfg := &options{} 182 | setDefaultOptions(cfg) 183 | bl := &bufferTestLogger{} 184 | l := &logger{opt: cfg, logger: bl} 185 | l.log(context.TODO(), LevelError, "msg", time.Now(), fmt.Errorf("dummy"), l.withQuery("query")) 186 | 187 | var content bufLog 188 | err := json.Unmarshal(bl.Bytes(), &content) 189 | assert.NoError(t, err) 190 | assert.Contains(t, content.Data, cfg.errorFieldname) 191 | assert.Contains(t, content.Data, cfg.sqlQueryFieldname) 192 | assert.Contains(t, content.Data, cfg.timeFieldname) 193 | assert.Contains(t, content.Data, cfg.durationFieldname) 194 | bl.Reset() 195 | } 196 | 197 | func TestLogTrimStringArgs(t *testing.T) { 198 | cfg := &options{} 199 | setDefaultOptions(cfg) 200 | 201 | longArgVal := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." 202 | bl := &bufferTestLogger{} 203 | l := &logger{opt: cfg, logger: bl} 204 | l.log( 205 | context.TODO(), 206 | LevelInfo, 207 | "msg", 208 | time.Now(), 209 | nil, 210 | l.withUID(cfg.stmtIDFieldname, ""), 211 | l.withQuery("query"), 212 | l.withArgs([]driver.Value{ 213 | longArgVal, 214 | []byte(longArgVal), 215 | []byte("short"), 216 | }), 217 | ) 218 | 219 | var content bufLog 220 | err := json.Unmarshal(bl.Bytes(), &content) 221 | assert.NoError(t, err) 222 | assert.Contains(t, content.Data, cfg.sqlQueryFieldname) 223 | assert.Contains(t, content.Data, cfg.timeFieldname) 224 | assert.Contains(t, content.Data, cfg.durationFieldname) 225 | assert.Contains(t, content.Data, cfg.sqlArgsFieldname) 226 | assert.NotContains(t, content.Data, cfg.stmtIDFieldname) 227 | trimmedArg, ok := content.Data[cfg.sqlArgsFieldname].([]interface{}) 228 | assert.True(t, ok) 229 | assert.Equal(t, 230 | fmt.Sprintf("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do (%d bytes truncated)", len(longArgVal)-maxArgValueLen), 231 | trimmedArg[0], 232 | ) 233 | assert.Equal(t, 234 | fmt.Sprintf("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do (%d bytes truncated)", len(longArgVal)-maxArgValueLen), 235 | trimmedArg[1], 236 | ) 237 | assert.Equal(t, 238 | "short", 239 | trimmedArg[2], 240 | ) 241 | bl.Reset() 242 | } 243 | 244 | func TestWithLogArgumentsFalse(t *testing.T) { 245 | cfg := &options{} 246 | setDefaultOptions(cfg) 247 | WithLogArguments(false)(cfg) 248 | 249 | bl := &bufferTestLogger{} 250 | l := &logger{opt: cfg, logger: bl} 251 | l.log( 252 | context.TODO(), 253 | LevelInfo, 254 | "msg", 255 | time.Now(), 256 | nil, 257 | l.withQuery("query"), 258 | l.withArgs([]driver.Value{ 259 | 1, 260 | []byte("kedua"), 261 | []byte("lanjut"), 262 | }), 263 | ) 264 | 265 | var content bufLog 266 | err := json.Unmarshal(bl.Bytes(), &content) 267 | assert.NoError(t, err) 268 | assert.Contains(t, content.Data, cfg.sqlQueryFieldname) 269 | assert.Contains(t, content.Data, cfg.timeFieldname) 270 | assert.Contains(t, content.Data, cfg.durationFieldname) 271 | // sql args should not logged 272 | assert.NotContains(t, content.Data, cfg.sqlArgsFieldname) 273 | bl.Reset() 274 | } 275 | 276 | func TestWithEmptyArgs(t *testing.T) { 277 | cfg := &options{} 278 | setDefaultOptions(cfg) 279 | 280 | bl := &bufferTestLogger{} 281 | l := &logger{opt: cfg, logger: bl} 282 | l.log( 283 | context.TODO(), 284 | LevelInfo, 285 | "msg", 286 | time.Now(), 287 | nil, 288 | l.withQuery("query"), 289 | l.withArgs([]driver.Value{}), 290 | ) 291 | 292 | var content bufLog 293 | err := json.Unmarshal(bl.Bytes(), &content) 294 | assert.NoError(t, err) 295 | assert.Contains(t, content.Data, cfg.sqlQueryFieldname) 296 | assert.Contains(t, content.Data, cfg.timeFieldname) 297 | assert.Contains(t, content.Data, cfg.durationFieldname) 298 | // empty args will not logged 299 | assert.NotContains(t, content.Data, cfg.sqlArgsFieldname) 300 | } 301 | 302 | func TestWithErrorDriverSkip(t *testing.T) { 303 | cfg := &options{} 304 | setDefaultOptions(cfg) 305 | bl := &bufferTestLogger{} 306 | l := &logger{opt: cfg, logger: bl} 307 | 308 | t.Run("Skip", func(t *testing.T) { 309 | l.log( 310 | context.TODO(), 311 | LevelError, 312 | "msg", 313 | time.Now(), 314 | driver.ErrSkip, 315 | ) 316 | 317 | assert.Empty(t, bl.Bytes()) 318 | }) 319 | 320 | t.Run("No Skip", func(t *testing.T) { 321 | WithLogDriverErrorSkip(true)(cfg) 322 | 323 | l.log( 324 | context.TODO(), 325 | LevelError, 326 | "msg", 327 | time.Now(), 328 | driver.ErrSkip, 329 | ) 330 | 331 | var content bufLog 332 | err := json.Unmarshal(bl.Bytes(), &content) 333 | assert.NoError(t, err) 334 | assert.Contains(t, content.Data, cfg.timeFieldname) 335 | assert.Contains(t, content.Data, cfg.durationFieldname) 336 | assert.Contains(t, content.Data, cfg.errorFieldname) 337 | }) 338 | } 339 | 340 | func TestWithSQLQueryAsMessage2(t *testing.T) { 341 | cfg := &options{} 342 | setDefaultOptions(cfg) 343 | bl := &bufferTestLogger{} 344 | l := &logger{opt: cfg, logger: bl} 345 | 346 | WithSQLQueryAsMessage(true)(cfg) 347 | 348 | l.log( 349 | context.TODO(), 350 | LevelInfo, 351 | "msg", 352 | time.Now(), 353 | nil, 354 | testLogger.withUID(cfg.stmtIDFieldname, l.opt.uidGenerator.UniqueID()), 355 | testLogger.withQuery("query"), 356 | testLogger.withArgs([]driver.Value{}), 357 | ) 358 | 359 | var content bufLog 360 | err := json.Unmarshal(bl.Bytes(), &content) 361 | assert.NoError(t, err) 362 | assert.NotContains(t, content.Data, cfg.sqlQueryFieldname) 363 | assert.Equal(t, "query", content.Message) 364 | assert.Contains(t, content.Data, cfg.timeFieldname) 365 | assert.Contains(t, content.Data, cfg.durationFieldname) 366 | assert.Contains(t, content.Data, cfg.stmtIDFieldname) 367 | // empty args will not logged 368 | assert.NotContains(t, content.Data, cfg.sqlArgsFieldname) 369 | } 370 | 371 | type bufferTestLogger struct { 372 | bytes.Buffer 373 | } 374 | 375 | type bufLog struct { 376 | Level string `json:"level"` 377 | Message string `json:"message"` 378 | Data map[string]interface{} `json:"data"` 379 | } 380 | 381 | func (bl *bufferTestLogger) Log(_ context.Context, level Level, msg string, data map[string]interface{}) { 382 | bl.Reset() 383 | _ = json.NewEncoder(bl).Encode(bufLog{level.String(), msg, data}) 384 | } 385 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | cryptoRand "crypto/rand" 5 | "encoding/binary" 6 | "fmt" 7 | "math/rand" 8 | "time" 9 | ) 10 | 11 | type options struct { 12 | errorFieldname string 13 | durationFieldname string 14 | timeFieldname string 15 | startTimeFieldname string 16 | sqlQueryFieldname string 17 | sqlArgsFieldname string 18 | stmtIDFieldname string 19 | connIDFieldname string 20 | txIDFieldname string 21 | sqlQueryAsMsg bool 22 | logArgs bool 23 | logDriverErrSkip bool 24 | wrapResult bool 25 | minimumLogLevel Level 26 | durationUnit DurationUnit 27 | timeFormat TimeFormat 28 | uidGenerator UIDGenerator 29 | includeStartTime bool 30 | preparerLevel Level 31 | queryerLevel Level 32 | execerLevel Level 33 | } 34 | 35 | // setDefaultOptions called first time before Log() called (see: OpenDriver()). 36 | // To change option value, use With* functions below. 37 | func setDefaultOptions(opt *options) { 38 | opt.errorFieldname = "error" 39 | opt.durationFieldname = "duration" 40 | opt.timeFieldname = "time" 41 | opt.startTimeFieldname = "start" 42 | opt.sqlQueryFieldname = "query" 43 | opt.sqlArgsFieldname = "args" 44 | opt.stmtIDFieldname = "stmt_id" 45 | opt.connIDFieldname = "conn_id" 46 | opt.txIDFieldname = "tx_id" 47 | opt.sqlQueryAsMsg = false 48 | opt.minimumLogLevel = LevelDebug 49 | opt.logArgs = true 50 | opt.logDriverErrSkip = false 51 | opt.wrapResult = true 52 | opt.durationUnit = DurationMillisecond 53 | opt.timeFormat = TimeFormatUnix 54 | opt.uidGenerator = newDefaultUIDDGenerator() 55 | opt.includeStartTime = false 56 | opt.preparerLevel = LevelInfo 57 | opt.queryerLevel = LevelInfo 58 | opt.execerLevel = LevelInfo 59 | } 60 | 61 | // DurationUnit is total time spent on an actual driver function call calculated by time.Since(start). 62 | type DurationUnit uint8 63 | 64 | const ( 65 | // DurationNanosecond will format time.Since() result to nanosecond unit (1/1_000_000_000 second). 66 | DurationNanosecond DurationUnit = iota 67 | // DurationMicrosecond will format time.Since() result to microsecond unit (1/1_000_000 second). 68 | DurationMicrosecond 69 | // DurationMillisecond will format time.Since() result to millisecond unit (1/1_000 second). 70 | DurationMillisecond 71 | ) 72 | 73 | func (du DurationUnit) format(duration time.Duration) float64 { 74 | nanosecond := float64(duration.Nanoseconds()) 75 | 76 | switch du { 77 | case DurationNanosecond: 78 | return nanosecond 79 | case DurationMicrosecond: 80 | return nanosecond / float64(time.Microsecond) 81 | case DurationMillisecond: 82 | return nanosecond / float64(time.Millisecond) 83 | default: 84 | return nanosecond 85 | } 86 | } 87 | 88 | // TimeFormat is time.Now() format when Log() deliver the log message. 89 | type TimeFormat uint8 90 | 91 | const ( 92 | // TimeFormatUnix will format log time to unix timestamp. 93 | TimeFormatUnix TimeFormat = iota 94 | // TimeFormatUnixNano will format log time to unix timestamp with nano seconds. 95 | TimeFormatUnixNano 96 | // TimeFormatRFC3339 will format log time to time.RFC3339 format. 97 | TimeFormatRFC3339 98 | // TimeFormatRFC3339Nano will format log time to time.RFC3339Nano format. 99 | TimeFormatRFC3339Nano 100 | ) 101 | 102 | func (tf TimeFormat) format(logTime time.Time) interface{} { 103 | switch tf { 104 | case TimeFormatUnix: 105 | return logTime.Unix() 106 | case TimeFormatUnixNano: 107 | return logTime.UnixNano() 108 | case TimeFormatRFC3339: 109 | return logTime.Format(time.RFC3339) 110 | case TimeFormatRFC3339Nano: 111 | return logTime.Format(time.RFC3339Nano) 112 | default: 113 | return logTime.Unix() 114 | } 115 | } 116 | 117 | // UIDGenerator is an interface to generate unique ID for context call (connection, statement, transaction). 118 | // The point of having unique id per context call is to easily track and analyze logs. 119 | // 120 | // Note: no possible way to track id when statement Execer(Context),Queryer(Context) called from under db.Tx. 121 | type UIDGenerator interface { 122 | UniqueID() string 123 | } 124 | 125 | const ( 126 | defaultUIDLen = 16 127 | defaultUIDCharlist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_" 128 | ) 129 | 130 | // newDefaultUIDDGenerator default unique id generator using crypto/rand as math/rand seed. 131 | func newDefaultUIDDGenerator() UIDGenerator { 132 | var s [16]byte 133 | if _, err := cryptoRand.Read(s[:]); err != nil { 134 | panic(fmt.Sprintf("sqldblogger: could not get random bytes from cryto/rand: '%s'", err.Error())) 135 | } 136 | 137 | // seed math/rand with 16 random bytes from crypto/rand to make sure rand.Seed is not 1. 138 | // rand.Seed will be used by rand.Read inside UniqueID(). 139 | rand.Seed(int64(binary.LittleEndian.Uint64(s[:]))) 140 | 141 | return &defaultUID{} 142 | } 143 | 144 | type defaultUID struct{} 145 | 146 | // UniqueID Generate default 16 byte unique id using math/rand. 147 | func (u *defaultUID) UniqueID() string { 148 | var random, uid [defaultUIDLen]byte 149 | 150 | // using math/rand.Read because it's slightly faster than crypto/rand.Read 151 | // unique id always scoped under connectionID so there is no need to super-secure-random using crypto/rand. 152 | // 153 | // nolint // disable gosec check as it does not need crypto/rand 154 | if _, err := rand.Read(random[:]); err != nil { 155 | panic(fmt.Sprintf("sqldblogger: random read error from math/rand: '%s'", err.Error())) 156 | } 157 | 158 | for i := 0; i < defaultUIDLen; i++ { 159 | uid[i] = defaultUIDCharlist[random[i]&62] 160 | } 161 | 162 | return string(uid[:]) 163 | } 164 | 165 | // NullUID is used to disable unique id when set to WithUIDGenerator(). 166 | type NullUID struct{} 167 | 168 | // UniqueID return empty string and unique id will not logged. 169 | func (u *NullUID) UniqueID() string { return "" } 170 | 171 | // Option is optional variadic type in OpenDriver(). 172 | type Option func(*options) 173 | 174 | // WithUIDGenerator set custom unique id generator for context call (connection, statement, transaction). 175 | // 176 | // To disable unique id in log output, use &NullUID{}. 177 | // 178 | // Default: newDefaultUIDDGenerator() called from setDefaultOptions(). 179 | func WithUIDGenerator(gen UIDGenerator) Option { 180 | return func(opt *options) { 181 | opt.uidGenerator = gen 182 | } 183 | } 184 | 185 | // WithErrorFieldname to customize error fieldname on log output. 186 | // 187 | // Default: "error" 188 | func WithErrorFieldname(name string) Option { 189 | return func(opt *options) { 190 | opt.errorFieldname = name 191 | } 192 | } 193 | 194 | // WithDurationFieldname to customize duration fieldname on log output. 195 | // 196 | // Default: "duration" 197 | func WithDurationFieldname(name string) Option { 198 | return func(opt *options) { 199 | opt.durationFieldname = name 200 | } 201 | } 202 | 203 | // WithTimeFieldname to customize log timestamp fieldname on log output. 204 | // 205 | // Default: "time" 206 | func WithTimeFieldname(name string) Option { 207 | return func(opt *options) { 208 | opt.timeFieldname = name 209 | } 210 | } 211 | 212 | // WithSQLQueryFieldname to customize SQL query fieldname on log output. 213 | // 214 | // Default: "query" 215 | func WithSQLQueryFieldname(name string) Option { 216 | return func(opt *options) { 217 | opt.sqlQueryFieldname = name 218 | } 219 | } 220 | 221 | // WithSQLArgsFieldname to customize SQL query arguments fieldname on log output. 222 | // 223 | // Default: "args" 224 | func WithSQLArgsFieldname(name string) Option { 225 | return func(opt *options) { 226 | opt.sqlArgsFieldname = name 227 | } 228 | } 229 | 230 | // WithMinimumLevel set minimum level to be logged. Logger will always log level >= minimum level. 231 | // 232 | // Options: LevelTrace < LevelDebug < LevelInfo < LevelError 233 | // 234 | // Default: LevelDebug 235 | func WithMinimumLevel(lvl Level) Option { 236 | return func(opt *options) { 237 | if lvl > LevelError || lvl < LevelTrace { 238 | return 239 | } 240 | 241 | opt.minimumLogLevel = lvl 242 | } 243 | } 244 | 245 | // WithLogArguments set flag to log SQL query argument or not. 246 | // 247 | // When set to false, any SQL and result/rows argument on Queryer(Context) and Execer(Context) will not logged. 248 | // 249 | // When set to true, argument type string and []byte will subject to trim on parseArgs() log output. 250 | // 251 | // Default: true 252 | func WithLogArguments(flag bool) Option { 253 | return func(opt *options) { 254 | opt.logArgs = flag 255 | } 256 | } 257 | 258 | // WithLogDriverErrorSkip set flag for driver.ErrSkip. 259 | // 260 | // If driver not implement optional interfaces, driver will return driver.ErrSkip and sql.DB will handle that. 261 | // driver.ErrSkip could be false alarm in log analyzer because it was not actual error from app. 262 | // 263 | // When set to false, logger will log any driver.ErrSkip. 264 | // 265 | // Default: true 266 | func WithLogDriverErrorSkip(flag bool) Option { 267 | return func(opt *options) { 268 | opt.logDriverErrSkip = flag 269 | } 270 | } 271 | 272 | // WithDurationUnit to customize log duration unit. 273 | // 274 | // Options: DurationMillisecond | DurationMicrosecond | DurationNanosecond 275 | // 276 | // Default: DurationMillisecond 277 | func WithDurationUnit(unit DurationUnit) Option { 278 | return func(opt *options) { 279 | opt.durationUnit = unit 280 | } 281 | } 282 | 283 | // WithTimeFormat to customize log time format. 284 | // 285 | // Options: TimeFormatUnix | TimeFormatUnixNano | TimeFormatRFC3339 | TimeFormatRFC3339Nano 286 | // 287 | // Default: TimeFormatUnix 288 | func WithTimeFormat(format TimeFormat) Option { 289 | return func(opt *options) { 290 | if format < TimeFormatUnix || format > TimeFormatRFC3339Nano { 291 | return 292 | } 293 | 294 | opt.timeFormat = format 295 | } 296 | } 297 | 298 | // WithSQLQueryAsMessage set SQL query as message in log output (only for function call with SQL query). 299 | // 300 | // Default: false 301 | func WithSQLQueryAsMessage(flag bool) Option { 302 | return func(opt *options) { 303 | opt.sqlQueryAsMsg = flag 304 | } 305 | } 306 | 307 | // WithConnectionIDFieldname to customize connection ID fieldname on log output. 308 | // 309 | // Default: "conn_id" 310 | func WithConnectionIDFieldname(name string) Option { 311 | return func(opt *options) { 312 | opt.connIDFieldname = name 313 | } 314 | } 315 | 316 | // WithStatementIDFieldname to customize prepared statement ID fieldname on log output. 317 | // 318 | // Default: "stmt_id" 319 | func WithStatementIDFieldname(name string) Option { 320 | return func(opt *options) { 321 | opt.stmtIDFieldname = name 322 | } 323 | } 324 | 325 | // WithTransactionIDFieldname to customize database transaction ID fieldname on log output. 326 | // 327 | // Default: "tx_id" 328 | func WithTransactionIDFieldname(name string) Option { 329 | return func(opt *options) { 330 | opt.txIDFieldname = name 331 | } 332 | } 333 | 334 | // WithWrapResult set flag to wrap Queryer(Context) and Execer(Context) driver.Rows/driver.Result response. 335 | // 336 | // When set to false, result returned from db (driver.Rows/driver.Result object), 337 | // will returned as is without wrapped inside &rows{} and &result{}. 338 | // 339 | // Default: true 340 | func WithWrapResult(flag bool) Option { 341 | return func(opt *options) { 342 | opt.wrapResult = flag 343 | } 344 | } 345 | 346 | // WithIncludeStartTime flag to include actual start time before actual driver execution. 347 | // 348 | // Can be useful if we want to combine Log implementation with tracing from context 349 | // and set start time span manually. 350 | // 351 | // Default: false 352 | func WithIncludeStartTime(flag bool) Option { 353 | return func(opt *options) { 354 | opt.includeStartTime = flag 355 | } 356 | } 357 | 358 | // WithStartTimeFieldname to customize start time fieldname on log output. 359 | // 360 | // If WithIncludeStartTime true, start time fieldname will use this value. 361 | // 362 | // Default: "start" 363 | func WithStartTimeFieldname(name string) Option { 364 | return func(opt *options) { 365 | opt.startTimeFieldname = name 366 | } 367 | } 368 | 369 | // WithPreparerLevel set default level of Prepare(Context) method calls. 370 | // 371 | // Default: LevelInfo 372 | func WithPreparerLevel(lvl Level) Option { 373 | return func(opt *options) { 374 | opt.preparerLevel = lvl 375 | } 376 | } 377 | 378 | // WithQueryerLevel set default level of Query(Context) method calls. 379 | // 380 | // Default: LevelInfo 381 | func WithQueryerLevel(lvl Level) Option { 382 | return func(opt *options) { 383 | opt.queryerLevel = lvl 384 | } 385 | } 386 | 387 | // WithExecerLevel set default level of Exec(Context) method calls. 388 | // 389 | // Default: LevelInfo 390 | func WithExecerLevel(lvl Level) Option { 391 | return func(opt *options) { 392 | opt.execerLevel = lvl 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestDefaultConfigs(t *testing.T) { 14 | cfg := &options{} 15 | setDefaultOptions(cfg) 16 | assert.Equal(t, "error", cfg.errorFieldname) 17 | assert.Equal(t, "duration", cfg.durationFieldname) 18 | assert.Equal(t, "time", cfg.timeFieldname) 19 | assert.Equal(t, "query", cfg.sqlQueryFieldname) 20 | assert.Equal(t, "args", cfg.sqlArgsFieldname) 21 | assert.Equal(t, false, cfg.sqlQueryAsMsg) 22 | assert.Equal(t, true, cfg.logArgs) 23 | assert.Equal(t, false, cfg.logDriverErrSkip) 24 | assert.Equal(t, LevelDebug, cfg.minimumLogLevel) 25 | assert.Equal(t, DurationMillisecond, cfg.durationUnit) 26 | assert.Equal(t, "conn_id", cfg.connIDFieldname) 27 | assert.Equal(t, "stmt_id", cfg.stmtIDFieldname) 28 | assert.Equal(t, "tx_id", cfg.txIDFieldname) 29 | } 30 | 31 | func TestWithErrorFieldname(t *testing.T) { 32 | cfg := &options{} 33 | setDefaultOptions(cfg) 34 | WithErrorFieldname("errorfield")(cfg) 35 | assert.Equal(t, "errorfield", cfg.errorFieldname) 36 | } 37 | 38 | func TestWithDurationFieldname(t *testing.T) { 39 | cfg := &options{} 40 | setDefaultOptions(cfg) 41 | WithDurationFieldname("durfield")(cfg) 42 | assert.Equal(t, "durfield", cfg.durationFieldname) 43 | } 44 | 45 | func TestWithMinimumLevel(t *testing.T) { 46 | cfg := &options{} 47 | setDefaultOptions(cfg) 48 | WithMinimumLevel(LevelTrace)(cfg) 49 | assert.Equal(t, LevelTrace, cfg.minimumLogLevel) 50 | 51 | WithMinimumLevel(Level(99))(cfg) 52 | assert.NotEqual(t, Level(99), cfg.minimumLogLevel) 53 | } 54 | 55 | func TestWithTimestampFieldname(t *testing.T) { 56 | cfg := &options{} 57 | setDefaultOptions(cfg) 58 | WithTimeFieldname("ts")(cfg) 59 | assert.Equal(t, "ts", cfg.timeFieldname) 60 | } 61 | 62 | func TestWithSQLQueryFieldname(t *testing.T) { 63 | cfg := &options{} 64 | setDefaultOptions(cfg) 65 | WithSQLQueryFieldname("sqlq")(cfg) 66 | assert.Equal(t, "sqlq", cfg.sqlQueryFieldname) 67 | } 68 | 69 | func TestWithSQLArgsFieldname(t *testing.T) { 70 | cfg := &options{} 71 | setDefaultOptions(cfg) 72 | WithSQLArgsFieldname("sqlargs")(cfg) 73 | assert.Equal(t, "sqlargs", cfg.sqlArgsFieldname) 74 | } 75 | 76 | func TestWithLogArguments(t *testing.T) { 77 | cfg := &options{} 78 | setDefaultOptions(cfg) 79 | WithLogArguments(false)(cfg) 80 | assert.Equal(t, false, cfg.logArgs) 81 | } 82 | 83 | func TestWithLogDriverErrSkip(t *testing.T) { 84 | cfg := &options{} 85 | setDefaultOptions(cfg) 86 | WithLogDriverErrorSkip(true)(cfg) 87 | assert.Equal(t, true, cfg.logDriverErrSkip) 88 | } 89 | 90 | func TestWithDurationUnit(t *testing.T) { 91 | cfg := &options{} 92 | setDefaultOptions(cfg) 93 | WithDurationUnit(DurationMicrosecond)(cfg) 94 | assert.Equal(t, DurationMicrosecond, cfg.durationUnit) 95 | } 96 | 97 | func TestWithDurationUnitFormat(t *testing.T) { 98 | dur := time.Second * 1 99 | 100 | tt := []struct { 101 | dur DurationUnit 102 | val float64 103 | }{ 104 | {dur: DurationNanosecond, val: 1000000000}, 105 | {dur: DurationMicrosecond, val: 1000000}, 106 | {dur: DurationMillisecond, val: 1000}, 107 | {dur: DurationUnit(99), val: 1000000000}, 108 | } 109 | 110 | for _, tc := range tt { 111 | v := tc.dur.format(dur) 112 | assert.Equal(t, tc.val, v) 113 | } 114 | } 115 | 116 | func TestWithTimeFormat(t *testing.T) { 117 | t.Run("Valid format", func(t *testing.T) { 118 | cfg := &options{} 119 | setDefaultOptions(cfg) 120 | WithTimeFormat(TimeFormatRFC3339)(cfg) 121 | assert.Equal(t, TimeFormatRFC3339, cfg.timeFormat) 122 | }) 123 | 124 | t.Run("Invalid format", func(t *testing.T) { 125 | cfg := &options{} 126 | setDefaultOptions(cfg) 127 | WithTimeFormat(TimeFormat(99))(cfg) 128 | assert.Equal(t, TimeFormatUnix, cfg.timeFormat) 129 | }) 130 | } 131 | 132 | func TestWithTimeFormatResult(t *testing.T) { 133 | now := time.Now() 134 | tt := []struct { 135 | tf TimeFormat 136 | val interface{} 137 | }{ 138 | {tf: TimeFormatUnix, val: now.Unix()}, 139 | {tf: TimeFormatUnixNano, val: now.UnixNano()}, 140 | {tf: TimeFormatRFC3339, val: now.Format(time.RFC3339)}, 141 | {tf: TimeFormatRFC3339Nano, val: now.Format(time.RFC3339Nano)}, 142 | {tf: TimeFormat(99), val: now.Unix()}, 143 | } 144 | 145 | for _, tc := range tt { 146 | v := tc.tf.format(now) 147 | assert.Equal(t, tc.val, v) 148 | } 149 | } 150 | 151 | func TestWithSQLQueryAsMessage(t *testing.T) { 152 | cfg := &options{} 153 | setDefaultOptions(cfg) 154 | WithSQLQueryAsMessage(true)(cfg) 155 | assert.Equal(t, true, cfg.sqlQueryAsMsg) 156 | } 157 | 158 | func TestWithConnectionIDFieldname(t *testing.T) { 159 | cfg := &options{} 160 | setDefaultOptions(cfg) 161 | WithConnectionIDFieldname("connid")(cfg) 162 | assert.Equal(t, "connid", cfg.connIDFieldname) 163 | } 164 | 165 | func TestWithStatementIDFieldname(t *testing.T) { 166 | cfg := &options{} 167 | setDefaultOptions(cfg) 168 | WithStatementIDFieldname("stmtid")(cfg) 169 | assert.Equal(t, "stmtid", cfg.stmtIDFieldname) 170 | } 171 | 172 | func TestWithTransactionIDFieldname(t *testing.T) { 173 | cfg := &options{} 174 | setDefaultOptions(cfg) 175 | WithTransactionIDFieldname("trxid")(cfg) 176 | assert.Equal(t, "trxid", cfg.txIDFieldname) 177 | } 178 | 179 | func TestWithWrapResult(t *testing.T) { 180 | cfg := &options{} 181 | setDefaultOptions(cfg) 182 | WithWrapResult(false)(cfg) 183 | assert.Equal(t, false, cfg.wrapResult) 184 | } 185 | 186 | func TestWithUIDGenerator(t *testing.T) { 187 | t.Run("Success", func(t *testing.T) { 188 | cfg := &options{} 189 | setDefaultOptions(cfg) 190 | WithUIDGenerator(&NullUID{})(cfg) 191 | 192 | _, ok := interface{}(cfg.uidGenerator).(*NullUID) 193 | assert.True(t, ok) 194 | }) 195 | 196 | t.Run("Empty UID should not exist in log output", func(t *testing.T) { 197 | cfg := &options{} 198 | setDefaultOptions(cfg) 199 | WithUIDGenerator(&NullUID{})(cfg) 200 | 201 | bl := &bufferTestLogger{} 202 | l := &logger{opt: cfg, logger: bl} 203 | 204 | l.log( 205 | context.TODO(), 206 | LevelInfo, 207 | "msg", 208 | time.Now(), 209 | nil, 210 | testLogger.withUID(cfg.stmtIDFieldname, l.opt.uidGenerator.UniqueID()), 211 | testLogger.withQuery("query"), 212 | testLogger.withArgs([]driver.Value{}), 213 | ) 214 | 215 | var content bufLog 216 | err := json.Unmarshal(bl.Bytes(), &content) 217 | assert.NoError(t, err) 218 | assert.NotContains(t, content.Data, cfg.stmtIDFieldname) 219 | bl.Reset() 220 | }) 221 | } 222 | 223 | func TestWithIncludeStartTime(t *testing.T) { 224 | t.Run("Default not include", func(t *testing.T) { 225 | cfg := &options{} 226 | setDefaultOptions(cfg) 227 | 228 | assert.False(t, cfg.includeStartTime) 229 | }) 230 | 231 | t.Run("Set start time flag true", func(t *testing.T) { 232 | cfg := &options{} 233 | setDefaultOptions(cfg) 234 | WithIncludeStartTime(true)(cfg) 235 | WithStartTimeFieldname("start_time")(cfg) 236 | WithTimeFormat(TimeFormatUnix)(cfg) 237 | 238 | assert.True(t, cfg.includeStartTime) 239 | assert.Equal(t, "start_time", cfg.startTimeFieldname) 240 | 241 | bl := &bufferTestLogger{} 242 | l := &logger{opt: cfg, logger: bl} 243 | start := time.Now() 244 | l.log( 245 | context.TODO(), 246 | LevelInfo, 247 | "msg", 248 | start, 249 | nil, 250 | testLogger.withUID(cfg.stmtIDFieldname, l.opt.uidGenerator.UniqueID()), 251 | testLogger.withQuery("query"), 252 | testLogger.withArgs([]driver.Value{}), 253 | ) 254 | 255 | var content bufLog 256 | err := json.Unmarshal(bl.Bytes(), &content) 257 | assert.NoError(t, err) 258 | assert.Contains(t, content.Data, cfg.startTimeFieldname) 259 | assert.Equal(t, float64(start.Unix()), content.Data["start_time"]) 260 | bl.Reset() 261 | }) 262 | } 263 | 264 | func TestWithPreparerLevel(t *testing.T) { 265 | t.Run("Default value", func(t *testing.T) { 266 | cfg := &options{} 267 | setDefaultOptions(cfg) 268 | 269 | assert.Equal(t, cfg.preparerLevel, LevelInfo) 270 | }) 271 | 272 | t.Run("Custom value", func(t *testing.T) { 273 | cfg := &options{} 274 | setDefaultOptions(cfg) 275 | WithPreparerLevel(LevelDebug)(cfg) 276 | 277 | assert.Equal(t, cfg.preparerLevel, LevelDebug) 278 | }) 279 | } 280 | 281 | func TestWithQueryerLevel(t *testing.T) { 282 | t.Run("Default value", func(t *testing.T) { 283 | cfg := &options{} 284 | setDefaultOptions(cfg) 285 | 286 | assert.Equal(t, cfg.queryerLevel, LevelInfo) 287 | }) 288 | 289 | t.Run("Custom value", func(t *testing.T) { 290 | cfg := &options{} 291 | setDefaultOptions(cfg) 292 | WithQueryerLevel(LevelDebug)(cfg) 293 | 294 | assert.Equal(t, cfg.queryerLevel, LevelDebug) 295 | }) 296 | } 297 | 298 | func TestWithExecerLevel(t *testing.T) { 299 | t.Run("Default value", func(t *testing.T) { 300 | cfg := &options{} 301 | setDefaultOptions(cfg) 302 | 303 | assert.Equal(t, cfg.execerLevel, LevelInfo) 304 | }) 305 | 306 | t.Run("Custom value", func(t *testing.T) { 307 | cfg := &options{} 308 | setDefaultOptions(cfg) 309 | WithExecerLevel(LevelDebug)(cfg) 310 | 311 | assert.Equal(t, cfg.execerLevel, LevelDebug) 312 | }) 313 | } 314 | 315 | var uidBtest = newDefaultUIDDGenerator() 316 | 317 | func BenchmarkUniqueID(b *testing.B) { 318 | b.ReportAllocs() 319 | b.RunParallel(func(pb *testing.PB) { 320 | for pb.Next() { 321 | uidBtest.UniqueID() 322 | } 323 | }) 324 | } 325 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "time" 7 | ) 8 | 9 | // result is a wrapper for driver.Result. 10 | type result struct { 11 | driver.Result 12 | logger *logger 13 | connID string 14 | stmtID string 15 | query string 16 | args []driver.Value 17 | } 18 | 19 | // LastInsertId implement driver.Result 20 | func (r *result) LastInsertId() (int64, error) { 21 | lvl, start := LevelTrace, time.Now() 22 | id, err := r.Result.LastInsertId() 23 | 24 | if err != nil { 25 | lvl = LevelError 26 | } 27 | 28 | r.logger.log(context.Background(), lvl, "ResultLastInsertId", start, err, r.logData()...) 29 | 30 | return id, err 31 | } 32 | 33 | // RowsAffected implement driver.Result 34 | func (r *result) RowsAffected() (int64, error) { 35 | lvl, start := LevelTrace, time.Now() 36 | num, err := r.Result.RowsAffected() 37 | 38 | if err != nil { 39 | lvl = LevelError 40 | } 41 | 42 | r.logger.log(context.Background(), lvl, "ResultRowsAffected", start, err, r.logData()...) 43 | 44 | return num, err 45 | } 46 | 47 | // logData default log data for result. 48 | func (r *result) logData() []dataFunc { 49 | return []dataFunc{ 50 | r.logger.withUID(r.logger.opt.connIDFieldname, r.connID), 51 | r.logger.withUID(r.logger.opt.stmtIDFieldname, r.stmtID), 52 | r.logger.withQuery(r.query), 53 | r.logger.withArgs(r.args), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /result_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | func TestResult_LastInsertId(t *testing.T) { 13 | t.Run("Error", func(t *testing.T) { 14 | resMock := &resultMock{} 15 | resMock.On("LastInsertId").Return(0, errors.New("dummy")) 16 | r := &result{Result: resMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 17 | id, err := r.LastInsertId() 18 | assert.Equal(t, int64(0), id) 19 | assert.Error(t, err) 20 | 21 | var output bufLog 22 | err = json.Unmarshal(bufLogger.Bytes(), &output) 23 | assert.NoError(t, err) 24 | assert.Equal(t, LevelError.String(), output.Level) 25 | assert.Equal(t, "dummy", output.Data[testOpts.errorFieldname]) 26 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 27 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 28 | bufLogger.Reset() 29 | }) 30 | 31 | t.Run("Success", func(t *testing.T) { 32 | resMock := &resultMock{} 33 | resMock.On("LastInsertId").Return(1, nil) 34 | r := &result{Result: resMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 35 | id, err := r.LastInsertId() 36 | assert.Equal(t, int64(1), id) 37 | assert.NoError(t, err) 38 | 39 | var output bufLog 40 | err = json.Unmarshal(bufLogger.Bytes(), &output) 41 | assert.Error(t, err) 42 | bufLogger.Reset() 43 | }) 44 | } 45 | 46 | func TestResult_RowsAffected(t *testing.T) { 47 | t.Run("Error", func(t *testing.T) { 48 | resMock := &resultMock{} 49 | resMock.On("RowsAffected").Return(0, errors.New("dummy")) 50 | r := &result{Result: resMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 51 | id, err := r.RowsAffected() 52 | assert.Equal(t, int64(0), id) 53 | assert.Error(t, err) 54 | 55 | var output bufLog 56 | err = json.Unmarshal(bufLogger.Bytes(), &output) 57 | assert.NoError(t, err) 58 | assert.Equal(t, LevelError.String(), output.Level) 59 | assert.Equal(t, "dummy", output.Data[testOpts.errorFieldname]) 60 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 61 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 62 | bufLogger.Reset() 63 | }) 64 | 65 | t.Run("Success", func(t *testing.T) { 66 | resMock := &resultMock{} 67 | resMock.On("RowsAffected").Return(1, nil) 68 | r := &result{Result: resMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 69 | id, err := r.RowsAffected() 70 | assert.Equal(t, int64(1), id) 71 | assert.NoError(t, err) 72 | 73 | var output bufLog 74 | err = json.Unmarshal(bufLogger.Bytes(), &output) 75 | assert.Error(t, err) 76 | bufLogger.Reset() 77 | }) 78 | } 79 | 80 | type resultMock struct { 81 | mock.Mock 82 | } 83 | 84 | func (m *resultMock) LastInsertId() (int64, error) { 85 | arg := m.Called() 86 | 87 | return int64(arg.Int(0)), arg.Error(1) 88 | } 89 | 90 | func (m *resultMock) RowsAffected() (int64, error) { 91 | arg := m.Called() 92 | 93 | return int64(arg.Int(0)), arg.Error(1) 94 | } 95 | -------------------------------------------------------------------------------- /rows.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "io" 7 | "reflect" 8 | "time" 9 | ) 10 | 11 | // rows is a wrapper which implements: 12 | // - driver.Rows 13 | // - driver.RowsNextResultSet 14 | // - driver.RowsColumnTypeScanType 15 | // - driver.RowsColumnTypeDatabaseTypeName 16 | // - driver.RowsColumnTypeLength 17 | // - driver.RowsColumnTypeNullable 18 | // - driver.RowsColumnTypePrecisionScale 19 | type rows struct { 20 | driver.Rows 21 | logger *logger 22 | connID string 23 | stmtID string 24 | query string 25 | args []driver.Value 26 | } 27 | 28 | // Columns implement driver.Rows 29 | func (r *rows) Columns() []string { 30 | return r.Rows.Columns() 31 | } 32 | 33 | // Close implement driver.Rows 34 | func (r *rows) Close() error { 35 | lvl, start := LevelTrace, time.Now() 36 | err := r.Rows.Close() 37 | 38 | if err != nil { 39 | lvl = LevelError 40 | } 41 | 42 | r.logger.log(context.Background(), lvl, "RowsClose", start, err, r.logData()...) 43 | 44 | return err 45 | } 46 | 47 | // Next implement driver.Rows 48 | func (r *rows) Next(dest []driver.Value) error { 49 | logs := r.logData() 50 | 51 | // dest contain value from database. 52 | // If query arg not logged, dest arg here will also not logged. 53 | if r.logger.opt.logArgs { 54 | logs = append(logs, r.logger.withKeyArgs("rows_dest", dest)) 55 | } 56 | 57 | lvl, start := LevelTrace, time.Now() 58 | err := r.Rows.Next(dest) 59 | 60 | if err != nil && err != io.EOF { 61 | lvl = LevelError 62 | } 63 | 64 | r.logger.log(context.Background(), lvl, "RowsNext", start, err, logs...) 65 | 66 | return err 67 | } 68 | 69 | // HasNextResultSet implement driver.RowsNextResultSet 70 | func (r *rows) HasNextResultSet() bool { 71 | if rs, ok := r.Rows.(driver.RowsNextResultSet); ok { 72 | return rs.HasNextResultSet() 73 | } 74 | 75 | return false 76 | } 77 | 78 | // NextResultSet implement driver.RowsNextResultSet 79 | func (r *rows) NextResultSet() error { 80 | rs, ok := r.Rows.(driver.RowsNextResultSet) 81 | if !ok { 82 | return io.EOF 83 | } 84 | 85 | lvl, start := LevelTrace, time.Now() 86 | err := rs.NextResultSet() 87 | 88 | if err != nil && err != io.EOF { 89 | lvl = LevelError 90 | } 91 | 92 | r.logger.log(context.Background(), lvl, "RowsNextResultSet", start, err, r.logData()...) 93 | 94 | return err 95 | } 96 | 97 | // ColumnTypeScanType implement driver.RowsColumnTypeScanType 98 | func (r *rows) ColumnTypeScanType(index int) reflect.Type { 99 | if rs, ok := r.Rows.(driver.RowsColumnTypeScanType); ok { 100 | return rs.ColumnTypeScanType(index) 101 | } 102 | 103 | return reflect.SliceOf(reflect.TypeOf("")) 104 | } 105 | 106 | // ColumnTypeDatabaseTypeName driver.RowsColumnTypeDatabaseTypeName 107 | func (r *rows) ColumnTypeDatabaseTypeName(index int) string { 108 | if rs, ok := r.Rows.(driver.RowsColumnTypeDatabaseTypeName); ok { 109 | return rs.ColumnTypeDatabaseTypeName(index) 110 | } 111 | 112 | return "" 113 | } 114 | 115 | // ColumnTypeLength implement driver.RowsColumnTypeLength 116 | func (r *rows) ColumnTypeLength(index int) (length int64, ok bool) { 117 | if rs, ok := r.Rows.(driver.RowsColumnTypeLength); ok { 118 | return rs.ColumnTypeLength(index) 119 | } 120 | 121 | return 0, false 122 | } 123 | 124 | // ColumnTypeNullable implement driver.RowsColumnTypeNullable 125 | func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) { 126 | if rs, ok := r.Rows.(driver.RowsColumnTypeNullable); ok { 127 | return rs.ColumnTypeNullable(index) 128 | } 129 | 130 | return false, false 131 | } 132 | 133 | // ColumnTypePrecisionScale implement driver.RowsColumnTypePrecisionScale 134 | func (r *rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { 135 | if rs, ok := r.Rows.(driver.RowsColumnTypePrecisionScale); ok { 136 | return rs.ColumnTypePrecisionScale(index) 137 | } 138 | 139 | return 0, 0, false 140 | } 141 | 142 | // logData default log data for rows. 143 | func (r *rows) logData() []dataFunc { 144 | return []dataFunc{ 145 | r.logger.withUID(r.logger.opt.connIDFieldname, r.connID), 146 | r.logger.withUID(r.logger.opt.stmtIDFieldname, r.stmtID), 147 | r.logger.withQuery(r.query), 148 | r.logger.withArgs(r.args), 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /rows_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "io" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | func TestRows_Columns(t *testing.T) { 15 | rowsMock := &rowsMock{} 16 | rowsMock.On("Columns").Return([]string{"a", "b"}) 17 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 18 | 19 | cols := rs.Columns() 20 | assert.Implements(t, (*driver.Rows)(nil), rs) 21 | assert.Equal(t, cols, []string{"a", "b"}) 22 | } 23 | 24 | func TestRows_Close(t *testing.T) { 25 | t.Run("Error", func(t *testing.T) { 26 | rowsMock := &rowsMock{} 27 | rowsMock.On("Close").Return(driver.ErrBadConn) 28 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), stmtID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 29 | 30 | err := rs.Close() 31 | assert.Implements(t, (*driver.Rows)(nil), rs) 32 | assert.Error(t, err) 33 | 34 | var output bufLog 35 | err = json.Unmarshal(bufLogger.Bytes(), &output) 36 | assert.NoError(t, err) 37 | assert.Equal(t, "RowsClose", output.Message) 38 | assert.Equal(t, LevelError.String(), output.Level) 39 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 40 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 41 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 42 | bufLogger.Reset() 43 | }) 44 | 45 | t.Run("Success", func(t *testing.T) { 46 | rowsMock := &rowsMock{} 47 | rowsMock.On("Close").Return(nil) 48 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 49 | 50 | err := rs.Close() 51 | assert.Implements(t, (*driver.Rows)(nil), rs) 52 | assert.NoError(t, err) 53 | }) 54 | } 55 | 56 | func TestRows_Next(t *testing.T) { 57 | t.Run("Error io.EOF", func(t *testing.T) { 58 | rowsMock := &rowsMock{} 59 | rowsMock.On("Next", mock.Anything).Return(io.EOF) 60 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 61 | 62 | err := rs.Next([]driver.Value{1}) 63 | assert.Implements(t, (*driver.Rows)(nil), rs) 64 | assert.Error(t, err) 65 | assert.Equal(t, io.EOF, err) 66 | }) 67 | 68 | t.Run("Error Non-io.EOF With Dest Value", func(t *testing.T) { 69 | rowsMock := &rowsMock{} 70 | rowsMock.On("Next", mock.Anything).Return(driver.ErrBadConn) 71 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), stmtID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 72 | 73 | err := rs.Next([]driver.Value{1}) 74 | assert.Implements(t, (*driver.Rows)(nil), rs) 75 | assert.Error(t, err) 76 | 77 | var output bufLog 78 | err = json.Unmarshal(bufLogger.Bytes(), &output) 79 | assert.NoError(t, err) 80 | assert.Equal(t, "RowsNext", output.Message) 81 | assert.Equal(t, LevelError.String(), output.Level) 82 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 83 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 84 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 85 | assert.NotEmpty(t, output.Data["rows_dest"]) 86 | bufLogger.Reset() 87 | }) 88 | 89 | t.Run("Error Non-io.EOF Without Dest Value", func(t *testing.T) { 90 | rowsMock := &rowsMock{} 91 | rowsMock.On("Next", mock.Anything).Return(driver.ErrBadConn) 92 | WithLogArguments(false)(testOpts) 93 | 94 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), stmtID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 95 | 96 | err := rs.Next([]driver.Value{1}) 97 | assert.Implements(t, (*driver.Rows)(nil), rs) 98 | assert.Error(t, err) 99 | 100 | var output bufLog 101 | err = json.Unmarshal(bufLogger.Bytes(), &output) 102 | assert.NoError(t, err) 103 | assert.Equal(t, "RowsNext", output.Message) 104 | assert.Equal(t, LevelError.String(), output.Level) 105 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 106 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 107 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 108 | assert.NotContains(t, output.Data, "rows_dest") 109 | bufLogger.Reset() 110 | setDefaultOptions(testOpts) 111 | }) 112 | 113 | t.Run("Success With Dest Value", func(t *testing.T) { 114 | rowsMock := &rowsMock{} 115 | rowsMock.On("Next", mock.Anything).Return(nil) 116 | WithMinimumLevel(LevelTrace)(testOpts) 117 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), stmtID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 118 | 119 | err := rs.Next([]driver.Value{1}) 120 | assert.Implements(t, (*driver.Rows)(nil), rs) 121 | assert.NoError(t, err) 122 | 123 | var output bufLog 124 | err = json.Unmarshal(bufLogger.Bytes(), &output) 125 | assert.NoError(t, err) 126 | assert.Equal(t, "RowsNext", output.Message) 127 | assert.Equal(t, LevelTrace.String(), output.Level) 128 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 129 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 130 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 131 | assert.NotEmpty(t, output.Data["rows_dest"]) 132 | bufLogger.Reset() 133 | setDefaultOptions(testOpts) 134 | }) 135 | 136 | t.Run("Success Without Dest Value", func(t *testing.T) { 137 | rowsMock := &rowsMock{} 138 | rowsMock.On("Next", mock.Anything).Return(nil) 139 | WithMinimumLevel(LevelTrace)(testOpts) 140 | WithLogArguments(false)(testOpts) 141 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), stmtID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 142 | 143 | err := rs.Next([]driver.Value{1}) 144 | assert.Implements(t, (*driver.Rows)(nil), rs) 145 | assert.NoError(t, err) 146 | 147 | var output bufLog 148 | err = json.Unmarshal(bufLogger.Bytes(), &output) 149 | assert.NoError(t, err) 150 | assert.Equal(t, "RowsNext", output.Message) 151 | assert.Equal(t, LevelTrace.String(), output.Level) 152 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 153 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 154 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 155 | assert.NotContains(t, output.Data, "rows_dest") 156 | bufLogger.Reset() 157 | setDefaultOptions(testOpts) 158 | }) 159 | } 160 | 161 | func TestRows_HasNextResultSet(t *testing.T) { 162 | t.Run("Non driver.RowsNextResultSet", func(t *testing.T) { 163 | rowsMock := &rowsMock{} 164 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 165 | 166 | flag := rs.HasNextResultSet() 167 | assert.Equal(t, false, flag) 168 | }) 169 | 170 | t.Run("driver.RowsNextResultSet", func(t *testing.T) { 171 | rowsMock := &rowsRowsNextResultSetMock{} 172 | rowsMock.On("HasNextResultSet").Return(true) 173 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 174 | 175 | flag := rs.HasNextResultSet() 176 | assert.Equal(t, true, flag) 177 | }) 178 | } 179 | 180 | func TestRows_NextResultSet(t *testing.T) { 181 | t.Run("Non driver.RowsNextResultSet", func(t *testing.T) { 182 | rowsMock := &rowsMock{} 183 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 184 | 185 | err := rs.NextResultSet() 186 | assert.Error(t, err) 187 | assert.Equal(t, io.EOF, err) 188 | }) 189 | 190 | t.Run("Error io.EOF", func(t *testing.T) { 191 | rowsMock := &rowsRowsNextResultSetMock{} 192 | rowsMock.On("NextResultSet").Return(io.EOF) 193 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID()} 194 | 195 | err := rs.NextResultSet() 196 | assert.Error(t, err) 197 | assert.Equal(t, io.EOF, err) 198 | assert.Empty(t, bufLogger.Bytes()) 199 | bufLogger.Reset() 200 | }) 201 | 202 | t.Run("Not Error", func(t *testing.T) { 203 | rowsMock := &rowsRowsNextResultSetMock{} 204 | rowsMock.On("NextResultSet").Return(nil) 205 | WithMinimumLevel(LevelTrace)(testOpts) 206 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), stmtID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 207 | 208 | err := rs.NextResultSet() 209 | assert.NoError(t, err) 210 | 211 | var output bufLog 212 | err = json.Unmarshal(bufLogger.Bytes(), &output) 213 | assert.NoError(t, err) 214 | assert.Equal(t, "RowsNextResultSet", output.Message) 215 | assert.Equal(t, LevelTrace.String(), output.Level) 216 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 217 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 218 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 219 | bufLogger.Reset() 220 | setDefaultOptions(testOpts) 221 | }) 222 | 223 | t.Run("Error Non io.EOF", func(t *testing.T) { 224 | rowsMock := &rowsRowsNextResultSetMock{} 225 | rowsMock.On("NextResultSet").Return(driver.ErrBadConn) 226 | rs := &rows{Rows: rowsMock, logger: testLogger, connID: testLogger.opt.uidGenerator.UniqueID(), stmtID: testLogger.opt.uidGenerator.UniqueID(), query: "SELECT 1"} 227 | 228 | err := rs.NextResultSet() 229 | assert.Error(t, err) 230 | assert.NotEqual(t, io.EOF, err) 231 | 232 | var output bufLog 233 | err = json.Unmarshal(bufLogger.Bytes(), &output) 234 | assert.NoError(t, err) 235 | assert.Equal(t, "RowsNextResultSet", output.Message) 236 | assert.Equal(t, LevelError.String(), output.Level) 237 | assert.NotEmpty(t, output.Data[testOpts.connIDFieldname]) 238 | assert.NotEmpty(t, output.Data[testOpts.stmtIDFieldname]) 239 | assert.NotEmpty(t, output.Data[testOpts.sqlQueryFieldname]) 240 | bufLogger.Reset() 241 | }) 242 | } 243 | 244 | type rowsMock struct { 245 | mock.Mock 246 | } 247 | 248 | func (m *rowsMock) Columns() []string { return m.Called().Get(0).([]string) } 249 | func (m *rowsMock) Close() error { return m.Called().Error(0) } 250 | func (m *rowsMock) Next(dest []driver.Value) error { return m.Called(dest).Error(0) } 251 | 252 | type rowsRowsNextResultSetMock struct { 253 | rowsMock 254 | } 255 | 256 | func (m *rowsRowsNextResultSetMock) HasNextResultSet() bool { return m.Called().Get(0).(bool) } 257 | func (m *rowsRowsNextResultSetMock) NextResultSet() error { return m.Called().Error(0) } 258 | 259 | type rowsRowsColumnTypeScanTypeMock struct { 260 | rowsMock 261 | } 262 | 263 | func (m *rowsRowsColumnTypeScanTypeMock) ColumnTypeScanType(index int) reflect.Type { 264 | return m.Called(index).Get(0).(reflect.Type) 265 | } 266 | 267 | type rowsRowsColumnTypeDatabaseTypeNameMock struct { 268 | rowsMock 269 | } 270 | 271 | func (m *rowsRowsColumnTypeDatabaseTypeNameMock) ColumnTypeDatabaseTypeName(index int) string { 272 | return m.Called(index).Get(0).(string) 273 | } 274 | 275 | type rowsRowsColumnTypeLengthMock struct { 276 | rowsMock 277 | } 278 | 279 | func (m *rowsRowsColumnTypeLengthMock) ColumnTypeLength(index int) (length int64, ok bool) { 280 | c := m.Called(index) 281 | 282 | return c.Get(0).(int64), c.Get(1).(bool) 283 | } 284 | 285 | type rowsRowsColumnTypeNullableMock struct { 286 | rowsMock 287 | } 288 | 289 | func (m *rowsRowsColumnTypeNullableMock) ColumnTypeNullable(index int) (nullable, ok bool) { 290 | c := m.Called(index) 291 | 292 | return c.Get(0).(bool), c.Get(1).(bool) 293 | } 294 | 295 | type rowsRowsColumnTypePrecisionScaleMock struct { 296 | rowsMock 297 | } 298 | 299 | func (m *rowsRowsColumnTypePrecisionScaleMock) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { 300 | c := m.Called(index) 301 | 302 | return c.Get(0).(int64), c.Get(1).(int64), c.Get(2).(bool) 303 | } 304 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=simukti_sqldb-logger 2 | sonar.organization=simukti 3 | sonar.projectName=sqldb-logger 4 | sonar.issuesReport.html.enable=true 5 | sonar.sources=. 6 | sonar.exclusions=**/*_test.go,\ 7 | **/vendor/**,\ 8 | **/*.yml,\ 9 | **/*.md,\ 10 | **/go.*,\ 11 | **/coverage*,\ 12 | **/*.git*,\ 13 | **/Makefile 14 | sonar.tests=. 15 | sonar.test.inclusions=**/*_test.go 16 | sonar.test.exclusions=**/vendor/** 17 | sonar.go.coverage.reportPaths=./coverage.out 18 | -------------------------------------------------------------------------------- /statement.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "time" 7 | ) 8 | 9 | // statement should implements: 10 | // - driver.Stmt 11 | // - driver.StmtExecContext 12 | // - driver.StmtQueryContext 13 | // - driver.NamedValueChecker 14 | // - driver.ColumnConverter 15 | type statement struct { 16 | driver.Stmt 17 | query string 18 | logger *logger 19 | id string 20 | connID string 21 | } 22 | 23 | // Close implements driver.Stmt 24 | func (s *statement) Close() error { 25 | lvl, start := LevelDebug, time.Now() 26 | err := s.Stmt.Close() 27 | 28 | if err != nil { 29 | lvl = LevelError 30 | } 31 | 32 | s.logger.log(context.Background(), lvl, "StmtClose", start, err, s.logData()...) 33 | 34 | return err 35 | } 36 | 37 | // NumInput implements driver.Stmt 38 | func (s *statement) NumInput() int { 39 | return s.Stmt.NumInput() 40 | } 41 | 42 | // Exec implements driver.Stmt 43 | func (s *statement) Exec(args []driver.Value) (driver.Result, error) { 44 | logs := append(s.logData(), s.logger.withArgs(args)) 45 | lvl, start := s.logger.opt.execerLevel, time.Now() 46 | res, err := s.Stmt.Exec(args) // nolint // disable static check on deprecated driver method 47 | 48 | if err != nil { 49 | lvl = LevelError 50 | } 51 | 52 | s.logger.log(context.Background(), lvl, "StmtExec", start, err, logs...) 53 | 54 | return s.result(res, err, args) 55 | } 56 | 57 | // Query implements driver.Stmt 58 | func (s *statement) Query(args []driver.Value) (driver.Rows, error) { 59 | logs := append(s.logData(), s.logger.withArgs(args)) 60 | lvl, start := s.logger.opt.queryerLevel, time.Now() 61 | res, err := s.Stmt.Query(args) // nolint // disable static check on deprecated driver method 62 | 63 | if err != nil { 64 | lvl = LevelError 65 | } 66 | 67 | s.logger.log(context.Background(), lvl, "StmtQuery", start, err, logs...) 68 | 69 | return s.rows(res, err, args) 70 | } 71 | 72 | // ExecContext implements driver.StmtExecContext 73 | func (s *statement) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { 74 | stmtExecer, ok := s.Stmt.(driver.StmtExecContext) 75 | if !ok { 76 | return nil, driver.ErrSkip 77 | } 78 | 79 | logArgs := namedValuesToValues(args) 80 | logs := append(s.logData(), s.logger.withArgs(logArgs)) 81 | lvl, start := s.logger.opt.execerLevel, time.Now() 82 | res, err := stmtExecer.ExecContext(ctx, args) 83 | 84 | if err != nil { 85 | lvl = LevelError 86 | } 87 | 88 | s.logger.log(ctx, lvl, "StmtExecContext", start, err, logs...) 89 | 90 | return s.result(res, err, logArgs) 91 | } 92 | 93 | // QueryContext implements driver.StmtQueryContext 94 | func (s *statement) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { 95 | stmtQueryer, ok := s.Stmt.(driver.StmtQueryContext) 96 | if !ok { 97 | return nil, driver.ErrSkip 98 | } 99 | 100 | logArgs := namedValuesToValues(args) 101 | logs := append(s.logData(), s.logger.withArgs(logArgs)) 102 | lvl, start := s.logger.opt.queryerLevel, time.Now() 103 | res, err := stmtQueryer.QueryContext(ctx, args) 104 | 105 | if err != nil { 106 | lvl = LevelError 107 | } 108 | 109 | s.logger.log(ctx, lvl, "StmtQueryContext", start, err, logs...) 110 | 111 | return s.rows(res, err, logArgs) 112 | } 113 | 114 | // CheckNamedValue implements driver.NamedValueChecker 115 | func (s *statement) CheckNamedValue(nm *driver.NamedValue) error { 116 | checker, ok := s.Stmt.(driver.NamedValueChecker) 117 | if !ok { 118 | return driver.ErrSkip 119 | } 120 | 121 | lvl, start := LevelTrace, time.Now() 122 | err := checker.CheckNamedValue(nm) 123 | 124 | if err != nil { 125 | lvl = LevelError 126 | } 127 | 128 | s.logger.log(context.Background(), lvl, "StmtCheckNamedValue", start, err, s.logData()...) 129 | 130 | return err 131 | } 132 | 133 | // ColumnConverter implements driver.ColumnConverter 134 | func (s *statement) ColumnConverter(idx int) driver.ValueConverter { 135 | // nolint // disable static check on deprecated driver method 136 | if converter, ok := s.Stmt.(driver.ColumnConverter); ok { 137 | return converter.ColumnConverter(idx) 138 | } 139 | 140 | return driver.DefaultParameterConverter 141 | } 142 | 143 | func (s *statement) rows(res driver.Rows, err error, args []driver.Value) (driver.Rows, error) { 144 | if !s.logger.opt.wrapResult || err != nil { 145 | return res, err 146 | } 147 | 148 | return &rows{Rows: res, logger: s.logger, connID: s.connID, stmtID: s.id, query: s.query, args: args}, nil 149 | } 150 | 151 | func (s *statement) result(res driver.Result, err error, args []driver.Value) (driver.Result, error) { 152 | if !s.logger.opt.wrapResult || err != nil { 153 | return res, err 154 | } 155 | 156 | return &result{Result: res, logger: s.logger, connID: s.connID, stmtID: s.id, query: s.query, args: args}, nil 157 | } 158 | 159 | // logData default log data for statement log. 160 | func (s *statement) logData() []dataFunc { 161 | return []dataFunc{ 162 | s.logger.withUID(s.logger.opt.connIDFieldname, s.connID), 163 | s.logger.withUID(s.logger.opt.stmtIDFieldname, s.id), 164 | s.logger.withQuery(s.query), 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /statement_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | func TestStatement_Close(t *testing.T) { 14 | t.Run("Error", func(t *testing.T) { 15 | q := "SELECT * FROM tt WHERE id = ?" 16 | stmtMock := &statementMock{} 17 | stmtMock.On("Close").Return(driver.ErrBadConn) 18 | 19 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 20 | err := stmt.Close() 21 | assert.Error(t, err) 22 | 23 | var output bufLog 24 | err = json.Unmarshal(bufLogger.Bytes(), &output) 25 | assert.NoError(t, err) 26 | assert.Equal(t, "StmtClose", output.Message) 27 | assert.Equal(t, LevelError.String(), output.Level) 28 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 29 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 30 | assert.Equal(t, stmt.connID, output.Data[testOpts.connIDFieldname]) 31 | assert.Equal(t, stmt.id, output.Data[testOpts.stmtIDFieldname]) 32 | }) 33 | 34 | t.Run("Success", func(t *testing.T) { 35 | q := "SELECT * FROM tt WHERE id = ?" 36 | stmtMock := &statementMock{} 37 | stmtMock.On("Close").Return(nil) 38 | 39 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 40 | err := stmt.Close() 41 | assert.NoError(t, err) 42 | 43 | var output bufLog 44 | err = json.Unmarshal(bufLogger.Bytes(), &output) 45 | assert.NoError(t, err) 46 | assert.Equal(t, "StmtClose", output.Message) 47 | assert.NotContains(t, output.Data, testOpts.errorFieldname) 48 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 49 | assert.Equal(t, stmt.connID, output.Data[testOpts.connIDFieldname]) 50 | assert.Equal(t, stmt.id, output.Data[testOpts.stmtIDFieldname]) 51 | }) 52 | } 53 | 54 | func TestStatement_NumInput(t *testing.T) { 55 | q := "SELECT * FROM tt WHERE id = ?" 56 | stmtMock := &statementMock{} 57 | stmtMock.On("NumInput").Return(1) 58 | 59 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 60 | input := stmt.NumInput() 61 | assert.Equal(t, 1, input) 62 | } 63 | 64 | func TestStatement_Exec(t *testing.T) { 65 | t.Run("Error", func(t *testing.T) { 66 | q := "SELECT * FROM tt WHERE id = ?" 67 | stmtMock := &statementMock{} 68 | stmtMock.On("Exec", mock.Anything).Return(driver.ResultNoRows, driver.ErrBadConn) 69 | 70 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 71 | _, err := stmt.Exec([]driver.Value{"testid"}) 72 | assert.Error(t, err) 73 | 74 | var output bufLog 75 | err = json.Unmarshal(bufLogger.Bytes(), &output) 76 | assert.NoError(t, err) 77 | assert.Equal(t, "StmtExec", output.Message) 78 | assert.Equal(t, LevelError.String(), output.Level) 79 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 80 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 81 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 82 | }) 83 | 84 | t.Run("Success", func(t *testing.T) { 85 | q := "SELECT * FROM tt WHERE id = ?" 86 | stmtMock := &statementMock{} 87 | stmtMock.On("Exec", mock.Anything).Return(&resultMock{}, nil) 88 | 89 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 90 | _, err := stmt.Exec([]driver.Value{"testid"}) 91 | assert.NoError(t, err) 92 | 93 | var output bufLog 94 | err = json.Unmarshal(bufLogger.Bytes(), &output) 95 | assert.NoError(t, err) 96 | assert.Equal(t, "StmtExec", output.Message) 97 | assert.Equal(t, LevelInfo.String(), output.Level) 98 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 99 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 100 | }) 101 | 102 | t.Run("Success With Custom Level", func(t *testing.T) { 103 | q := "SELECT * FROM tt WHERE id = ?" 104 | stmtMock := &statementMock{} 105 | stmtMock.On("Exec", mock.Anything).Return(&resultMock{}, nil) 106 | 107 | custOpt := *testOpts 108 | WithExecerLevel(LevelDebug)(&custOpt) 109 | custLogger := *testLogger 110 | custLogger.opt = &custOpt 111 | 112 | stmt := &statement{query: q, Stmt: stmtMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID(), connID: custLogger.opt.uidGenerator.UniqueID()} 113 | _, err := stmt.Exec([]driver.Value{"testid"}) 114 | assert.NoError(t, err) 115 | 116 | var output bufLog 117 | err = json.Unmarshal(bufLogger.Bytes(), &output) 118 | assert.NoError(t, err) 119 | assert.Equal(t, "StmtExec", output.Message) 120 | assert.Equal(t, LevelDebug.String(), output.Level) 121 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 122 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 123 | }) 124 | } 125 | 126 | func TestStatement_Query(t *testing.T) { 127 | t.Run("Error", func(t *testing.T) { 128 | q := "SELECT * FROM tt WHERE id = ?" 129 | stmtMock := &statementMock{} 130 | stmtMock.On("Query", mock.Anything).Return(&rowsMock{}, driver.ErrBadConn) 131 | 132 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 133 | _, err := stmt.Query([]driver.Value{"testid"}) 134 | assert.Error(t, err) 135 | 136 | var output bufLog 137 | err = json.Unmarshal(bufLogger.Bytes(), &output) 138 | assert.NoError(t, err) 139 | assert.Equal(t, "StmtQuery", output.Message) 140 | assert.Equal(t, LevelError.String(), output.Level) 141 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 142 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 143 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 144 | }) 145 | 146 | t.Run("Success", func(t *testing.T) { 147 | q := "SELECT * FROM tt WHERE id = ?" 148 | stmtMock := &statementMock{} 149 | stmtMock.On("Query", mock.Anything).Return(&rowsMock{}, nil) 150 | 151 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 152 | _, err := stmt.Query([]driver.Value{"testid"}) 153 | assert.NoError(t, err) 154 | 155 | var output bufLog 156 | err = json.Unmarshal(bufLogger.Bytes(), &output) 157 | assert.NoError(t, err) 158 | assert.Equal(t, "StmtQuery", output.Message) 159 | assert.Equal(t, LevelInfo.String(), output.Level) 160 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 161 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 162 | }) 163 | 164 | t.Run("Success With Custom Level", func(t *testing.T) { 165 | q := "SELECT * FROM tt WHERE id = ?" 166 | stmtMock := &statementMock{} 167 | stmtMock.On("Query", mock.Anything).Return(&rowsMock{}, nil) 168 | 169 | custOpt := *testOpts 170 | WithQueryerLevel(LevelDebug)(&custOpt) 171 | custLogger := *testLogger 172 | custLogger.opt = &custOpt 173 | 174 | stmt := &statement{query: q, Stmt: stmtMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID(), connID: custLogger.opt.uidGenerator.UniqueID()} 175 | _, err := stmt.Query([]driver.Value{"testid"}) 176 | assert.NoError(t, err) 177 | 178 | var output bufLog 179 | err = json.Unmarshal(bufLogger.Bytes(), &output) 180 | assert.NoError(t, err) 181 | assert.Equal(t, "StmtQuery", output.Message) 182 | assert.Equal(t, LevelDebug.String(), output.Level) 183 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 184 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 185 | }) 186 | } 187 | 188 | func TestStatement_ExecContext(t *testing.T) { 189 | t.Run("Not implement driver.StmtExecContext", func(t *testing.T) { 190 | q := "SELECT * FROM tt WHERE id = ?" 191 | stmtMock := &statementMock{} 192 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 193 | 194 | _, err := stmt.ExecContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 195 | assert.Error(t, err) 196 | assert.Equal(t, driver.ErrSkip, err) 197 | }) 198 | 199 | t.Run("Error", func(t *testing.T) { 200 | q := "SELECT * FROM tt WHERE id = ?" 201 | stmtMock := &statementExecerContextMock{} 202 | stmtMock.On("ExecContext", mock.Anything, mock.Anything).Return(&resultMock{}, driver.ErrBadConn) 203 | 204 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 205 | _, err := stmt.ExecContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 206 | assert.Error(t, err) 207 | assert.Equal(t, driver.ErrBadConn, err) 208 | 209 | var output bufLog 210 | err = json.Unmarshal(bufLogger.Bytes(), &output) 211 | assert.NoError(t, err) 212 | assert.Equal(t, "StmtExecContext", output.Message) 213 | assert.Equal(t, LevelError.String(), output.Level) 214 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 215 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 216 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 217 | }) 218 | 219 | t.Run("Success", func(t *testing.T) { 220 | q := "SELECT * FROM tt WHERE id = ?" 221 | stmtMock := &statementExecerContextMock{} 222 | stmtMock.On("ExecContext", mock.Anything, mock.Anything).Return(&resultMock{}, nil) 223 | 224 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 225 | _, err := stmt.ExecContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 226 | assert.NoError(t, err) 227 | 228 | var output bufLog 229 | err = json.Unmarshal(bufLogger.Bytes(), &output) 230 | assert.NoError(t, err) 231 | assert.Equal(t, "StmtExecContext", output.Message) 232 | assert.Equal(t, LevelInfo.String(), output.Level) 233 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 234 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 235 | }) 236 | 237 | t.Run("Success With Custom Level", func(t *testing.T) { 238 | q := "SELECT * FROM tt WHERE id = ?" 239 | stmtMock := &statementExecerContextMock{} 240 | stmtMock.On("ExecContext", mock.Anything, mock.Anything).Return(&resultMock{}, nil) 241 | 242 | custOpt := *testOpts 243 | WithExecerLevel(LevelDebug)(&custOpt) 244 | custLogger := *testLogger 245 | custLogger.opt = &custOpt 246 | 247 | stmt := &statement{query: q, Stmt: stmtMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID(), connID: custLogger.opt.uidGenerator.UniqueID()} 248 | _, err := stmt.ExecContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 249 | assert.NoError(t, err) 250 | 251 | var output bufLog 252 | err = json.Unmarshal(bufLogger.Bytes(), &output) 253 | assert.NoError(t, err) 254 | assert.Equal(t, "StmtExecContext", output.Message) 255 | assert.Equal(t, LevelDebug.String(), output.Level) 256 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 257 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 258 | }) 259 | } 260 | 261 | func TestStatement_QueryContext(t *testing.T) { 262 | t.Run("Not implement driver.StmtQueryContext", func(t *testing.T) { 263 | q := "SELECT * FROM tt WHERE id = ?" 264 | stmtMock := &statementMock{} 265 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 266 | 267 | _, err := stmt.QueryContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 268 | assert.Error(t, err) 269 | assert.Equal(t, driver.ErrSkip, err) 270 | }) 271 | 272 | t.Run("Error", func(t *testing.T) { 273 | q := "SELECT * FROM tt WHERE id = ?" 274 | stmtMock := &statementQueryerContextMock{} 275 | stmtMock.On("QueryContext", mock.Anything, mock.Anything).Return(&rowsMock{}, driver.ErrBadConn) 276 | 277 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 278 | _, err := stmt.QueryContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 279 | assert.Error(t, err) 280 | assert.Equal(t, driver.ErrBadConn, err) 281 | 282 | var output bufLog 283 | err = json.Unmarshal(bufLogger.Bytes(), &output) 284 | assert.NoError(t, err) 285 | assert.Equal(t, "StmtQueryContext", output.Message) 286 | assert.Equal(t, LevelError.String(), output.Level) 287 | assert.Equal(t, driver.ErrBadConn.Error(), output.Data[testOpts.errorFieldname]) 288 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 289 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 290 | }) 291 | 292 | t.Run("Success", func(t *testing.T) { 293 | q := "SELECT * FROM tt WHERE id = ?" 294 | stmtMock := &statementQueryerContextMock{} 295 | stmtMock.On("QueryContext", mock.Anything, mock.Anything).Return(&rowsMock{}, nil) 296 | 297 | stmt := &statement{query: q, Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 298 | _, err := stmt.QueryContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 299 | assert.NoError(t, err) 300 | 301 | var output bufLog 302 | err = json.Unmarshal(bufLogger.Bytes(), &output) 303 | assert.NoError(t, err) 304 | assert.Equal(t, "StmtQueryContext", output.Message) 305 | assert.Equal(t, LevelInfo.String(), output.Level) 306 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 307 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 308 | }) 309 | 310 | t.Run("Success With Custom Level", func(t *testing.T) { 311 | q := "SELECT * FROM tt WHERE id = ?" 312 | stmtMock := &statementQueryerContextMock{} 313 | stmtMock.On("QueryContext", mock.Anything, mock.Anything).Return(&rowsMock{}, nil) 314 | 315 | custOpt := *testOpts 316 | WithQueryerLevel(LevelDebug)(&custOpt) 317 | custLogger := *testLogger 318 | custLogger.opt = &custOpt 319 | 320 | stmt := &statement{query: q, Stmt: stmtMock, logger: &custLogger, id: custLogger.opt.uidGenerator.UniqueID(), connID: custLogger.opt.uidGenerator.UniqueID()} 321 | _, err := stmt.QueryContext(context.TODO(), []driver.NamedValue{{Name: "", Ordinal: 0, Value: "testid"}}) 322 | assert.NoError(t, err) 323 | 324 | var output bufLog 325 | err = json.Unmarshal(bufLogger.Bytes(), &output) 326 | assert.NoError(t, err) 327 | assert.Equal(t, "StmtQueryContext", output.Message) 328 | assert.Equal(t, LevelDebug.String(), output.Level) 329 | assert.Equal(t, q, output.Data[testOpts.sqlQueryFieldname]) 330 | assert.Equal(t, []interface{}{"testid"}, output.Data[testOpts.sqlArgsFieldname]) 331 | }) 332 | } 333 | 334 | func TestStatement_QueryContext2(t *testing.T) { 335 | // make sure conn id flow into statement 336 | driverConnMock := &driverConnMock{} 337 | stmtMock := &statementMock{} 338 | stmtMock.On("Query", mock.Anything).Return(&rowsMock{}, nil) 339 | driverConnMock.On("Prepare", mock.Anything).Return(stmtMock, nil) 340 | q := "SELECT * FROM tt WHERE id = ?" 341 | conn := &connection{Conn: driverConnMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 342 | stmt, err := conn.Prepare(q) 343 | assert.NoError(t, err) 344 | 345 | var connOutput bufLog 346 | err = json.Unmarshal(bufLogger.Bytes(), &connOutput) 347 | assert.NoError(t, err) 348 | assert.Equal(t, LevelInfo.String(), connOutput.Level) 349 | assert.Equal(t, conn.id, connOutput.Data[testOpts.connIDFieldname]) 350 | 351 | _, rsErr := stmt.Query([]driver.Value{1}) 352 | assert.NoError(t, rsErr) 353 | var stmtOutput bufLog 354 | err = json.Unmarshal(bufLogger.Bytes(), &stmtOutput) 355 | assert.NoError(t, err) 356 | assert.Equal(t, LevelInfo.String(), stmtOutput.Level) 357 | assert.Equal(t, conn.id, stmtOutput.Data[testOpts.connIDFieldname]) 358 | assert.NotEmpty(t, stmtOutput.Data[testOpts.stmtIDFieldname]) 359 | } 360 | 361 | func TestStatement_CheckNamedValue(t *testing.T) { 362 | t.Run("Error", func(t *testing.T) { 363 | stmtMock := &statementNamedValueCheckerMock{} 364 | stmtMock.On("CheckNamedValue", mock.Anything).Return(driver.ErrBadConn) 365 | 366 | stmt := &statement{Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 367 | err := stmt.CheckNamedValue(&driver.NamedValue{Name: "", Ordinal: 0, Value: "testid"}) 368 | assert.Error(t, err) 369 | 370 | var stmtOutput bufLog 371 | err = json.Unmarshal(bufLogger.Bytes(), &stmtOutput) 372 | assert.NoError(t, err) 373 | assert.Equal(t, LevelError.String(), stmtOutput.Level) 374 | assert.Equal(t, "StmtCheckNamedValue", stmtOutput.Message) 375 | assert.NotEmpty(t, stmtOutput.Data[testOpts.stmtIDFieldname]) 376 | assert.NotEmpty(t, stmtOutput.Data[testOpts.connIDFieldname]) 377 | }) 378 | 379 | t.Run("Not implement driver.NamedValueChecker", func(t *testing.T) { 380 | stmtMock := &statementMock{} 381 | 382 | stmt := &statement{Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 383 | err := stmt.CheckNamedValue(&driver.NamedValue{Name: "", Ordinal: 0, Value: "testid"}) 384 | assert.Error(t, err) 385 | assert.Equal(t, driver.ErrSkip, err) 386 | }) 387 | } 388 | 389 | func TestStatement_ColumnConverter(t *testing.T) { 390 | t.Run("Return as is", func(t *testing.T) { 391 | stmtMock := &statementValueConverterMock{} 392 | stmtMock.On("ColumnConverter", mock.Anything).Return(driver.NotNull{Converter: driver.DefaultParameterConverter}) 393 | 394 | stmt := &statement{Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 395 | cnv := stmt.ColumnConverter(1) 396 | val, err := cnv.ConvertValue(1) 397 | assert.NoError(t, err) 398 | intVal, ok := val.(int64) 399 | assert.True(t, ok) 400 | assert.Equal(t, int64(1), intVal) 401 | }) 402 | 403 | t.Run("Not implement driver.ColumnConverter", func(t *testing.T) { 404 | stmtMock := &statementMock{} 405 | stmt := &statement{Stmt: stmtMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID(), connID: testLogger.opt.uidGenerator.UniqueID()} 406 | cnv := stmt.ColumnConverter(1) 407 | assert.Equal(t, driver.DefaultParameterConverter, cnv) 408 | }) 409 | } 410 | 411 | type statementMock struct { 412 | mock.Mock 413 | } 414 | 415 | func (m *statementMock) Close() error { 416 | return m.Called().Error(0) 417 | } 418 | func (m *statementMock) NumInput() int { 419 | return m.Called().Int(0) 420 | } 421 | func (m *statementMock) Exec(args []driver.Value) (driver.Result, error) { 422 | arg := m.Called(args) 423 | 424 | return arg.Get(0).(driver.Result), arg.Error(1) 425 | } 426 | 427 | func (m *statementMock) Query(args []driver.Value) (driver.Rows, error) { 428 | arg := m.Called(args) 429 | 430 | return arg.Get(0).(driver.Rows), arg.Error(1) 431 | } 432 | 433 | type statementExecerContextMock struct { 434 | statementMock 435 | } 436 | 437 | func (m *statementExecerContextMock) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { 438 | arg := m.Called(ctx, args) 439 | 440 | return arg.Get(0).(driver.Result), arg.Error(1) 441 | } 442 | 443 | type statementQueryerContextMock struct { 444 | statementMock 445 | } 446 | 447 | func (m *statementQueryerContextMock) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { 448 | arg := m.Called(ctx, args) 449 | 450 | return arg.Get(0).(driver.Rows), arg.Error(1) 451 | } 452 | 453 | type statementNamedValueCheckerMock struct { 454 | statementMock 455 | } 456 | 457 | func (m *statementNamedValueCheckerMock) CheckNamedValue(nm *driver.NamedValue) error { 458 | return m.Called().Error(0) 459 | } 460 | 461 | type statementValueConverterMock struct { 462 | statementMock 463 | } 464 | 465 | func (m *statementValueConverterMock) ColumnConverter(idx int) driver.ValueConverter { 466 | return m.Called(idx).Get(0).(driver.ValueConverter) 467 | } 468 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ev 3 | go vet 4 | test -z "$(go fmt ./...)" # fail if not formatted properly 5 | go test -v -race -coverprofile=coverage.out -covermode=atomic ./... 6 | ls -d logadapter/* | xargs -I {} bash -c "cd '{}' \ 7 | && go mod tidy && go mod vendor \ 8 | && go test -race -coverprofile=coverage.out -covermode=atomic -coverpkg=./... ./... \ 9 | && cat coverage.out | grep -v \"mode:\" >> ../../coverage.out \ 10 | && rm coverage.out" 11 | # for go repo with nested modules, remove repo prefix, otherwise goveralls will failed. 12 | sed -i -e 's/github.com\/simukti\/sqldb-logger/./g' coverage.out 13 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "time" 7 | ) 8 | 9 | type transaction struct { 10 | driver.Tx 11 | id string 12 | connID string 13 | logger *logger 14 | } 15 | 16 | // Commit implement driver.Tx 17 | func (tx *transaction) Commit() error { 18 | lvl, start := LevelDebug, time.Now() 19 | err := tx.Tx.Commit() 20 | 21 | if err != nil { 22 | lvl = LevelError 23 | } 24 | 25 | tx.logger.log(context.Background(), lvl, "Commit", start, err, tx.logData()...) 26 | 27 | return err 28 | } 29 | 30 | // Rollback implement driver.Tx 31 | func (tx *transaction) Rollback() error { 32 | lvl, start := LevelDebug, time.Now() 33 | err := tx.Tx.Rollback() 34 | 35 | if err != nil { 36 | lvl = LevelError 37 | } 38 | 39 | tx.logger.log(context.Background(), lvl, "Rollback", start, err, tx.logData()...) 40 | 41 | return err 42 | } 43 | 44 | // logData default log data for transaction. 45 | func (tx *transaction) logData() []dataFunc { 46 | return []dataFunc{ 47 | tx.logger.withUID(tx.logger.opt.connIDFieldname, tx.connID), 48 | tx.logger.withUID(tx.logger.opt.txIDFieldname, tx.id), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /transaction_test.go: -------------------------------------------------------------------------------- 1 | package sqldblogger 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | func TestTransaction_Commit(t *testing.T) { 13 | t.Run("Error", func(t *testing.T) { 14 | txMock := &transactionMock{} 15 | txMock.On("Commit").Return(driver.ErrBadConn) 16 | 17 | conn := &transaction{Tx: txMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 18 | err := conn.Commit() 19 | assert.Error(t, err) 20 | 21 | var output bufLog 22 | err = json.Unmarshal(bufLogger.Bytes(), &output) 23 | assert.NoError(t, err) 24 | assert.Equal(t, "Commit", output.Message) 25 | assert.Equal(t, LevelError.String(), output.Level) 26 | }) 27 | 28 | t.Run("Success", func(t *testing.T) { 29 | txMock := &transactionMock{} 30 | txMock.On("Commit").Return(nil) 31 | 32 | conn := &transaction{Tx: txMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 33 | err := conn.Commit() 34 | assert.NoError(t, err) 35 | 36 | var output bufLog 37 | err = json.Unmarshal(bufLogger.Bytes(), &output) 38 | assert.NoError(t, err) 39 | assert.Equal(t, "Commit", output.Message) 40 | assert.Equal(t, LevelDebug.String(), output.Level) 41 | }) 42 | } 43 | 44 | func TestTransaction_Rollback(t *testing.T) { 45 | t.Run("Error", func(t *testing.T) { 46 | txMock := &transactionMock{} 47 | txMock.On("Rollback").Return(driver.ErrBadConn) 48 | 49 | conn := &transaction{Tx: txMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 50 | err := conn.Rollback() 51 | assert.Error(t, err) 52 | 53 | var output bufLog 54 | err = json.Unmarshal(bufLogger.Bytes(), &output) 55 | assert.NoError(t, err) 56 | assert.Equal(t, "Rollback", output.Message) 57 | assert.Equal(t, LevelError.String(), output.Level) 58 | }) 59 | 60 | t.Run("Success", func(t *testing.T) { 61 | txMock := &transactionMock{} 62 | txMock.On("Rollback").Return(nil) 63 | 64 | conn := &transaction{Tx: txMock, logger: testLogger, id: testLogger.opt.uidGenerator.UniqueID()} 65 | err := conn.Rollback() 66 | assert.NoError(t, err) 67 | 68 | var output bufLog 69 | err = json.Unmarshal(bufLogger.Bytes(), &output) 70 | assert.NoError(t, err) 71 | assert.Equal(t, "Rollback", output.Message) 72 | assert.Equal(t, LevelDebug.String(), output.Level) 73 | }) 74 | } 75 | 76 | type transactionMock struct { 77 | mock.Mock 78 | } 79 | 80 | func (m *transactionMock) Commit() error { 81 | return m.Called().Error(0) 82 | } 83 | 84 | func (m *transactionMock) Rollback() error { 85 | return m.Called().Error(0) 86 | } 87 | --------------------------------------------------------------------------------