github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/depends/conf/log/log.go (about)

     1  package log
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"fmt"
     7  	"os"
     8  	"runtime"
     9  	"strconv"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/sirupsen/logrus"
    14  	"go.opentelemetry.io/otel/sdk/trace"
    15  
    16  	"github.com/machinefi/w3bstream/pkg/depends/base/consts"
    17  	"github.com/machinefi/w3bstream/pkg/depends/kit/metax"
    18  )
    19  
    20  type Log struct {
    21  	Name         string
    22  	Level        Level            `env:""`
    23  	Output       LoggerOutputType `env:""`
    24  	Format       LoggerFormatType
    25  	CKEndpoint   string             `env:""`
    26  	Exporter     trace.SpanExporter `env:"-"`
    27  	ReportCaller bool
    28  }
    29  
    30  func (l *Log) SetDefault() {
    31  	if l.Level == 0 {
    32  		l.Level = DebugLevel
    33  	}
    34  	if l.Output == 0 {
    35  		l.Output = LOGGER_OUTPUT_TYPE__ALWAYS
    36  	}
    37  	if l.Format == 0 {
    38  		l.Format = LOGGER_FORMAT_TYPE__JSON
    39  	}
    40  	if l.Name == "" {
    41  		l.Name = "unknown"
    42  		if v := os.Getenv(consts.EnvProjectName); v != "" {
    43  			l.Name = v
    44  		}
    45  	}
    46  }
    47  
    48  func (l *Log) InitLogrus() {
    49  	if l.Format == LOGGER_FORMAT_TYPE__JSON {
    50  		logrus.SetFormatter(JsonFormatter)
    51  	} else {
    52  		logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
    53  	}
    54  
    55  	logrus.SetLevel(l.Level.LogrusLogLevel())
    56  	logrus.SetReportCaller(l.ReportCaller)
    57  	// TODO add hook with goid meta logrus.AddHook(goid.Default)
    58  	logrus.AddHook(&ProjectAndMetaHook{l.Name})
    59  
    60  	if l.CKEndpoint != "" {
    61  		if ckHook, err := newClickhouseHook(l.CKEndpoint); err != nil {
    62  			logrus.Errorf("new ck hook error: %s", err)
    63  		} else {
    64  			logrus.AddHook(ckHook)
    65  		}
    66  	}
    67  	logrus.SetOutput(os.Stdout)
    68  }
    69  
    70  func (l *Log) InitSpanLog() {
    71  	if l.Exporter == nil {
    72  		return
    73  	}
    74  	if err := InstallPipeline(l.Output, l.Format, l.Exporter); err != nil {
    75  		panic(err)
    76  	}
    77  }
    78  
    79  func (l *Log) Init() {
    80  	l.InitLogrus()
    81  	l.InitSpanLog()
    82  }
    83  
    84  var ckBatchCount = 1000
    85  
    86  type ClickhouseHook struct {
    87  	CKEndpoint     string
    88  	ckDB           *sql.DB
    89  	insertSql      string
    90  	entries        []logrus.Entry
    91  	lastInsertTime time.Time
    92  	duration       time.Duration
    93  	lock           *sync.Mutex
    94  }
    95  
    96  func newClickhouseHook(endpoint string) (*ClickhouseHook, error) {
    97  	clickhouseHook := &ClickhouseHook{CKEndpoint: endpoint, entries: make([]logrus.Entry, 0, ckBatchCount*2)}
    98  	clickhouseHook.insertSql = `INSERT INTO w3b.server_logs (Timestamp, ProjectName, Msg) VALUES (?, ?, ?)`
    99  	clickhouseHook.lastInsertTime = time.Now()
   100  	clickhouseHook.duration = time.Second
   101  	clickhouseHook.lock = &sync.Mutex{}
   102  	db, err := clickhouseHook.connection()
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	clickhouseHook.ckDB = db
   107  	return clickhouseHook, nil
   108  }
   109  
   110  func (ck *ClickhouseHook) Fire(entry *logrus.Entry) error {
   111  	if !ck.ping() {
   112  		conn, err := ck.connection()
   113  		if err != nil {
   114  			logrus.Errorf("ck connection error: %s", err)
   115  			return err
   116  		}
   117  		ck.ckDB = conn
   118  	}
   119  
   120  	ck.lock.Lock()
   121  	defer ck.lock.Unlock()
   122  	ck.entries = append(ck.entries, *entry)
   123  	if len(ck.entries) >= ckBatchCount || time.Since(ck.lastInsertTime) > ck.duration {
   124  		ck.insertLogs(ck.entries)
   125  	}
   126  	return nil
   127  }
   128  
   129  func (ck *ClickhouseHook) Levels() []logrus.Level {
   130  	return logrus.AllLevels
   131  }
   132  
   133  func (ck *ClickhouseHook) connection() (*sql.DB, error) {
   134  	conn, err := sql.Open("clickhouse", ck.CKEndpoint)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	err = conn.Ping()
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	return conn, nil
   143  }
   144  
   145  func (ck *ClickhouseHook) ping() bool {
   146  	if err := ck.ckDB.Ping(); err != nil {
   147  		logrus.Errorf("ck ping error: %s", err)
   148  		return false
   149  	}
   150  	return true
   151  }
   152  
   153  func (ck *ClickhouseHook) insertLogs(entries []logrus.Entry) error {
   154  	err := ck.doWithTx(func(tx *sql.Tx) error {
   155  		statement, err := tx.PrepareContext(context.Background(), ck.insertSql)
   156  		if err != nil {
   157  			return fmt.Errorf(" ck prepareContext error: %w", err)
   158  		}
   159  		defer func() {
   160  			if err := statement.Close(); err != nil {
   161  				logrus.Errorf("ck statement close error: %s", err)
   162  			}
   163  		}()
   164  		for _, item := range entries {
   165  			msg, _ := item.Bytes()
   166  			if _, err := statement.ExecContext(context.Background(), item.Time, item.Data["@prj"], msg); err != nil {
   167  				return fmt.Errorf("ck execContext error:%w", err)
   168  			}
   169  		}
   170  		return nil
   171  	})
   172  	if err == nil {
   173  		ck.entries = ck.entries[:0]
   174  	}
   175  	return err
   176  }
   177  
   178  func (ck *ClickhouseHook) doWithTx(fn func(tx *sql.Tx) error) error {
   179  	tx, err := ck.ckDB.Begin()
   180  	if err != nil {
   181  		return fmt.Errorf("ck tx begin error: %w", err)
   182  	}
   183  	defer func() {
   184  		tx.Rollback()
   185  	}()
   186  	if err := fn(tx); err != nil {
   187  		return err
   188  	}
   189  	return tx.Commit()
   190  }
   191  
   192  type ProjectAndMetaHook struct {
   193  	Name string
   194  }
   195  
   196  func (h *ProjectAndMetaHook) Fire(entry *logrus.Entry) error {
   197  	ctx := entry.Context
   198  	if ctx == nil {
   199  		ctx = context.Background()
   200  	}
   201  	meta := metax.GetMetaFrom(ctx)
   202  	if entry.Data["@prj"] == nil {
   203  		entry.Data["@prj"] = h.Name
   204  	}
   205  	for k, v := range meta {
   206  		entry.Data["meta."+k] = v
   207  	}
   208  	return nil
   209  }
   210  
   211  func (h *ProjectAndMetaHook) Levels() []logrus.Level { return logrus.AllLevels }
   212  
   213  var (
   214  	project       = "unknown"
   215  	JsonFormatter = &logrus.JSONFormatter{
   216  		FieldMap: logrus.FieldMap{
   217  			logrus.FieldKeyLevel: "@lv",
   218  			logrus.FieldKeyTime:  "@ts",
   219  			logrus.FieldKeyFunc:  "@fn",
   220  			logrus.FieldKeyFile:  "@fl",
   221  		},
   222  		CallerPrettyfier: func(f *runtime.Frame) (fn string, file string) {
   223  			return f.Function + " line:" + strconv.FormatInt(int64(f.Line), 10), ""
   224  		},
   225  		TimestampFormat: "20060102-150405.000Z07:00",
   226  	}
   227  )
   228  
   229  func init() {
   230  	if v := os.Getenv(consts.EnvProjectName); v != "" {
   231  		project = v
   232  		if version := os.Getenv(consts.EnvProjectVersion); version != "" {
   233  			project = project + "@" + version
   234  		}
   235  	}
   236  	logrus.SetFormatter(JsonFormatter)
   237  	logrus.SetReportCaller(true)
   238  }