Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for stdlib logger #28

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions examples/helloworld/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"context"
"database/sql"
"log"
"os"

"github.com/vippsas/go-querysql/querysql"

_ "github.com/denisenkom/go-mssqldb"
)

func initdb() (*sql.DB, error) {
dsn := os.Getenv("SQL_DSN")
if dsn == "" {
dsn = "sqlserver://127.0.0.1:1433?database=master&user id=sa&password=VippsPw1"
}
return sql.Open("sqlserver", dsn)
}

func main() {
sqldb, err := initdb()
if err != nil {
panic(err.Error())
}

logger := log.Default()
ctx := querysql.WithLogger(context.Background(), querysql.StdMSSQLLogger(logger))

qry := `select _log='info', message='hello world from a query'`
_, err = querysql.ExecContext(ctx, sqldb, qry, "world")
if err != nil {
panic(err.Error())
}
}
36 changes: 36 additions & 0 deletions examples/helloworld_logrus/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"context"
"database/sql"
"os"

"github.com/sirupsen/logrus"
"github.com/vippsas/go-querysql/querysql"

_ "github.com/denisenkom/go-mssqldb"
)

func initdb() (*sql.DB, error) {
dsn := os.Getenv("SQL_DSN")
if dsn == "" {
dsn = "sqlserver://127.0.0.1:1433?database=master&user id=sa&password=VippsPw1"
}
return sql.Open("sqlserver", dsn)
}

func main() {
sqldb, err := initdb()
if err != nil {
panic(err.Error())
}

logger := logrus.StandardLogger()
ctx := querysql.WithLogger(context.Background(), querysql.LogrusMSSQLLogger(logger, logrus.InfoLevel))

qry := `select _log='info', message='hello world from a query'`
_, err = querysql.ExecContext(ctx, sqldb, qry, "world")
if err != nil {
panic(err.Error())
}
}
77 changes: 77 additions & 0 deletions examples/metrics/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"context"
"database/sql"
"fmt"
"os"

_ "github.com/denisenkom/go-mssqldb"
"github.com/sirupsen/logrus"
"github.com/vippsas/go-querysql/querysql"
)

func initdb() (*sql.DB, error) {
dsn := os.Getenv("SQL_DSN")
if dsn == "" {
dsn = "sqlserver://127.0.0.1:1433?database=master&user id=sa&password=VippsPw1"
}
return sql.Open("sqlserver", dsn)
}

func populatedb(ctx context.Context, sqldb *sql.DB) error {
qry := `drop table if exists
create table MyUsers (
Id int identity(1,1) primary key,
UserName nvarchar(50) not null,
UserAge int
);
insert into MyUsers (UserName, UserAge) values ('Bob Doe', 42);
insert into MyUsers (UserName, UserAge) values ('Johny Doe', 10);
`
_, err := querysql.ExecContext(ctx, sqldb, qry, "world")
return err
}

func processUsers(ctx context.Context, sqldb *sql.DB) error {
qry := `select * from MyUsers;
select _function='UserMetrics', label='size.MyUsers', numProcessed=@@rowcount;
`
type rowType struct {
Id int
UserName string
UserAge int
}
users, err := querysql.Slice[rowType](ctx, sqldb, qry, "world")
fmt.Println(users)
return err
}

// This function is going to be called when the query in processUsers is run
func UserMetrics(label string, count int) {
fmt.Println(label, count)
}

func main() {
sqldb, err := initdb()
if err != nil {
panic(err.Error())
}

logger := logrus.StandardLogger()
ctx := querysql.WithLogger(context.Background(), querysql.LogrusMSSQLLogger(logger, logrus.InfoLevel))
ctx = querysql.WithDispatcher(ctx, querysql.GoMSSQLDispatcher([]interface{}{
UserMetrics,
}))

err = populatedb(ctx, sqldb)
if err != nil {
panic(err.Error())
}

err = processUsers(ctx, sqldb)
if err != nil {
panic(err.Error())
}

}
109 changes: 109 additions & 0 deletions querysql/logmssql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package querysql

import (
"database/sql"
"encoding/hex"
"fmt"
"log"

"github.com/sirupsen/logrus"
)

func StdMSSQLLogger(logger *log.Logger) RowsLogger {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It accepts the standard library logger; but not that the implementation itself uses a bit of code from logrus. We can change the internals later, if we want to. But having a dependency with logrus is not a problem since we need to support it anyway.

defaultLogLevel := logrus.InfoLevel
return func(rows *sql.Rows) error {
var logLevel string

cols, err := rows.Columns()
if err != nil {
return err
}
colTypes, err := rows.ColumnTypes()
if err != nil {
return err
}

// For logging just scan *everything* into a string type straight from SQL driver to make things simple here...
// The first column is the log level by protocol of RowsLogger.
fields := make([]interface{}, len(cols))
scanPointers := make([]interface{}, len(cols))
scanPointers[0] = &logLevel
for i := 1; i < len(cols); i++ {
scanPointers[i] = &fields[i]
}

hadRow := false
for rows.Next() {
hadRow = true
if err = rows.Scan(scanPointers...); err != nil {
return err
}
parsedLogLevel, err := logrus.ParseLevel(logLevel)
if err != nil {
emitLogEntry(logger, logrus.Fields{
"event": "invalid.log.level",
"invalid.level": logLevel,
}, logrus.ErrorLevel)
parsedLogLevel = defaultLogLevel
}

logrusFields := logrus.Fields{}
for i, value := range fields {
if i == 0 {
continue
}
// we post-process the types of the values a bit to make some types more readable in logs
switch typedValue := value.(type) {
case []uint8:
switch colTypes[i].DatabaseTypeName() {
case "MONEY":
value = string(typedValue)
case "UNIQUEIDENTIFIER":
value, err = ParseSQLUUIDBytes(typedValue)
if err != nil {
return fmt.Errorf("could not decode UUID from SQL: %w", err)
}
default:
value = "0x" + hex.EncodeToString(typedValue)
}
}
logrusFields[cols[i]] = value
}
emitLogEntry(logger, logrusFields, parsedLogLevel)
}
if err = rows.Err(); err != nil {
return err
}
if !hadRow {
// it can be quite annoying to have logging of empty tables turn into nothing, so log
// an indication that the log statement was there, with an empty table
// in this case loglevel is unreachable, and we really can only log the keys,
// but let's hope INFO isn't overboard
logrusFields := logrus.Fields{}
logrusFields["_norows"] = true
for _, col := range cols[1:] {
logrusFields[col] = ""
}
emitLogEntry(logger, logrusFields, defaultLogLevel)
}
return nil
}
}

func emitLogEntry(logger *log.Logger, fields logrus.Fields, level logrus.Level) {
str := ""
for k, v := range fields {
str += fmt.Sprintf(" %s='%v'", k, v)
}
switch level {
case logrus.PanicLevel:
logger.Panic(str)
case logrus.FatalLevel:
logger.Fatal(str)
case logrus.ErrorLevel, logrus.WarnLevel, logrus.DebugLevel, logrus.TraceLevel, logrus.InfoLevel:
str = fmt.Sprintf("level=%s %s", level.String(), str)
logger.Print(str)
default:
panic(fmt.Sprintf("Log level %d not handled in emitLogEntry", level))
}
}
36 changes: 0 additions & 36 deletions querysql/logrusmssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ package querysql
import (
"database/sql"
"encoding/hex"
"errors"
"fmt"

"github.com/google/uuid"
"github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -109,37 +107,3 @@ func logrusEmitLogEntry(logger logrus.FieldLogger, level logrus.Level) {
panic(fmt.Sprintf("Log level %d not handled in logrusEmitLogEntry", level))
}
}

func ParseSQLUUIDBytes(v []uint8) (uuid.UUID, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this function to a common file

if len(v) != 16 {
return uuid.UUID{}, errors.New("ParseSQLUUIDBytes: did not get 16 bytes")
}
var shuffled [16]uint8
// This: select convert(uniqueidentifier, '00010203-0405-0607-0809-0a0b0c0d0e0f')
// Returns this when passed to uuid.FromBytes:
// 03020100-0504-0706-0809-0a0b0c0d0e0f
// So, shuffling first
shuffled[0x0] = v[0x3]
shuffled[0x1] = v[0x2]
shuffled[0x2] = v[0x1]
shuffled[0x3] = v[0x0]

shuffled[0x4] = v[0x5]
shuffled[0x5] = v[0x4]

shuffled[0x6] = v[0x7]
shuffled[0x7] = v[0x6]

// The rest are not shuffled :shrug:
shuffled[0x8] = v[0x8]
shuffled[0x9] = v[0x9]

shuffled[0xa] = v[0xa]
shuffled[0xb] = v[0xb]
shuffled[0xc] = v[0xc]
shuffled[0xd] = v[0xd]
shuffled[0xe] = v[0xe]
shuffled[0xf] = v[0xf]

return uuid.FromBytes(shuffled[:])
}
41 changes: 41 additions & 0 deletions querysql/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package querysql

import (
"errors"

"github.com/google/uuid"
)

func ParseSQLUUIDBytes(v []uint8) (uuid.UUID, error) {
if len(v) != 16 {
return uuid.UUID{}, errors.New("ParseSQLUUIDBytes: did not get 16 bytes")
}
var shuffled [16]uint8
// This: select convert(uniqueidentifier, '00010203-0405-0607-0809-0a0b0c0d0e0f')
// Returns this when passed to uuid.FromBytes:
// 03020100-0504-0706-0809-0a0b0c0d0e0f
// So, shuffling first
shuffled[0x0] = v[0x3]
shuffled[0x1] = v[0x2]
shuffled[0x2] = v[0x1]
shuffled[0x3] = v[0x0]

shuffled[0x4] = v[0x5]
shuffled[0x5] = v[0x4]

shuffled[0x6] = v[0x7]
shuffled[0x7] = v[0x6]

// The rest are not shuffled :shrug:
shuffled[0x8] = v[0x8]
shuffled[0x9] = v[0x9]

shuffled[0xa] = v[0xa]
shuffled[0xb] = v[0xb]
shuffled[0xc] = v[0xc]
shuffled[0xd] = v[0xd]
shuffled[0xe] = v[0xe]
shuffled[0xf] = v[0xf]

return uuid.FromBytes(shuffled[:])
}
Loading
Loading