├── .gitignore ├── splunk └── v2 │ ├── go.mod │ ├── response_test.go │ ├── writer_test.go │ ├── response.go │ ├── writer.go │ └── splunk.go ├── .travis.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /splunk/v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ZachtimusPrime/Go-Splunk-HTTP/splunk/v2 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | # - 1.5.x 4 | # - 1.6.x 5 | # - 1.7.x 6 | - 1.10.x 7 | - 1.11.x 8 | - 1.12.x 9 | - 1.13.x 10 | - 1.14.x 11 | - 1.15.x 12 | before_install: 13 | # - go get github.com/mattn/goveralls # code coverage tracking 14 | - go get -t -v ./... # grab project dependencies (non-vendor) 15 | install: 16 | # - 17 | before_script: 18 | - gofmt -d -s . # gofmt code so it's pretty 19 | - go vet ./... # some lint-type checking 20 | script: 21 | # - go test -v -race ./... # run go tests 22 | # - $HOME/gopath/bin/goveralls -service=travis-ci # run code coverage 23 | - cd splunk/v2 24 | - go build 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /splunk/v2/response_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestEventCollectorResponseError(t *testing.T) { 9 | invalidEventNumber := 2 10 | ackID := 12345 11 | 12 | testCases := []struct { 13 | name string 14 | input *EventCollectorResponse 15 | expect string 16 | }{ 17 | { 18 | name: "Response is nil", 19 | input: nil, 20 | expect: "", 21 | }, { 22 | name: "All response attributes are set", 23 | input: &EventCollectorResponse{ 24 | Text: "An error", 25 | Code: 10, 26 | InvalidEventNumber: &invalidEventNumber, 27 | AckID: &ackID, 28 | }, 29 | expect: "An error (Code: 10, InvalidEventNumber: 2, AckID: 12345)", 30 | }, { 31 | name: "Some response attributes are set", 32 | input: &EventCollectorResponse{ 33 | Text: "An error", 34 | Code: 10, 35 | AckID: &ackID, 36 | }, 37 | expect: "An error (Code: 10, AckID: 12345)", 38 | }, 39 | } 40 | 41 | for _, tc := range testCases { 42 | t.Run(tc.name, func(t *testing.T) { 43 | errStr := tc.input.Error() 44 | if errStr != tc.expect { 45 | t.Errorf("Expected %q, got %q", tc.expect, errStr) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestStatusCodeHTTPCode(t *testing.T) { 52 | testCases := []struct { 53 | name string 54 | input StatusCode 55 | expectCode int 56 | expectErr bool 57 | }{ 58 | { 59 | name: "Known status code", 60 | input: IncorrectIndex, 61 | expectCode: http.StatusBadRequest, 62 | expectErr: false, 63 | }, { 64 | name: "Unknown status code", 65 | input: StatusCode(100), 66 | expectCode: -1, 67 | expectErr: true, 68 | }, 69 | } 70 | 71 | for _, tc := range testCases { 72 | t.Run(tc.name, func(t *testing.T) { 73 | code, err := tc.input.HTTPCode() 74 | 75 | if !tc.expectErr && err != nil { 76 | t.Fatalf("Unexpected error: %s", err) 77 | } 78 | if tc.expectErr && err == nil { 79 | t.Fatalf("Expected an error to occur") 80 | } 81 | 82 | if code != tc.expectCode { 83 | t.Errorf("Expected %d, got %d", tc.expectCode, code) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /splunk/v2/writer_test.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "sync" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestWriter_Write(t *testing.T) { 15 | numWrites := 1000 16 | numMessages := 0 17 | lock := sync.Mutex{} 18 | notify := make(chan bool, numWrites) 19 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | b, _ := ioutil.ReadAll(r.Body) 21 | split := strings.Split(string(b), "\n") 22 | num := 0 23 | // Since we batch our logs up before we send them: 24 | // Increment our messages counter by one for each JSON object we got in this response 25 | // We don't know how many responses we'll get, we only care about the number of messages 26 | for _, line := range split { 27 | if strings.HasPrefix(line, "{") { 28 | num++ 29 | notify <- true 30 | } 31 | } 32 | lock.Lock() 33 | numMessages = numMessages + num 34 | lock.Unlock() 35 | })) 36 | 37 | // Create a writer that's flushing constantly. We want this test to run 38 | // quickly 39 | writer := Writer{ 40 | Client: NewClient(server.Client(), server.URL, "", "", "", ""), 41 | FlushInterval: 1 * time.Millisecond, 42 | } 43 | // Send a bunch of messages in separate goroutines to make sure we're properly 44 | // testing Writer's concurrency promise 45 | for i := 0; i < numWrites; i++ { 46 | go writer.Write([]byte(fmt.Sprintf("%d", i))) 47 | } 48 | // To notify our test we've collected everything we need. 49 | doneChan := make(chan bool) 50 | go func() { 51 | for i := 0; i < numWrites; i++ { 52 | // Do nothing, just loop through to the next one 53 | <-notify 54 | } 55 | doneChan <- true 56 | }() 57 | select { 58 | case <-doneChan: 59 | // Do nothing, we're good 60 | case <-time.After(1 * time.Second): 61 | t.Errorf("Timed out waiting for messages") 62 | } 63 | // We may have received more than numWrites amount of messages, check that case 64 | if numMessages != numWrites { 65 | t.Errorf("Didn't get the right number of messages, expected %d, got %d", numWrites, numMessages) 66 | } 67 | } 68 | 69 | func TestWriter_Errors(t *testing.T) { 70 | numMessages := 1000 71 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 | w.WriteHeader(http.StatusBadRequest) 73 | fmt.Fprintln(w, "bad request") 74 | })) 75 | writer := Writer{ 76 | Client: NewClient(server.Client(), server.URL, "", "", "", ""), 77 | // Will flush after the last message is sent 78 | FlushThreshold: numMessages - 1, 79 | // Don't let the flush interval cause raciness 80 | FlushInterval: 5 * time.Minute, 81 | } 82 | for i := 0; i < numMessages; i++ { 83 | _, _ = writer.Write([]byte("some data")) 84 | } 85 | select { 86 | case <-writer.Errors(): 87 | // good to go, got our error 88 | case <-time.After(1 * time.Second): 89 | t.Errorf("Timed out waiting for error, should have gotten 1 error") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /splunk/v2/response.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // EventCollectorResponse is the payload returned by the HTTP Event Collector 11 | // in response to requests. 12 | // https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector 13 | type EventCollectorResponse struct { 14 | Text string `json:"text"` 15 | Code StatusCode `json:"code"` 16 | InvalidEventNumber *int `json:"invalid-event-number"` 17 | AckID *int `json:"ackId"` 18 | } 19 | 20 | var _ error = (*EventCollectorResponse)(nil) 21 | 22 | // Error implements the error interface. 23 | func (r *EventCollectorResponse) Error() string { 24 | if r == nil { 25 | return "" 26 | } 27 | 28 | var sb strings.Builder 29 | 30 | sb.WriteString(r.Text + " (Code: " + strconv.Itoa(int(r.Code))) 31 | if r.InvalidEventNumber != nil { 32 | sb.WriteString(", InvalidEventNumber: " + strconv.Itoa(*r.InvalidEventNumber)) 33 | } 34 | if r.AckID != nil { 35 | sb.WriteString(", AckID: " + strconv.Itoa(*r.AckID)) 36 | } 37 | sb.WriteRune(')') 38 | 39 | return sb.String() 40 | } 41 | 42 | // StatusCode defines the meaning of responses returned by HTTP Event Collector 43 | // endpoints. 44 | type StatusCode int8 45 | 46 | const ( 47 | Success StatusCode = iota 48 | TokenDisabled 49 | TokenRequired 50 | InvalidAuthz 51 | InvalidToken 52 | NoData 53 | InvalidDataFormat 54 | IncorrectIndex 55 | InternalServerError 56 | ServerBusy 57 | DataChannelMissing 58 | InvalidDataChannel 59 | EventFieldRequired 60 | EventFieldBlank 61 | ACKDisabled 62 | ErrorHandlingIndexedFields 63 | QueryStringAuthzNotEnabled 64 | ) 65 | 66 | // HTTPCode returns the HTTP code corresponding to the given StatusCode. It 67 | // returns -1 and an error in case the HTTP status code can not be determined. 68 | func (c StatusCode) HTTPCode() (code int, err error) { 69 | switch c { 70 | case Success: 71 | code = http.StatusOK 72 | case TokenDisabled: 73 | code = http.StatusForbidden 74 | case TokenRequired: 75 | code = http.StatusUnauthorized 76 | case InvalidAuthz: 77 | code = http.StatusUnauthorized 78 | case InvalidToken: 79 | code = http.StatusForbidden 80 | case NoData: 81 | code = http.StatusBadRequest 82 | case InvalidDataFormat: 83 | code = http.StatusBadRequest 84 | case IncorrectIndex: 85 | code = http.StatusBadRequest 86 | case InternalServerError: 87 | code = http.StatusInternalServerError 88 | case ServerBusy: 89 | code = http.StatusServiceUnavailable 90 | case DataChannelMissing: 91 | code = http.StatusBadRequest 92 | case InvalidDataChannel: 93 | code = http.StatusBadRequest 94 | case EventFieldRequired: 95 | code = http.StatusBadRequest 96 | case EventFieldBlank: 97 | code = http.StatusBadRequest 98 | case ACKDisabled: 99 | code = http.StatusBadRequest 100 | case ErrorHandlingIndexedFields: 101 | code = http.StatusBadRequest 102 | case QueryStringAuthzNotEnabled: 103 | code = http.StatusBadRequest 104 | default: 105 | code = -1 106 | err = fmt.Errorf("unknown status code %d", c) 107 | } 108 | 109 | return 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-Splunk-HTTP 2 | A simple and lightweight HTTP Splunk logging package for Go. Instantiates a logging connection object to your Splunk server and allows you to submit log events as desired. [Uses HTTP event collection on a Splunk server](http://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector). 3 | 4 | [![GoDoc](https://godoc.org/github.com/ZachtimusPrime/Go-Splunk-HTTP/splunk?status.svg)](https://godoc.org/github.com/ZachtimusPrime/Go-Splunk-HTTP/splunk) 5 | [![Build Status](https://travis-ci.org/ZachtimusPrime/Go-Splunk-HTTP.svg?branch=master)](https://travis-ci.org/ZachtimusPrime/Go-Splunk-HTTP) 6 | [![Coverage Status](https://coveralls.io/repos/github/ZachtimusPrime/Go-Splunk-HTTP/badge.svg?branch=master)](https://coveralls.io/github/ZachtimusPrime/Go-Splunk-HTTP?branch=master) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/ZachtimusPrime/Go-Splunk-HTTP)](https://goreportcard.com/report/github.com/ZachtimusPrime/Go-Splunk-HTTP) 8 | 9 | ## Table of Contents ## 10 | 11 | * [Installation](#installation) 12 | * [Usage](#usage) 13 | 14 | ## Installation ## 15 | 16 | ```bash 17 | go get "github.com/ZachtimusPrime/Go-Splunk-HTTP/splunk/v2" 18 | ``` 19 | 20 | ## Usage ## 21 | 22 | Construct a new Splunk HTTP client, then send log events as desired. 23 | 24 | For example: 25 | 26 | ```go 27 | package main 28 | 29 | import "github.com/ZachtimusPrime/Go-Splunk-HTTP/splunk/v2" 30 | 31 | func main() { 32 | 33 | // Create new Splunk client 34 | splunk := splunk.NewClient( 35 | nil, 36 | "https://{your-splunk-URL}:8088/services/collector", 37 | "{your-token}", 38 | "{your-source}", 39 | "{your-sourcetype}", 40 | "{your-index}" 41 | ) 42 | 43 | // Use the client to send a log with the go host's current time 44 | err := splunk.Log( 45 | interface{"msg": "send key/val pairs or json objects here", "msg2": "anything that is useful to you in the log event"} 46 | ) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // Use the client to send a log with a provided timestamp 52 | err = splunk.LogWithTime( 53 | time.Now(), 54 | interface{"msg": "send key/val pairs or json objects here", "msg2": "anything that is useful to you in the log event"} 55 | ) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | // Use the client to send a batch of log events 61 | var events []splunk.Event 62 | events = append( 63 | events, 64 | splunk.NewEvent( 65 | interface{"msg": "event1"}, 66 | "{desired-source}", 67 | "{desired-sourcetype}", 68 | "{desired-index}" 69 | ) 70 | ) 71 | events = append( 72 | events, 73 | splunk.NewEvent( 74 | interface{"msg": "event2"}, 75 | "{desired-source}", 76 | "{desired-sourcetype}", 77 | "{desired-index}" 78 | ) 79 | ) 80 | err = splunk.LogEvents(events) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | 86 | ``` 87 | 88 | ## Splunk Writer ## 89 | To support logging libraries, and other output, we've added an asynchronous Writer. It supports retries, and different intervals for flushing messages & max log messages in its buffer 90 | 91 | The easiest way to get access to the writer with an existing client is to do: 92 | 93 | ```go 94 | writer := splunkClient.Writer() 95 | ``` 96 | 97 | This will give you an io.Writer you can use to direct output to splunk. However, since the io.Writer() is asynchronous, it will never return an error from its Write() function. To access errors generated from the Client, 98 | Instantiate your Writer this way: 99 | 100 | ```go 101 | splunk.Writer{ 102 | Client: splunkClient 103 | } 104 | ``` 105 | Since the type will now be splunk.Writer(), you can access the `Errors()` function, which returns a channel of errors. You can then spin up a goroutine to listen on this channel and report errors, or you can handle however you like. 106 | 107 | Optionally, you can add more configuration to the writer. 108 | 109 | ```go 110 | splunk.Writer { 111 | Client: splunkClient, 112 | FlushInterval: 10 *time.Second, // How often we'll flush our buffer 113 | FlushThreshold: 25, // Max messages we'll keep in our buffer, regardless of FlushInterval 114 | MaxRetries: 2, // Number of times we'll retry a failed send 115 | } 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /splunk/v2/writer.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | bufferSize = 100 11 | defaultInterval = 2 * time.Second 12 | defaultThreshold = 10 13 | defaultRetries = 2 14 | ) 15 | 16 | // Writer is a threadsafe, aysnchronous splunk writer. 17 | // It implements io.Writer for usage in logging libraries, or whatever you want to send to splunk :) 18 | // Writer.Client's configuration determines what source, sourcetype & index will be used for events 19 | // Example for logrus: 20 | // splunkWriter := &splunk.Writer {Client: client} 21 | // logrus.SetOutput(io.MultiWriter(os.Stdout, splunkWriter)) 22 | type Writer struct { 23 | Client *Client 24 | // How often the write buffer should be flushed to splunk 25 | FlushInterval time.Duration 26 | // How many Write()'s before buffer should be flushed to splunk 27 | FlushThreshold int 28 | // Max number of retries we should do when we flush the buffer 29 | MaxRetries int 30 | dataChan chan *message 31 | errors chan error 32 | once sync.Once 33 | } 34 | 35 | // Associates some bytes with the time they were written 36 | // Helpful if we have long flush intervals to more precisely record the time at which 37 | // a message was written 38 | type message struct { 39 | data json.RawMessage 40 | writtenAt time.Time 41 | } 42 | 43 | // Writer asynchronously writes to splunk in batches 44 | func (w *Writer) Write(b []byte) (int, error) { 45 | // only initialize once. Keep all of our buffering in one thread 46 | w.once.Do(func() { 47 | // synchronously set up dataChan 48 | w.dataChan = make(chan *message, bufferSize) 49 | // Spin up single goroutine to listen to our writes 50 | w.errors = make(chan error, bufferSize) 51 | go w.listen() 52 | }) 53 | // Make a local copy of the bytearray so it doesn't get overwritten by 54 | // the next call to Write() 55 | var b2 = make([]byte, len(b)) 56 | copy(b2, b) 57 | // Send the data to the channel 58 | w.dataChan <- &message{ 59 | data: b2, 60 | writtenAt: time.Now(), 61 | } 62 | // We don't know if we've hit any errors yet, so just say we're good 63 | return len(b), nil 64 | } 65 | 66 | // Errors returns a buffered channel of errors. Might be filled over time, might not 67 | // Useful if you want to record any errors hit when sending data to splunk 68 | func (w *Writer) Errors() <-chan error { 69 | return w.errors 70 | } 71 | 72 | // listen for messages 73 | func (w *Writer) listen() { 74 | if w.FlushInterval <= 0 { 75 | w.FlushInterval = defaultInterval 76 | } 77 | if w.FlushThreshold == 0 { 78 | w.FlushThreshold = defaultThreshold 79 | } 80 | ticker := time.NewTicker(w.FlushInterval) 81 | buffer := make([]*message, 0) 82 | //Define function so we can flush in several places 83 | flush := func() { 84 | // Go send the data to splunk 85 | go w.send(buffer, w.MaxRetries) 86 | // Make a new array since the old one is getting used by the splunk client now 87 | buffer = make([]*message, 0) 88 | } 89 | for { 90 | select { 91 | case <-ticker.C: 92 | if len(buffer) > 0 { 93 | flush() 94 | } 95 | case d := <-w.dataChan: 96 | buffer = append(buffer, d) 97 | if len(buffer) > w.FlushThreshold { 98 | flush() 99 | } 100 | } 101 | } 102 | } 103 | 104 | // send sends data to splunk, retrying upon failure 105 | func (w *Writer) send(messages []*message, retries int) { 106 | // Create events from our data so we can send them to splunk 107 | events := make([]*Event, len(messages)) 108 | for i, m := range messages { 109 | // Use the configuration of the Client for the event 110 | events[i] = w.Client.NewEventWithTime(m.writtenAt, m.data, w.Client.Source, w.Client.SourceType, w.Client.Index) 111 | } 112 | // Send the events to splunk 113 | err := w.Client.LogEvents(events) 114 | // If we had any failures, retry as many times as they requested 115 | if err != nil { 116 | for i := 0; i < retries; i++ { 117 | // retry 118 | err = w.Client.LogEvents(events) 119 | if err == nil { 120 | return 121 | } 122 | } 123 | // if we've exhausted our max retries, let someone know via Errors() 124 | // might not have retried if retries == 0 125 | select { 126 | case w.errors <- err: 127 | // Don't block in case no one is listening or our errors channel is full 128 | default: 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /splunk/v2/splunk.go: -------------------------------------------------------------------------------- 1 | package splunk 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "os" 13 | "time" 14 | ) 15 | 16 | // Event represents the log event object that is sent to Splunk when Client.Log is called. 17 | type Event struct { 18 | Time EventTime `json:"time"` // when the event happened 19 | Host string `json:"host"` // hostname 20 | Source string `json:"source,omitempty"` // optional description of the source of the event; typically the app's name 21 | SourceType string `json:"sourcetype,omitempty"` // optional name of a Splunk parsing configuration; this is usually inferred by Splunk 22 | Index string `json:"index,omitempty"` // optional name of the Splunk index to store the event in; not required if the token has a default index set in Splunk 23 | Event interface{} `json:"event"` // throw any useful key/val pairs here 24 | } 25 | 26 | // EventTime marshals timestamps using the Splunk HTTP Event Collector's default format. 27 | type EventTime struct { 28 | time.Time 29 | } 30 | 31 | func (t EventTime) MarshalJSON() ([]byte, error) { 32 | // The milliseconds are truncated, not rounded to nearest; eg. 12:00:00.5008274 will be logged as 12:00:00.500. 33 | return []byte(fmt.Sprintf("%d.%d", t.Unix(), t.Nanosecond()/1e6)), nil 34 | } 35 | 36 | // Client manages communication with Splunk's HTTP Event Collector. 37 | // New client objects should be created using the NewClient function. 38 | // 39 | // The URL field must be defined and pointed at a Splunk servers Event Collector port (i.e. https://{your-splunk-URL}:8088/services/collector). 40 | // The Token field must be defined with your access token to the Event Collector. 41 | type Client struct { 42 | HTTPClient *http.Client // HTTP client used to communicate with the API 43 | URL string 44 | Hostname string 45 | Token string 46 | Source string //Default source 47 | SourceType string //Default source type 48 | Index string //Default index 49 | } 50 | 51 | // NewClient creates a new client to Splunk. 52 | // This should be the primary way a Splunk client object is constructed. 53 | // 54 | // If an httpClient object is specified it will be used instead of the 55 | // default http.DefaultClient. 56 | func NewClient(httpClient *http.Client, URL string, Token string, Source string, SourceType string, Index string) *Client { 57 | // Create a new client 58 | if httpClient == nil { 59 | tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}} 60 | httpClient = &http.Client{Timeout: time.Second * 20, Transport: tr} 61 | } 62 | hostname, _ := os.Hostname() 63 | c := &Client{ 64 | HTTPClient: httpClient, 65 | URL: URL, 66 | Hostname: hostname, 67 | Token: Token, 68 | Source: Source, 69 | SourceType: SourceType, 70 | Index: Index, 71 | } 72 | return c 73 | } 74 | 75 | // NewEvent creates a new log event to send to Splunk. 76 | // This should be the primary way a Splunk log object is constructed, and is automatically called by the Log function attached to the client. 77 | // This method takes the current timestamp for the event, meaning that the event is generated at runtime. 78 | func (c *Client) NewEvent(event interface{}, source string, sourcetype string, index string) *Event { 79 | e := &Event{ 80 | Time: EventTime{time.Now()}, 81 | Host: c.Hostname, 82 | Source: source, 83 | SourceType: sourcetype, 84 | Index: index, 85 | Event: event, 86 | } 87 | return e 88 | } 89 | 90 | // NewEventWithTime creates a new log event with a specified timetamp to send to Splunk. 91 | // This is similar to NewEvent but if you want to log in a different time rather than time.Now this becomes handy. If that's 92 | // the case, use this function to create the Event object and the the LogEvent function. 93 | func (c *Client) NewEventWithTime(t time.Time, event interface{}, source string, sourcetype string, index string) *Event { 94 | e := &Event{ 95 | Time: EventTime{time.Now()}, 96 | Host: c.Hostname, 97 | Source: source, 98 | SourceType: sourcetype, 99 | Index: index, 100 | Event: event, 101 | } 102 | return e 103 | } 104 | 105 | // Client.Log is used to construct a new log event and POST it to the Splunk server. 106 | // 107 | // All that must be provided for a log event are the desired map[string]string key/val pairs. These can be anything 108 | // that provide context or information for the situation you are trying to log (i.e. err messages, status codes, etc). 109 | // The function auto-generates the event timestamp and hostname for you. 110 | func (c *Client) Log(event interface{}) error { 111 | // create Splunk log 112 | log := c.NewEvent(event, c.Source, c.SourceType, c.Index) 113 | return c.LogEvent(log) 114 | } 115 | 116 | // Client.LogWithTime is used to construct a new log event with a specified timestamp and POST it to the Splunk server. 117 | // 118 | // This is similar to Client.Log, just with the t parameter. 119 | func (c *Client) LogWithTime(t time.Time, event interface{}) error { 120 | // create Splunk log 121 | log := c.NewEventWithTime(t, event, c.Source, c.SourceType, c.Index) 122 | return c.LogEvent(log) 123 | } 124 | 125 | // Client.LogEvent is used to POST a single event to the Splunk server. 126 | func (c *Client) LogEvent(e *Event) error { 127 | // Convert requestBody struct to byte slice to prep for http.NewRequest 128 | b, err := json.Marshal(e) 129 | if err != nil { 130 | return err 131 | } 132 | return c.doRequest(bytes.NewBuffer(b)) 133 | } 134 | 135 | // Client.LogEvents is used to POST multiple events with a single request to the Splunk server. 136 | func (c *Client) LogEvents(events []*Event) error { 137 | buf := new(bytes.Buffer) 138 | for _, e := range events { 139 | b, err := json.Marshal(e) 140 | if err != nil { 141 | return err 142 | } 143 | buf.Write(b) 144 | // Each json object should be separated by a blank line 145 | buf.WriteString("\r\n\r\n") 146 | } 147 | // Convert requestBody struct to byte slice to prep for http.NewRequest 148 | return c.doRequest(buf) 149 | } 150 | 151 | //Writer is a convience method for creating an io.Writer from a Writer with default values 152 | func (c *Client) Writer() io.Writer { 153 | return &Writer{ 154 | Client: c, 155 | } 156 | } 157 | 158 | // Client.doRequest is used internally to POST the bytes of events to the Splunk server. 159 | func (c *Client) doRequest(b *bytes.Buffer) error { 160 | // make new request 161 | url := c.URL 162 | req, err := http.NewRequest("POST", url, b) 163 | req.Header.Add("Content-Type", "application/json") 164 | req.Header.Add("Authorization", "Splunk "+c.Token) 165 | 166 | // receive response 167 | res, err := c.HTTPClient.Do(req) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | // need to make sure we close the body to avoid hanging the connection 173 | defer res.Body.Close() 174 | 175 | // If statusCode is not OK, return the error 176 | switch res.StatusCode { 177 | case 200: 178 | // need to read the reply otherwise the connection hangs 179 | io.Copy(ioutil.Discard, res.Body) 180 | return nil 181 | default: 182 | respBody, err := ioutil.ReadAll(res.Body) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | // try deserializing response body to a typed HEC response 188 | hecResp := &EventCollectorResponse{} 189 | if err := json.Unmarshal(respBody, hecResp); err == nil { 190 | return hecResp 191 | } 192 | 193 | // otherwise, return the response body as an error string 194 | return errors.New(string(respBody)) 195 | } 196 | } 197 | --------------------------------------------------------------------------------