-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add an io.Writer to splunk This allows us to use splunk as an output source in a much more flexible way in particular, it allows us to be non blocking for uploading to splunk Fixes: #6 * Add Writer to README.md Fixes #6 * Add configuration details Fixes #6
- Loading branch information
1 parent
2e2b5d4
commit 6a3d71e
Showing
4 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package splunk | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
) | ||
|
||
const ( | ||
bufferSize = 100 | ||
defaultInterval = 2 * time.Second | ||
defaultThreshold = 10 | ||
defaultRetries = 2 | ||
) | ||
|
||
// Writer is a threadsafe, aysnchronous splunk writer. | ||
// It implements io.Writer for usage in logging libraries, or whatever you want to send to splunk :) | ||
// Writer.Client's configuration determines what source, sourcetype & index will be used for events | ||
// Example for logrus: | ||
// splunkWriter := &splunk.Writer {Client: client} | ||
// logrus.SetOutput(io.MultiWriter(os.Stdout, splunkWriter)) | ||
type Writer struct { | ||
Client *Client | ||
// How often the write buffer should be flushed to splunk | ||
FlushInterval time.Duration | ||
// How many Write()'s before buffer should be flushed to splunk | ||
FlushThreshold int | ||
// Max number of retries we should do when we flush the buffer | ||
MaxRetries int | ||
dataChan chan *message | ||
errors chan error | ||
once sync.Once | ||
} | ||
|
||
// Associates some bytes with the time they were written | ||
// Helpful if we have long flush intervals to more precisely record the time at which | ||
// a message was written | ||
type message struct { | ||
data []byte | ||
writtenAt time.Time | ||
} | ||
|
||
// Writer asynchronously writes to splunk in batches | ||
func (w *Writer) Write(b []byte) (int, error) { | ||
// only initialize once. Keep all of our buffering in one thread | ||
w.once.Do(func() { | ||
// synchronously set up dataChan | ||
w.dataChan = make(chan *message, bufferSize) | ||
// Spin up single goroutine to listen to our writes | ||
w.errors = make(chan error, bufferSize) | ||
go w.listen() | ||
}) | ||
// Send the data to the channel | ||
w.dataChan <- &message{ | ||
data: b, | ||
writtenAt: time.Now(), | ||
} | ||
// We don't know if we've hit any errors yet, so just say we're good | ||
return len(b), nil | ||
} | ||
|
||
// Errors returns a buffered channel of errors. Might be filled over time, might not | ||
// Useful if you want to record any errors hit when sending data to splunk | ||
func (w *Writer) Errors() <-chan error { | ||
return w.errors | ||
} | ||
|
||
// listen for messages | ||
func (w *Writer) listen() { | ||
if w.FlushInterval <= 0 { | ||
w.FlushInterval = defaultInterval | ||
} | ||
if w.FlushThreshold == 0 { | ||
w.FlushThreshold = defaultThreshold | ||
} | ||
ticker := time.NewTicker(w.FlushInterval) | ||
buffer := make([]*message, 0) | ||
//Define function so we can flush in several places | ||
flush := func() { | ||
// Go send the data to splunk | ||
go w.send(buffer, w.MaxRetries) | ||
// Make a new array since the old one is getting used by the splunk client now | ||
buffer = make([]*message, 0) | ||
} | ||
for { | ||
select { | ||
case <-ticker.C: | ||
if len(buffer) > 0 { | ||
flush() | ||
} | ||
case d := <-w.dataChan: | ||
buffer = append(buffer, d) | ||
if len(buffer) > w.FlushThreshold { | ||
flush() | ||
} | ||
} | ||
} | ||
} | ||
|
||
// send sends data to splunk, retrying upon failure | ||
func (w *Writer) send(messages []*message, retries int) { | ||
// Create events from our data so we can send them to splunk | ||
events := make([]*Event, len(messages)) | ||
for i, m := range messages { | ||
// Use the configuration of the Client for the event | ||
events[i] = w.Client.NewEventWithTime(m.writtenAt.Unix(), m.data, w.Client.Source, w.Client.SourceType, w.Client.Index) | ||
} | ||
// Send the events to splunk | ||
err := w.Client.LogEvents(events) | ||
// If we had any failures, retry as many times as they requested | ||
if err != nil { | ||
for i := 0; i < retries; i++ { | ||
// retry | ||
err = w.Client.LogEvents(events) | ||
if err == nil { | ||
return | ||
} | ||
} | ||
// if we've exhausted our max retries, let someone know via Errors() | ||
// might not have retried if retries == 0 | ||
select { | ||
case w.errors <- err: | ||
// Don't block in case no one is listening or our errors channel is full | ||
default: | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package splunk | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"sync" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestWriter_Write(t *testing.T) { | ||
numWrites := 1000 | ||
numMessages := 0 | ||
lock := sync.Mutex{} | ||
notify := make(chan bool, numWrites) | ||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
b, _ := ioutil.ReadAll(r.Body) | ||
split := strings.Split(string(b), "\n") | ||
num := 0 | ||
// Since we batch our logs up before we send them: | ||
// Increment our messages counter by one for each JSON object we got in this response | ||
// We don't know how many responses we'll get, we only care about the number of messages | ||
for _, line := range split { | ||
if strings.HasPrefix(line, "{") { | ||
num++ | ||
notify <- true | ||
} | ||
} | ||
lock.Lock() | ||
numMessages = numMessages + num | ||
lock.Unlock() | ||
})) | ||
|
||
// Create a writer that's flushing constantly. We want this test to run | ||
// quickly | ||
writer := Writer{ | ||
Client: NewClient(server.Client(), server.URL, "", "", "", ""), | ||
FlushInterval: 1 * time.Millisecond, | ||
} | ||
// Send a bunch of messages in separate goroutines to make sure we're properly | ||
// testing Writer's concurrency promise | ||
for i := 0; i < numWrites; i++ { | ||
go writer.Write([]byte(fmt.Sprintf("%d", i))) | ||
} | ||
// To notify our test we've collected everything we need. | ||
doneChan := make(chan bool) | ||
go func() { | ||
for i := 0; i < numWrites; i++ { | ||
// Do nothing, just loop through to the next one | ||
<-notify | ||
} | ||
doneChan <- true | ||
}() | ||
select { | ||
case <-doneChan: | ||
// Do nothing, we're good | ||
case <-time.After(1 * time.Second): | ||
t.Errorf("Timed out waiting for messages") | ||
} | ||
// We may have received more than numWrites amount of messages, check that case | ||
if numMessages != numWrites { | ||
t.Errorf("Didn't get the right number of messages, expected %d, got %d", numWrites, numMessages) | ||
} | ||
} | ||
|
||
func TestWriter_Errors(t *testing.T) { | ||
numMessages := 1000 | ||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusBadRequest) | ||
fmt.Fprintln(w, "bad request") | ||
})) | ||
writer := Writer{ | ||
Client: NewClient(server.Client(), server.URL, "", "", "", ""), | ||
// Will flush after the last message is sent | ||
FlushThreshold: numMessages - 1, | ||
// Don't let the flush interval cause raciness | ||
FlushInterval: 5 * time.Minute, | ||
} | ||
for i := 0; i < numMessages; i++ { | ||
_, _ = writer.Write([]byte("some data")) | ||
} | ||
select { | ||
case <-writer.Errors(): | ||
// good to go, got our error | ||
case <-time.After(1 * time.Second): | ||
t.Errorf("Timed out waiting for error, should have gotten 1 error") | ||
} | ||
} |