statping-ng/utils/log.go

256 lines
6.1 KiB
Go

package utils
import (
"fmt"
"github.com/fatih/structs"
"github.com/getsentry/sentry-go"
Logger "github.com/sirupsen/logrus"
"github.com/statping-ng/statping-ng/types/null"
"gopkg.in/natefinch/lumberjack.v2"
"io"
"os"
"reflect"
"strings"
"sync"
"time"
)
var (
Log = Logger.StandardLogger()
ljLogger *lumberjack.Logger
LastLines []*logRow
LockLines sync.Mutex
VerboseMode int
allowReports bool
)
const (
logFilePath = "/logs/statping.log"
errorReporter = "https://518d5b04a52b4130bbbbd5b9e70cb7ba@sentry.statping.com/2"
)
func SentryInit(allow bool) {
allowReports = allow
goEnv := Params.GetString("GO_ENV")
allowReports := Params.GetBool("ALLOW_REPORTS")
if allow || goEnv == "test" || allowReports {
if err := sentry.Init(sentry.ClientOptions{
Dsn: errorReporter,
Environment: goEnv,
Release: Params.GetString("VERSION"),
AttachStacktrace: true,
}); err != nil {
Log.Errorln(err)
}
Log.Infoln("Error Reporting initiated, thank you!")
sentry.CaptureMessage("sentry connected")
}
}
func SentryErr(err error) {
if !allowReports {
return
}
sentry.CaptureException(err)
}
func sentryTags() map[string]string {
val := make(map[string]string)
val["database"] = Params.GetString("DB_CONN")
return val
}
func SentryLogEntry(entry *Logger.Entry) {
e := sentry.NewEvent()
e.Message = entry.Message
e.Tags = sentryTags()
e.Release = Params.GetString("VERSION")
e.Contexts = entry.Data
sentry.CaptureEvent(e)
}
type hook struct {
Entries []Logger.Entry
mu sync.RWMutex
}
func (t *hook) Fire(e *Logger.Entry) error {
pushLastLine(e.Message)
if e.Level == Logger.ErrorLevel && allowReports {
SentryLogEntry(e)
}
return nil
}
func (t *hook) Levels() []Logger.Level {
return Logger.AllLevels
}
// ToFields accepts any amount of interfaces to create a new mapping for log.Fields. You will need to
// turn on verbose mode by starting Statping with "-v". This function will convert a struct of to the
// base struct name, and each field into it's own mapping, for example:
// type "*services.Service", on string field "Name" converts to "service_name=value". There is also an
// additional field called "_pointer" that will return the pointer hex value.
func ToFields(d ...interface{}) map[string]interface{} {
if !Log.IsLevelEnabled(Logger.DebugLevel) {
return nil
}
fieldKey := make(map[string]interface{})
for _, v := range d {
spl := strings.Split(fmt.Sprintf("%T", v), ".")
trueType := spl[len(spl)-1]
if !structs.IsStruct(v) {
continue
}
for _, f := range structs.Fields(v) {
if f.IsExported() && !f.IsZero() && f.Kind() != reflect.Ptr && f.Kind() != reflect.Slice && f.Kind() != reflect.Chan {
field := strings.ToLower(trueType + "_" + f.Name())
fieldKey[field] = replaceVal(f.Value())
}
}
fieldKey[strings.ToLower(trueType+"_pointer")] = fmt.Sprintf("%p", v)
}
return fieldKey
}
// replaceVal accepts an interface to be converted into human readable type
func replaceVal(d interface{}) interface{} {
switch v := d.(type) {
case null.NullBool:
return v.Bool
case null.NullString:
return v.String
case null.NullFloat64:
return v.Float64
case null.NullInt64:
return v.Int64
case string:
if len(v) > 500 {
return v[:500] + "... (truncated in logs)"
}
return v
case time.Time:
return v.String()
case time.Duration:
return v.String()
default:
return d
}
}
// createLog will create the '/logs' directory based on a directory
func createLog(dir string) error {
if !FolderExists(dir + "/logs") {
return CreateDirectory(dir + "/logs")
}
return nil
}
// InitLogs will create the '/logs' directory and creates a file '/logs/statup.log' for application logging
func InitLogs() error {
InitEnvs()
if Params.GetBool("DISABLE_LOGS") {
return nil
}
if err := createLog(Directory); err != nil {
return err
}
ljLogger = &lumberjack.Logger{
Filename: Directory + logFilePath,
MaxSize: Params.GetInt("LOGS_MAX_SIZE"),
MaxBackups: Params.GetInt("LOGS_MAX_COUNT"),
MaxAge: Params.GetInt("LOGS_MAX_AGE"),
}
mw := io.MultiWriter(os.Stdout, ljLogger)
Log.SetOutput(mw)
Log.SetFormatter(&Logger.TextFormatter{
ForceColors: !Params.GetBool("DISABLE_COLORS"),
DisableColors: Params.GetBool("DISABLE_COLORS"),
})
checkVerboseMode()
return nil
}
// checkVerboseMode will reset the Logging verbose setting. You can set
// the verbose level with "-v 3" or by setting VERBOSE=3 environment variable.
// statping -v 1 (only Warnings)
// statping -v 2 (Info and Warnings, default)
// statping -v 3 (Info, Warnings and Debug)
// statping -v 4 (Info, Warnings, Debug and Traces (SQL queries))
func checkVerboseMode() {
switch VerboseMode {
case 1:
Log.SetLevel(Logger.WarnLevel)
case 2:
Log.SetLevel(Logger.InfoLevel)
case 3:
Log.SetLevel(Logger.DebugLevel)
case 4:
Log.SetReportCaller(true)
Log.SetLevel(Logger.TraceLevel)
default:
Log.SetLevel(Logger.InfoLevel)
}
Log.Debugf("logging running in %v mode", Log.GetLevel().String())
}
// CloseLogs will close the log file correctly on shutdown
func CloseLogs() {
if ljLogger != nil {
ljLogger.Rotate()
Log.Writer().Close()
ljLogger.Close()
}
sentry.Flush(5 * time.Second)
}
func pushLastLine(line interface{}) {
LockLines.Lock()
defer LockLines.Unlock()
LastLines = append(LastLines, newLogRow(line))
// We want to store max 1000 lines in memory (for /logs page).
for len(LastLines) > 1000 {
LastLines = LastLines[1:]
}
}
// GetLastLine returns 1 line for a recent log entry
func GetLastLine() *logRow {
LockLines.Lock()
defer LockLines.Unlock()
if len(LastLines) > 0 {
return LastLines[len(LastLines)-1]
}
return nil
}
type logRow struct {
Date time.Time
Line interface{}
}
func newLogRow(line interface{}) (lgRow *logRow) {
lgRow = new(logRow)
lgRow.Date = time.Now()
lgRow.Line = line
return
}
func (o *logRow) lineAsString() string {
switch v := o.Line.(type) {
case string:
return v
case error:
return v.Error()
case []byte:
return string(v)
}
return ""
}
func (o *logRow) FormatForHtml() string {
return fmt.Sprintf("%s: %s", o.Date.Format("2006-01-02 15:04:05"), o.lineAsString())
}