Skip to content

Commit

Permalink
Gelf/Graylog handler (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
instabledesign authored Jul 2, 2019
1 parent a359e0b commit 73f2839
Show file tree
Hide file tree
Showing 24 changed files with 587 additions and 42 deletions.
2 changes: 1 addition & 1 deletion benchmarks/formatter/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func BenchmarkDefaultFormatter(b *testing.B) {
b.ReportAllocs()

jsonFormatter := formatter.NewJsonEncoder()
jsonFormatter := formatter.NewJSONEncoder()

b.ResetTimer()
for n := 0; n < b.N; n++ {
Expand Down
20 changes: 20 additions & 0 deletions benchmarks/formatter/gelf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package benchmark_formatter_test

import (
"testing"

"github.com/gol4ng/logger"
"github.com/gol4ng/logger/formatter"
)

func BenchmarkGelfFormatter(b *testing.B) {
b.ReportAllocs()

gelfFormatter := formatter.NewGelf()
e := logger.Entry{Message: "This log message is really logged.", Level: logger.InfoLevel}

b.ResetTimer()
for n := 0; n < b.N; n++ {
gelfFormatter.Format(e)
}
}
2 changes: 1 addition & 1 deletion benchmarks/formatter/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func BenchmarkJsonFormatter(b *testing.B) {
b.ReportAllocs()

jsonFormatter := formatter.NewJsonEncoder()
jsonFormatter := formatter.NewJSONEncoder()

b.ResetTimer()
for n := 0; n < b.N; n++ {
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func BenchmarkLoggerJsonFormatter(b *testing.B) {
b.ReportAllocs()

myLogger := logger.NewLogger(
handler.Stream(&NopWriter{}, formatter.NewJsonEncoder()),
handler.Stream(&NopWriter{}, formatter.NewJSONEncoder()),
)

b.ResetTimer()
Expand Down
15 changes: 10 additions & 5 deletions entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ type Entry struct {
// String will return Entry as string
func (e *Entry) String() string {
builder := &strings.Builder{}
EntryToString(*e, builder)
return builder.String()
}

// EntryToString will write entry as string in builder
func EntryToString(entry Entry, builder *strings.Builder) {
builder.WriteString("<")
builder.WriteString(e.Level.String())
builder.WriteString(entry.Level.String())
builder.WriteString("> ")
builder.WriteString(e.Message)
if e.Context != nil {
builder.WriteString(entry.Message)
if entry.Context != nil {
builder.WriteString(" [ ")
builder.WriteString(e.Context.String())
builder.WriteString(entry.Context.String())
builder.WriteString(" ]")
}
return builder.String()
}
23 changes: 23 additions & 0 deletions entry_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package logger_test

import (
"strings"
"testing"

"github.com/gol4ng/logger"
Expand All @@ -27,3 +28,25 @@ func TestEntry_String(t *testing.T) {
})
}
}

func TestEntryToString(t *testing.T) {
tests := []struct {
name string
entry logger.Entry
strings []string
}{
{name: "Empty context", entry: logger.Entry{Message: "log message", Level: logger.DebugLevel, Context: logger.NewContext()}, strings: []string{"<debug> log message [ <nil> ]"}},
{name: "Simple entry", entry: logger.Entry{Message: "log message", Level: logger.DebugLevel, Context: logger.Ctx("my_key", "my_value")}, strings: []string{"<debug> log message [ <my_key:my_value> ]"}},
{name: "Simple entry", entry: logger.Entry{Message: "log message", Level: logger.DebugLevel, Context: logger.Ctx("my_key", "my_value").Add("my_int_val", 3)}, strings: []string{"<debug> log message [", "<my_key:my_value>", "<my_int_val:3>"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := &strings.Builder{}
logger.EntryToString(tt.entry, builder)
for _, s := range tt.strings {
assert.Contains(t, builder.String(), s)
}
})
}
}
25 changes: 25 additions & 0 deletions examples/formatter/gelf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package example_formatter_test

import (
"fmt"
"os"
"time"

"bou.ke/monkey"

"github.com/gol4ng/logger"
"github.com/gol4ng/logger/formatter"
)

func ExampleGelfFormatter() {
monkey.Patch(time.Now, func() time.Time { return time.Unix(513216000, 0) })
monkey.Patch(os.Hostname, func() (string, error) { return "my_fake_hostname", nil })
defer monkey.UnpatchAll()

gelfFormatter := formatter.NewGelf()

fmt.Println(gelfFormatter.Format(logger.Entry{Message: "My log message", Level: logger.InfoLevel, Context: logger.NewContext().Add("my key", "my_value")}))

//Output:
//{"version":"1.1","host":"my_fake_hostname","level":6,"timestamp":513216000.000,"short_message":"My log message","full_message":"<info> My log message [ <my key:my_value> ]","_my_key":"my_value"}
}
22 changes: 22 additions & 0 deletions examples/formatter/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package example_formatter_test

import (
"fmt"

"github.com/gol4ng/logger"
"github.com/gol4ng/logger/formatter"
)

func ExampleJsonFormatter() {
jsonFormatter := formatter.NewJSONEncoder()

fmt.Println(jsonFormatter.Format(
logger.Entry{
Message: "My log message",
Level: logger.InfoLevel,
Context: logger.NewContext().Add("my_key", "my_value"),
},
))
//Output:
//{"Message":"My log message","Level":6,"Context":{"my_key":"my_value"}}
}
2 changes: 1 addition & 1 deletion formatter/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (n *DefaultFormatter) Format(entry logger.Entry) string {
builder.WriteString(entry.Message)
if entry.Context != nil {
builder.WriteString(" ")
MarshalContextTo(entry.Context, builder)
ContextToJSON(entry.Context, builder)
}

return builder.String()
Expand Down
86 changes: 86 additions & 0 deletions formatter/gelf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package formatter

import (
"encoding/json"
"os"
"strconv"
"strings"
"time"

"github.com/gol4ng/logger"
)

const (
// Version is the current Gelf version
Version string = "1.1"
)

// Gelf formatter will transform Entry into Gelf format
type Gelf struct {
hostname string
version string
}

// Format will return Entry as gelf
func (g *Gelf) Format(entry logger.Entry) string {
builder := &strings.Builder{}

builder.WriteString(`{"version":"`)
builder.WriteString(g.version)

builder.WriteString(`","host":"`)
builder.WriteString(g.hostname)

builder.WriteString(`","level":`)
builder.WriteString(strconv.Itoa(int(entry.Level)))

builder.WriteString(`,"timestamp":`)
builder.WriteString(strconv.FormatFloat(float64(time.Now().UnixNano())/1e9, 'f', 3, 64))

builder.WriteString(`,"short_message":"`)
builder.WriteString(entry.Message)

builder.WriteString(`","full_message":"`)
logger.EntryToString(entry, builder)
builder.WriteString(`"`)

if entry.Context != nil {
for name, field := range *entry.Context {
builder.WriteString(`,"_`)
builder.WriteString(strings.Replace(name, " ", "_", -1))
builder.WriteString(`":`)
d, _ := json.Marshal(field.Value)
builder.Write(d)
}
}

builder.WriteString("}")

return builder.String()
}

// NewGelf will create a new Gelf formatter
func NewGelf() *Gelf {
hostname, err := os.Hostname()
if err != nil {
panic(err)
}

return &Gelf{hostname: hostname, version: Version}
}

// GelfTCPFormatter will decorate Gelf formatter in order to add null byte between each entry
// http://docs.graylog.org/en/3.0/pages/gelf.html#gelf-via-tcp
type GelfTCPFormatter struct {
*Gelf
}

// Format will return Entry as gelf TCP
func (g *GelfTCPFormatter) Format(entry logger.Entry) string {
return g.Gelf.Format(entry) + "\x00"
}

// NewGelfTCP will create a new GelfTCPFormatter
func NewGelfTCP() *GelfTCPFormatter {
return &GelfTCPFormatter{Gelf: NewGelf()}
}
27 changes: 27 additions & 0 deletions formatter/gelf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package formatter_test

import (
"os"
"testing"
"time"

"bou.ke/monkey"
"github.com/stretchr/testify/assert"

"github.com/gol4ng/logger"
"github.com/gol4ng/logger/formatter"
)

func TestGelf_Format(t *testing.T) {
monkey.Patch(time.Now, func() time.Time { return time.Unix(513216000, 0) })
monkey.Patch(os.Hostname, func() (string, error) { return "my_fake_hostname", nil })
defer monkey.UnpatchAll()

gelf := formatter.NewGelf()

assert.Equal(
t,
"{\"version\":\"1.1\",\"host\":\"my_fake_hostname\",\"level\":6,\"timestamp\":513216000.000,\"short_message\":\"My log message\",\"full_message\":\"<info> My log message [ <my key:my_value> ]\",\"_my_key\":\"my_value\"}",
gelf.Format(logger.Entry{Message: "My log message", Level: logger.InfoLevel, Context: logger.NewContext().Add("my key", "my_value")}),
)
}
43 changes: 22 additions & 21 deletions formatter/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
//
// the encode function is useful if you do not use the default provided logger implementation
type Json struct {
encode func(interface{}) ([]byte, error)
encode func(logger.Entry) ([]byte, error)
}

// Format will return Entry as json
Expand All @@ -22,8 +22,19 @@ func (j *Json) Format(entry logger.Entry) string {
return string(b)
}

// MarshalContextTo will marshall the logger context into json
func MarshalContextTo(context *logger.Context, builder *strings.Builder) {
// NewJSONEncoder will create a new Json with default json encoder function
func NewJSONEncoder() *Json {
return NewJSON(JSONEncoder)
}

// NewJSON will create a new Json with given json encoder
// it allow you tu use your own json encoder
func NewJSON(encode func(logger.Entry) ([]byte, error)) *Json {
return &Json{encode: encode}
}

// ContextToJSON will marshall the logger context into json
func ContextToJSON(context *logger.Context, builder *strings.Builder) {
if context == nil || len(*context) == 0 {
builder.WriteString("null")
} else {
Expand All @@ -44,8 +55,8 @@ func MarshalContextTo(context *logger.Context, builder *strings.Builder) {
}
}

// MarshalEntryTo will marshall the logger Entry into json
func MarshalEntryTo(entry logger.Entry, builder *strings.Builder) {
// EntryToJSON will marshall the logger Entry into json
func EntryToJSON(entry logger.Entry, builder *strings.Builder) {
builder.WriteRune('{')

builder.WriteString("\"Message\":\"")
Expand All @@ -59,25 +70,15 @@ func MarshalEntryTo(entry logger.Entry, builder *strings.Builder) {
builder.WriteRune(',')
builder.WriteString("\"Context\":")

MarshalContextTo(entry.Context, builder)
ContextToJSON(entry.Context, builder)

builder.WriteRune('}')
}

func entryJsonEncoder(value interface{}) ([]byte, error) {
data := &strings.Builder{}
MarshalEntryTo(value.(logger.Entry), data)
// JSONEncoder will return Entry to json string
func JSONEncoder(entry logger.Entry) ([]byte, error) {
builder := &strings.Builder{}
EntryToJSON(entry, builder)

return []byte(data.String()), nil
}

// NewJsonEncoder will create a new Json with default json encoder function
func NewJsonEncoder() *Json {
return NewJson(entryJsonEncoder)
}

// NewJson will create a new Json with given json encoder
// it allow you tu use your own json encoder
func NewJson(encode func(interface{}) ([]byte, error)) *Json {
return &Json{encode: encode}
return []byte(builder.String()), nil
}
Loading

0 comments on commit 73f2839

Please sign in to comment.