Skip to content

Commit 51b2290

Browse files
committed
Add loggly handler.
This handler sends log15 records to loggly. No third party dependencies are required.
1 parent 7cf5571 commit 51b2290

File tree

3 files changed

+376
-0
lines changed

3 files changed

+376
-0
lines changed

Diff for: ext/loggly/loggly.go

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package loggly
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"os"
11+
"strconv"
12+
"strings"
13+
"time"
14+
15+
"gopkg.in/inconshreveable/log15.v2"
16+
)
17+
18+
const (
19+
// TokenPlaceholder is used to replace the token in a URI or request with the actual client token.
20+
TokenPlaceholder = `{token}`
21+
)
22+
23+
var (
24+
// EndpointBase can be modified to a custom URI.
25+
// This should be done before a new LogglyHandler is created.
26+
EndpointSingle = `https://logs-01.loggly.com/inputs/` + TokenPlaceholder
27+
28+
// EndpointBulkBase can be modified to a custom URI
29+
// This should be done before a new LogglyHandler is created.
30+
EndpointBulk = `https://logs-01.loggly.com/bulk/` + TokenPlaceholder
31+
)
32+
33+
// LogglyHandler sends logs to Loggly.
34+
// LogglyHandler should be created by NewLogglyHandler.
35+
// Exported fields can be modified during setup, but should not be touched when the Handler is in use.
36+
// LogglyHandler implements log15.Handler
37+
type LogglyHandler struct {
38+
// Client can be modified or replaced with a custom http.Client
39+
Client *http.Client
40+
41+
// Defaults contains key/value items that are added to every log message.
42+
// Extra values can be added during the log15 setup.
43+
//
44+
// NewLogglyHandler adds a single record: "hostname", with the return value from os.Hostname().
45+
// When os.Hostname() returns with an error, the key "hostname" is not set and this map will be empty.
46+
Defaults map[string]interface{}
47+
48+
// Tags are sent to loggly with the log.
49+
Tags []string
50+
51+
// Endpoint can not be modified after the LogglyHandler was created.
52+
endpointSingle string
53+
endpointBulk string
54+
}
55+
56+
// NewLogglyHandler creates a new LogglyHandler instance
57+
// Exported field on the LogglyHandler can modified before it is being used.
58+
func NewLogglyHandler(token string) *LogglyHandler {
59+
lh := &LogglyHandler{
60+
endpointSingle: strings.Replace(EndpointSingle, TokenPlaceholder, token, -1),
61+
endpointBulk: strings.Replace(EndpointBulk, TokenPlaceholder, token, -1),
62+
63+
Client: &http.Client{},
64+
65+
Defaults: make(map[string]interface{}),
66+
}
67+
68+
// if hostname is retrievable, set it as extra field
69+
if hostname, err := os.Hostname(); err == nil {
70+
lh.Defaults["hostname"] = hostname
71+
}
72+
73+
return lh
74+
}
75+
76+
// Log sends the given *log15.Record to loggly.
77+
// Standard fields are:
78+
// - message, the record's message.
79+
// - level, the record's level as string.
80+
// - timestamp, the record's timestamp in UTC timezone truncated to microseconds.
81+
// - context, (optional) the context fields from the record.
82+
// Extra fields are the configurable with the LogglyHandler.Defaults map
83+
// By default this contains:
84+
// - hostname, the system hostname
85+
func (lh *LogglyHandler) Log(r *log15.Record) error {
86+
// create message structure
87+
msg := lh.createMessage(r)
88+
89+
// send message
90+
err := lh.sendSingle(msg)
91+
if err != nil {
92+
return err
93+
}
94+
95+
return nil
96+
}
97+
98+
// createMessage takes a log15.Record and returns a loggly message structure
99+
func (lh *LogglyHandler) createMessage(r *log15.Record) map[string]interface{} {
100+
// set standard values
101+
msg := map[string]interface{}{
102+
"message": r.Msg,
103+
"level": r.Lvl.String(),
104+
// for loggly we need to truncate the timestamp to microsecond precision and convert it to UTC timezone
105+
"timestamp": r.Time.Truncate(time.Microsecond).In(time.UTC),
106+
}
107+
108+
// apply defaults
109+
for key, value := range lh.Defaults {
110+
msg[key] = value
111+
}
112+
113+
// optionally add context
114+
if len(r.Ctx) > 0 {
115+
context := make(map[string]interface{}, len(r.Ctx)/2)
116+
for i := 0; i < len(r.Ctx); i += 2 {
117+
key := r.Ctx[i]
118+
value := r.Ctx[i+1]
119+
keyStr, ok := key.(string)
120+
if !ok {
121+
keyStr = fmt.Sprintf("%v", key)
122+
}
123+
context[keyStr] = value
124+
}
125+
msg["context"] = context
126+
}
127+
128+
// got a nice message to deliver
129+
return msg
130+
}
131+
132+
// sendSingle sends a single loggly structure to their http endpoint
133+
func (lh *LogglyHandler) sendSingle(msg map[string]interface{}) error {
134+
// encode the message to json
135+
postBuffer := &bytes.Buffer{}
136+
err := json.NewEncoder(postBuffer).Encode(msg)
137+
if err != nil {
138+
return err
139+
}
140+
141+
// create request
142+
req, err := http.NewRequest("POST", lh.endpointSingle, postBuffer)
143+
req.Header.Add("User-Agent", "log15")
144+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
145+
req.Header.Add("Content-Length", strconv.Itoa(postBuffer.Len()))
146+
147+
// apply tags
148+
if len(lh.Tags) > 0 {
149+
req.Header.Add("X-Loggly-Tag", strings.Join(lh.Tags, ","))
150+
}
151+
152+
// do request
153+
resp, err := lh.Client.Do(req)
154+
if err != nil {
155+
return err
156+
}
157+
defer resp.Body.Close()
158+
159+
// check statuscode
160+
if resp.StatusCode != 200 {
161+
resp, _ := ioutil.ReadAll(resp.Body)
162+
return fmt.Errorf("error: %s", string(resp))
163+
}
164+
165+
// validate response
166+
response := &logglyResponse{}
167+
err = json.NewDecoder(resp.Body).Decode(&response)
168+
if err != nil {
169+
return err
170+
}
171+
if response.Response != "ok" {
172+
return errors.New(`loggly response was not "ok"`)
173+
}
174+
175+
// all done
176+
return nil
177+
}

Diff for: ext/loggly/response.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package loggly
2+
3+
//go:generate ffjson $GOFILE
4+
5+
// logglyResponse defines the json returned by the loggly endpoint.
6+
// The value for Response should be "ok". Unmarshalling is optimized by ffjson.
7+
type logglyResponse struct {
8+
Response string `json:"response"`
9+
}

Diff for: ext/loggly/response_ffjson.go

+190
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)