├── example
├── Procfile
├── config
│ └── fluentd.conf
├── README.md
└── server.go
├── message.go
├── LICENSE
├── README.md
├── config.go
├── config_test.go
└── logger.go
/example/Procfile:
--------------------------------------------------------------------------------
1 | fluent: fluentd -c config/fluentd.conf
2 | gin: go run server.go
3 |
--------------------------------------------------------------------------------
/example/config/fluentd.conf:
--------------------------------------------------------------------------------
1 |
2 | type forward
3 | port 24224
4 |
5 |
6 |
7 | type stdout
8 |
9 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 |
3 | Sample application for fluent-logger-go using [gin](https://github.com/gin-gonic/gin).
4 |
5 | ## Setup
6 |
7 | ```
8 | $ go get github.com/gin-gonic/gin
9 | $ gem install fluentd
10 | $ gem install foreman
11 | ```
12 |
13 | ## Launch
14 |
15 | ```
16 | $ foreman start
17 | ```
18 |
19 | If you visit http://localhost:3000/foo, { "id": "foo" } is logged to fluentd.
20 |
--------------------------------------------------------------------------------
/message.go:
--------------------------------------------------------------------------------
1 | package fluent
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/ugorji/go/codec"
7 | )
8 |
9 | var (
10 | mh codec.MsgpackHandle
11 | )
12 |
13 | type message struct {
14 | tag string
15 | time time.Time
16 | data interface{}
17 | }
18 |
19 | func (m *message) toMsgpack() ([]byte, error) {
20 | pack := []byte{}
21 | encoder := codec.NewEncoderBytes(&pack, &mh)
22 |
23 | rawMessage := []interface{}{m.tag, m.time.Unix(), m.data}
24 | err := encoder.Encode(rawMessage)
25 |
26 | return pack, err
27 | }
28 |
--------------------------------------------------------------------------------
/example/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "github.com/k0kubun/fluent-logger-go"
6 | )
7 |
8 | var (
9 | logger *fluent.Logger
10 | )
11 |
12 | func logId(c *gin.Context) {
13 | id := c.Params.ByName("id")
14 | logger.Post("logId", map[string]string{"id": id})
15 | c.String(200, "OK")
16 | }
17 |
18 | func main() {
19 | logger = fluent.NewLogger(fluent.Config{
20 | BufferLength: 3 * 1024,
21 | })
22 |
23 | r := gin.Default()
24 | r.GET("/:id", logId)
25 | r.Run(":3000")
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Takashi Kokubun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fluent-logger-go
2 |
3 | Goroutine based asynchronous event logger for Fluentd
4 |
5 | ## Project status
6 |
7 | Not maintained. Please use [fluent/fluent-logger-golang](https://github.com/fluent/fluent-logger-golang) instead.
8 |
9 | ## Features
10 |
11 | * Channel based non-blocking logging interface
12 | * Queued events are periodically sent to fluentd altogether
13 |
14 | ## Installation
15 |
16 | ```
17 | $ go get github.com/k0kubun/fluent-logger-go
18 | ```
19 |
20 | ## Usage
21 |
22 | ```go
23 | package main
24 |
25 | import "github.com/gin-gonic/gin"
26 | import "github.com/k0kubun/fluent-logger-go"
27 |
28 | var logger *fluent.Logger
29 |
30 | func logId(c *gin.Context) {
31 | id := c.Params.ByName("id")
32 | logger.Post("idlog", map[string]string{"id": id})
33 | c.String(200, "Logged id")
34 | }
35 |
36 | func main() {
37 | logger = fluent.NewLogger(fluent.Config{})
38 | r := gin.Default()
39 | r.GET("/:id", logId)
40 | r.Run(":3000")
41 | }
42 | ```
43 |
44 | ### Documentation
45 |
46 | API documentation can be found here: https://godoc.org/github.com/k0kubun/fluent-logger-go
47 |
48 | ## Related projects
49 |
50 | You would like to use another one for some use case.
51 |
52 | * [t-k/fluent-logger-golang](https://github.com/t-k/fluent-logger-golang)
53 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package fluent
2 |
3 | import "time"
4 |
5 | const (
6 | DefaultFluentHost = "127.0.0.1"
7 | DefaultFluentPort = 24224
8 | DefaultChannelLength = 1000
9 | DefaultBufferLength = 10 * 1024
10 | DefaultMaxTrialForConnection = 10
11 | DefaultConnectionTimeout = 3 * time.Second
12 | DefaultBufferingTimeout = 100 * time.Millisecond
13 | DefaultTagPrefix = ""
14 | )
15 |
16 | var (
17 | intDefault int
18 | stringDefault string
19 | durationDefault time.Duration
20 | )
21 |
22 | // Config is just for fluent.NewLogger() argument.
23 | type Config struct {
24 | // You can customize fluentd host and port.
25 | FluentHost string
26 | FluentPort int
27 |
28 | // If buffered channel's length is equal to ChannelLength, main thread blocks.
29 | ChannelLength int
30 |
31 | // If all posted messages' size reaches BufferLength, logger flushes all logs.
32 | BufferLength int
33 |
34 | // Retry connection with fluentd MaxTrialForConnection times.
35 | MaxTrialForConnection int
36 |
37 | // Wait for connection until ConnectionTimeout.
38 | ConnectionTimeout time.Duration
39 |
40 | // Logger flushes its buffer on each BufferingTimeout interval.
41 | BufferingTimeout time.Duration
42 |
43 | // Tag prefix. When set TagPrefix to "foo" and post with "bar.baz",
44 | // you'll get "foo.bar.baz" tag.
45 | TagPrefix string
46 | }
47 |
48 | func (c *Config) applyDefaultValues() {
49 | assignIfDefault(&c.FluentHost, DefaultFluentHost)
50 | assignIfDefault(&c.FluentPort, DefaultFluentPort)
51 | assignIfDefault(&c.ChannelLength, DefaultChannelLength)
52 | assignIfDefault(&c.BufferLength, DefaultBufferLength)
53 | assignIfDefault(&c.MaxTrialForConnection, DefaultMaxTrialForConnection)
54 | assignIfDefault(&c.ConnectionTimeout, DefaultConnectionTimeout)
55 | assignIfDefault(&c.BufferingTimeout, DefaultBufferingTimeout)
56 | assignIfDefault(&c.TagPrefix, DefaultTagPrefix)
57 | }
58 |
59 | func assignIfDefault(target interface{}, DefaultValue interface{}) {
60 | switch target.(type) {
61 | case *string:
62 | ptr := target.(*string)
63 | if *ptr == stringDefault {
64 | *ptr = DefaultValue.(string)
65 | }
66 | case *int:
67 | ptr := target.(*int)
68 | if *ptr == intDefault {
69 | *ptr = DefaultValue.(int)
70 | }
71 | case *time.Duration:
72 | ptr := target.(*time.Duration)
73 | if *ptr == durationDefault {
74 | *ptr = DefaultValue.(time.Duration)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/config_test.go:
--------------------------------------------------------------------------------
1 | package fluent
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestApplyDefaultValues(t *testing.T) {
9 | config := Config{}
10 |
11 | assertEqual(t, config.FluentHost, stringDefault)
12 | assertEqual(t, config.FluentPort, intDefault)
13 | assertEqual(t, config.ChannelLength, intDefault)
14 | assertEqual(t, config.BufferLength, intDefault)
15 | assertEqual(t, config.MaxTrialForConnection, intDefault)
16 | assertEqual(t, config.ConnectionTimeout, durationDefault)
17 | assertEqual(t, config.BufferingTimeout, durationDefault)
18 | assertEqual(t, config.TagPrefix, stringDefault)
19 |
20 | config.applyDefaultValues()
21 | assertEqual(t, config.FluentHost, DefaultFluentHost)
22 | assertEqual(t, config.FluentPort, DefaultFluentPort)
23 | assertEqual(t, config.ChannelLength, DefaultChannelLength)
24 | assertEqual(t, config.BufferLength, DefaultBufferLength)
25 | assertEqual(t, config.MaxTrialForConnection, DefaultMaxTrialForConnection)
26 | assertEqual(t, config.ConnectionTimeout, DefaultConnectionTimeout)
27 | assertEqual(t, config.BufferingTimeout, DefaultBufferingTimeout)
28 | assertEqual(t, config.TagPrefix, DefaultTagPrefix)
29 |
30 | config = Config{
31 | FluentHost: "localhost",
32 | FluentPort: 80,
33 | ChannelLength: 1,
34 | BufferLength: 2,
35 | MaxTrialForConnection: 3,
36 | ConnectionTimeout: 2 * time.Second,
37 | BufferingTimeout: 2 * time.Second,
38 | TagPrefix: "prefix",
39 | }
40 |
41 | config.applyDefaultValues()
42 | assertEqual(t, config.FluentHost, "localhost")
43 | assertEqual(t, config.FluentPort, 80)
44 | assertEqual(t, config.ChannelLength, 1)
45 | assertEqual(t, config.BufferLength, 2)
46 | assertEqual(t, config.MaxTrialForConnection, 3)
47 | assertEqual(t, config.ConnectionTimeout, 2*time.Second)
48 | assertEqual(t, config.BufferingTimeout, 2*time.Second)
49 | assertEqual(t, config.TagPrefix, "prefix")
50 | }
51 |
52 | func assertEqual(t *testing.T, actual interface{}, expect interface{}) {
53 | switch actual.(type) {
54 | case string:
55 | if actual.(string) != expect.(string) {
56 | t.Errorf("expected %s, but got %s\n", expect.(string), actual.(string))
57 | }
58 | case int:
59 | if actual.(int) != expect.(int) {
60 | t.Errorf("expected %d, but got %d\n", expect.(int), actual.(int))
61 | }
62 | case time.Duration:
63 | if actual.(time.Duration) != expect.(time.Duration) {
64 | t.Errorf("expected %d, but got %d\n", expect.(time.Duration), actual.(time.Duration))
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | package fluent
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "net"
7 | "strconv"
8 | "time"
9 | )
10 |
11 | // Logger owns asynchronous logging to fluentd.
12 | type Logger struct {
13 | config Config
14 | postCh chan message
15 | buffer []byte
16 | conn net.Conn
17 | ticker *time.Ticker
18 | logError bool
19 | }
20 |
21 | // NewLogger() launches a goroutine to log and returns logger.
22 | // Logger has a channel to interact with the goroutine.
23 | func NewLogger(config Config) *Logger {
24 | config.applyDefaultValues()
25 |
26 | logger := &Logger{
27 | config: config,
28 | postCh: make(chan message, config.ChannelLength),
29 | buffer: make([]byte, 0, config.BufferLength),
30 | ticker: time.NewTicker(config.BufferingTimeout),
31 | logError: true,
32 | }
33 | logger.connect()
34 | go logger.loop()
35 |
36 | return logger
37 | }
38 |
39 | // You can send message to logger's goroutine via channel.
40 | // This logging is executed asynchronously.
41 | func (l *Logger) Post(tag string, data interface{}) {
42 | tag = l.prependTagPrefix(tag)
43 | l.postCh <- message{tag: tag, time: time.Now(), data: data}
44 | }
45 |
46 | // You can send message immediately to fluentd.
47 | func (l *Logger) Log(tag string, data interface{}) error {
48 | tag = l.prependTagPrefix(tag)
49 | msg := &message{tag: tag, time: time.Now(), data: data}
50 | pack, err := msg.toMsgpack()
51 | if err != nil {
52 | return err
53 | }
54 |
55 | l.buffer = append(l.buffer, pack...)
56 | return l.sendMessage()
57 | }
58 |
59 | func (l *Logger) loop() {
60 | for {
61 | select {
62 | case msg := <-l.postCh:
63 | pack, err := msg.toMsgpack()
64 | if err != nil {
65 | log.Printf("message pack dump error: " + err.Error())
66 | continue
67 | }
68 |
69 | l.buffer = append(l.buffer, pack...)
70 | if len(l.buffer) > l.config.BufferLength {
71 | l.sendMessage()
72 | }
73 | case <-l.ticker.C:
74 | l.sendMessage()
75 | }
76 | }
77 | }
78 |
79 | func (l *Logger) sendMessage() error {
80 | if len(l.buffer) == 0 {
81 | return errors.New("Buffer is empty")
82 | }
83 |
84 | l.connect()
85 | if l.conn == nil {
86 | return errors.New("Failed to establish connection with fluentd")
87 | }
88 |
89 | _, err := l.conn.Write(l.buffer)
90 |
91 | if err == nil {
92 | l.buffer = l.buffer[0:0]
93 | } else {
94 | log.Printf("failed to send message: " + err.Error())
95 | l.conn.Close()
96 | l.conn = nil
97 | }
98 | return err
99 | }
100 |
101 | func (l *Logger) connect() {
102 | if l.conn != nil {
103 | return
104 | }
105 |
106 | var err error
107 | for i := 0; i < l.config.MaxTrialForConnection; i++ {
108 | l.conn, err = net.DialTimeout(
109 | "tcp",
110 | l.config.FluentHost+":"+strconv.Itoa(l.config.FluentPort),
111 | l.config.ConnectionTimeout,
112 | )
113 |
114 | if err == nil {
115 | l.logError = true
116 | return
117 | }
118 | }
119 |
120 | if l.logError {
121 | log.Printf("failed to establish connection with fluentd: " + err.Error())
122 | l.logError = false
123 | }
124 | }
125 |
126 | func (l *Logger) prependTagPrefix(tag string) string {
127 | if l.config.TagPrefix != "" {
128 | tag = l.config.TagPrefix + "." + tag
129 | }
130 | return tag
131 | }
132 |
--------------------------------------------------------------------------------