2022-01-23 09:36:24 +02:00
|
|
|
package chdebug
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"reflect"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/fatih/color"
|
|
|
|
"github.com/uptrace/go-clickhouse/ch"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Option func(*QueryHook)
|
|
|
|
|
|
|
|
// WithEnabled enables/disables the hook.
|
|
|
|
func WithEnabled(on bool) Option {
|
|
|
|
return func(h *QueryHook) {
|
|
|
|
h.enabled = on
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithVerbose configures the hook to log all queries
|
|
|
|
// (by default, only failed queries are logged).
|
|
|
|
func WithVerbose(on bool) Option {
|
|
|
|
return func(h *QueryHook) {
|
|
|
|
h.verbose = on
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithWriter sets the log output to an io.Writer
|
|
|
|
// the default is os.Stderr
|
|
|
|
func WithWriter(w io.Writer) Option {
|
|
|
|
return func(h *QueryHook) {
|
|
|
|
h.writer = w
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromEnv configures the hook using the environment variable value.
|
|
|
|
// For example, WithEnv("CHDEBUG"):
|
|
|
|
// - CHDEBUG=0 - disables the hook.
|
|
|
|
// - CHDEBUG=1 - enables the hook.
|
|
|
|
// - CHDEBUG=2 - enables the hook and verbose mode.
|
2022-08-28 16:53:07 +03:00
|
|
|
func FromEnv(keys ...string) Option {
|
|
|
|
if len(keys) == 0 {
|
|
|
|
keys = []string{"CHDEBUG"}
|
2022-01-23 09:36:24 +02:00
|
|
|
}
|
|
|
|
return func(h *QueryHook) {
|
2022-08-28 16:53:07 +03:00
|
|
|
for _, key := range keys {
|
|
|
|
if env, ok := os.LookupEnv(key); ok {
|
|
|
|
h.enabled = env != "" && env != "0"
|
|
|
|
h.verbose = env == "2"
|
|
|
|
break
|
|
|
|
}
|
2022-01-23 09:36:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type QueryHook struct {
|
|
|
|
enabled bool
|
|
|
|
verbose bool
|
|
|
|
writer io.Writer
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ ch.QueryHook = (*QueryHook)(nil)
|
|
|
|
|
|
|
|
func NewQueryHook(opts ...Option) *QueryHook {
|
|
|
|
h := &QueryHook{
|
|
|
|
enabled: true,
|
|
|
|
writer: os.Stderr,
|
|
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt(h)
|
|
|
|
}
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *QueryHook) BeforeQuery(ctx context.Context, evt *ch.QueryEvent) context.Context {
|
|
|
|
return ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *QueryHook) AfterQuery(ctx context.Context, event *ch.QueryEvent) {
|
|
|
|
if !h.enabled {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !h.verbose {
|
|
|
|
switch event.Err {
|
|
|
|
case nil, sql.ErrNoRows:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
dur := now.Sub(event.StartTime)
|
|
|
|
|
|
|
|
args := []any{
|
|
|
|
"[ch]",
|
|
|
|
now.Format(" 15:04:05.000 "),
|
|
|
|
formatOperation(event),
|
|
|
|
fmt.Sprintf(" %10s ", dur.Round(time.Microsecond)),
|
|
|
|
event.Query,
|
|
|
|
}
|
|
|
|
|
|
|
|
if event.Err != nil {
|
|
|
|
typ := reflect.TypeOf(event.Err).String()
|
|
|
|
args = append(args,
|
|
|
|
"\t",
|
|
|
|
color.New(color.BgRed).Sprintf(" %s ", typ+": "+event.Err.Error()),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintln(h.writer, args...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatOperation(event *ch.QueryEvent) string {
|
|
|
|
operation := event.Operation()
|
|
|
|
return operationColor(operation).Sprintf(" %-16s ", operation)
|
|
|
|
}
|
|
|
|
|
|
|
|
func operationColor(operation string) *color.Color {
|
|
|
|
switch operation {
|
|
|
|
case "SELECT":
|
|
|
|
return color.New(color.BgGreen, color.FgHiWhite)
|
|
|
|
case "INSERT":
|
|
|
|
return color.New(color.BgBlue, color.FgHiWhite)
|
|
|
|
case "UPDATE":
|
|
|
|
return color.New(color.BgYellow, color.FgHiBlack)
|
|
|
|
case "DELETE":
|
|
|
|
return color.New(color.BgMagenta, color.FgHiWhite)
|
|
|
|
default:
|
|
|
|
return color.New(color.BgWhite, color.FgHiBlack)
|
|
|
|
}
|
|
|
|
}
|