Skip to content

Commit

Permalink
Send Gorm logs directly to datadog (#5)
Browse files Browse the repository at this point in the history
* add gorm logger

* fix query time

* fix rows_affected

* remove errors, fix messages

* change custom fields type when sending logs

* add offline logs

* return error and change function name

* fix send

* add Offline log prefix

* improve offline logs

* change execution time field name

* send logs into the channels wrapped in go routines

* improve gorm logger

* fix check v

* fix err overwrite

* fix

* send first the log

* don't send fatal log in a goroutine, wait instead
  • Loading branch information
emaele authored Oct 18, 2021
1 parent ff08451 commit 34b7262
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 56 deletions.
125 changes: 77 additions & 48 deletions datadog.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/sirupsen/logrus"
)

func (l *StandardLogger) SetupDataDogLogger(datadogEndpoint, datadogAPIKey string, sendDebugLogs, localmode bool) error {
func (l *StandardLogger) SetupDataDogLogger(datadogEndpoint, datadogAPIKey, offlineLogsPath string, sendDebugLogs, localmode bool) error {

// if provided endpoint is empty we fallback to the default one
if datadogEndpoint == "" {
Expand All @@ -30,6 +30,12 @@ func (l *StandardLogger) SetupDataDogLogger(datadogEndpoint, datadogAPIKey strin
l.initChannel()
}

// offline logs path
if offlineLogsPath != "" {
l.offlineLogsPath = offlineLogsPath
l.EnableOfflineLogs(true)
}

// set debug mode with provided value
l.SetDebugMode(sendDebugLogs)

Expand Down Expand Up @@ -83,79 +89,88 @@ func (l *StandardLogger) SetUpCustomHTTPClient(httpClient *http.Client) error {
}

// SendInfoLog sends a log with info level to the log channel
func (l *StandardLogger) SendInfoLog(message string, customFields map[string]string) {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.InfoLevel,
}
func (l *StandardLogger) SendInfoLog(message string, customFields map[string]interface{}) {
go func() {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.InfoLevel,
}
}()
}

// SendInfofLog sends a formatted log with info level to the log channel
func (l *StandardLogger) SendInfofLog(message string, customFields map[string]string, args ...interface{}) {
func (l *StandardLogger) SendInfofLog(message string, customFields map[string]interface{}, args ...interface{}) {
l.SendInfoLog(fmt.Sprintf(message, args...), customFields)
}

// SendWarnLog sends a log with warning level to the log channel
func (l *StandardLogger) SendWarnLog(message string, customFields map[string]string) {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.WarnLevel,
}
func (l *StandardLogger) SendWarnLog(message string, customFields map[string]interface{}) {
go func() {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.WarnLevel,
}
}()
}

// SendWarnfLog sends a formatted log with warn level to the log channel
func (l *StandardLogger) SendWarnfLog(message string, customFields map[string]string, args ...interface{}) {
func (l *StandardLogger) SendWarnfLog(message string, customFields map[string]interface{}, args ...interface{}) {
l.SendWarnLog(fmt.Sprintf(message, args...), customFields)
}

// SendErrLog sends a log with error level to the log channel
func (l *StandardLogger) SendErrLog(message string, customFields map[string]string) {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.ErrorLevel,
}
func (l *StandardLogger) SendErrLog(message string, customFields map[string]interface{}) {
go func() {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.ErrorLevel,
}
}()
}

// SendErrfLog sends a formatted log with error level to the log channel
func (l *StandardLogger) SendErrfLog(message string, customFields map[string]string, args ...interface{}) {
func (l *StandardLogger) SendErrfLog(message string, customFields map[string]interface{}, args ...interface{}) {
l.SendErrLog(fmt.Sprintf(message, args...), customFields)
}

// SendDebugLog sends a log with debug level to the log channel
func (l *StandardLogger) SendDebugLog(message string, customFields map[string]string) {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.DebugLevel,
}
func (l *StandardLogger) SendDebugLog(message string, customFields map[string]interface{}) {
go func() {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.DebugLevel,
}
}()
}

// SendDebugfLog sends a formatted log with debug level to the log channel
func (l *StandardLogger) SendDebugfLog(message string, customFields map[string]string, args ...interface{}) {
func (l *StandardLogger) SendDebugfLog(message string, customFields map[string]interface{}, args ...interface{}) {
l.SendDebugLog(fmt.Sprintf(message, args...), customFields)
}

// SendFatalLog sends a log with fatal level to the log channel
func (l *StandardLogger) SendFatalLog(message string, customFields map[string]string) {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.FatalLevel,
}
func (l *StandardLogger) SendFatalLog(message string, customFields map[string]interface{}) {
func() {
l.logChan <- Log{
Message: message,
CustomFields: customFields,
Level: logrus.FatalLevel,
}
}()
}

// SendFatalfLog sends a formatted log with fatal level to the log channel
func (l *StandardLogger) SendFatalfLog(message string, customFields map[string]string, args ...interface{}) {
func (l *StandardLogger) SendFatalfLog(message string, customFields map[string]interface{}, args ...interface{}) {
l.SendFatalLog(fmt.Sprintf(message, args...), customFields)
}

// startLogRoutineListener handles the incoming logs
func (l *StandardLogger) startLogRoutineListener() {
var logWriter io.Writer
// var httpClient http.Client
l.SetOutput(logWriter)

for logElem := range l.logChan {
Expand All @@ -176,22 +191,36 @@ func (l *StandardLogger) startLogRoutineListener() {
newLog.Data[key] = value
}

// If sendDebugLogs is true print the log with Println
if l.sendDebugLogs || l.localMode {
logBytes, err := newLog.Bytes()
if err != nil {
l.SendWarnLog(fmt.Sprintf("error converting log to bytes %v", err), nil)
continue
}

fmt.Println(string(logBytes))
logBytes, err := newLog.Bytes()
if err != nil {
l.SendWarnLog(fmt.Sprintf("error converting log to bytes %v", err), nil)
continue
}

// Performing http request to datadog
if !l.localMode {
// If localMode is true print the log with Println
if l.localMode {
fmt.Println(string(logBytes))
} else {
err := l.sendLogToDD(newLog, l.httpClient)
if err != nil {
log.Printf("unable to send log to DataDog, %v", err)
if l.saveOfflineLogs {
var offsaveErr error

newLog.Message = fmt.Sprintf("OFFLINE LOG at %v | %s", time.Now().String(), newLog.Message)

logBytes, offsaveErr = newLog.Bytes()
if err != nil {
l.SendWarnLog(fmt.Sprintf("error converting log to bytes %v", offsaveErr), nil)
continue
}

offsaveErr = l.saveLogToFile(logBytes, fmt.Sprintf("log-%s.json", time.Now().Format(time.RFC3339Nano)))
if offsaveErr != nil {
fmt.Println(offsaveErr)
}
}

continue
}
}
Expand Down
59 changes: 59 additions & 0 deletions gorm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package logpet

import (
"fmt"
"strconv"
"time"
)

func CreateGormLogger(logger *StandardLogger) *GormLogger {
return &GormLogger{Logger: logger}
}

type GormLogger struct {
Logger *StandardLogger
}

// Print handles log events from Gorm for the custom logger.
func (gLogger *GormLogger) Print(v ...interface{}) {
fields := map[string]interface{}{
"section": "database",
}

if nil == v {
gLogger.Logger.SendDebugLog("v database arguments are empty", fields)
return
}

switch v[0] {
case "sql":

if len(v) != 6 {
gLogger.Logger.SendErrfLog("wrong db log format, %v", fields, v)
return
}

queryfields := map[string]string{
"execution_time": v[2].(time.Duration).String(),
"arguments": fmt.Sprintf("%v", v[4]),
"rows_affected": strconv.FormatInt(v[5].(int64), 10),
"function_line": v[1].(string),
}

fields["query"] = queryfields

gLogger.Logger.SendDebugfLog(v[3].(string), fields)
case "log":
if len(v) != 3 {
gLogger.Logger.SendErrfLog("wrong db log format, %v", fields, v)
return
}

queryfields := map[string]string{
"function_line": v[1].(string),
}

fields["query"] = queryfields
gLogger.Logger.SendWarnfLog("%v", fields, v[2])
}
}
106 changes: 106 additions & 0 deletions offline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package logpet

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)

func (l *StandardLogger) EnableOfflineLogs(enable bool) {
l.saveOfflineLogs = enable
}

func (l *StandardLogger) saveLogToFile(toSave []byte, filename string) error {

_, err := os.Stat(l.offlineLogsPath)
if os.IsNotExist(err) {
err = os.Mkdir(l.offlineLogsPath, 0644)
if err != nil {
return err
}
}

filename = strings.ReplaceAll(filename, ":", "-")

filename = filepath.Join(l.offlineLogsPath, filename)

_, err = os.Stat(filename)
if os.IsNotExist(err) {
_, err := os.Create(filename)
if err != nil {
return err
}
}

err = ioutil.WriteFile(filename, toSave, 0644)
if err != nil {
return err
}
return nil
}

func (l *StandardLogger) SendOfflineLogs() error {

dir, err := ioutil.ReadDir(l.offlineLogsPath)
if err != nil {
return errors.New(fmt.Sprintf("unable to open directory %s, %v", l.offlineLogsPath, err))
}

for _, logfile := range dir {
if strings.HasPrefix(logfile.Name(), "log-") {

filename := filepath.Join(l.offlineLogsPath, logfile.Name())

// read and send
file, err := os.Open(filename)
if err != nil {
l.SendErrfLog("unable to open file %s", nil, logfile.Name())
continue
}

var logs ClientLog

err = json.NewDecoder(file).Decode(&logs)
if err != nil {
l.SendErrfLog("error decoding json log %s, due to %v", nil, file.Name(), err)

// Close file and handle err
err = file.Close()
if err != nil {
l.SendErrfLog("error closing json log %s, due to %v", nil, file.Name(), err)
}
continue
}

switch logs.Level {
case "info":
l.SendInfoLog(logs.Message, nil)
case "warning":
l.SendWarnLog(logs.Message, nil)
case "error":
l.SendErrLog(logs.Message, nil)
case "debug":
l.SendDebugLog(logs.Message, nil)
case "fatal":
l.SendErrfLog("EX FATAL | %s", nil, logs.Message)
}

// Close file and handle err
err = file.Close()
if err != nil {
l.SendErrfLog("unable to close file %s due to %v", nil, file.Name(), err)
}

err = os.Remove(filename)
if err != nil {
l.SendErrfLog("unable to remove file %s due to %v", nil, file.Name(), err)
}
}
}

return nil
}
24 changes: 16 additions & 8 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,26 @@ import (
// StandardLogger is a new type useful to add new methods for default log formats.
type StandardLogger struct {
*logrus.Logger
CustomFields map[string]interface{}
logChan chan Log
ddAPIKey string
ddEndpoint string
sendDebugLogs bool
localMode bool
httpClient *http.Client
CustomFields map[string]interface{}
logChan chan Log
ddAPIKey string
ddEndpoint string
sendDebugLogs bool
localMode bool
httpClient *http.Client
saveOfflineLogs bool
offlineLogsPath string
}

// Log is a type containing log message and level
type Log struct {
Message string
CustomFields map[string]string
CustomFields map[string]interface{}
Level logrus.Level
}

// ClientLog is a struct used for offline logs
type ClientLog struct {
Level string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
}

0 comments on commit 34b7262

Please sign in to comment.