├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client.go ├── client_test.go ├── cluster.go ├── cluster_test.go ├── errors.go ├── event.go ├── example └── main.go ├── glide.lock ├── glide.yaml ├── go.mod ├── go.sum ├── hec.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: go 3 | go: 4 | - 1.7.x 5 | services: 6 | - docker 7 | before_install: 8 | - sudo add-apt-repository -y ppa:masterminds/glide && sudo apt-get update 9 | - sudo apt-get install -y glide 10 | - glide install 11 | install: 12 | - go build -o build/example ./example/main.go 13 | before_script: 14 | - docker pull fuyufjh/docker-splunk-hec:6.5.0 15 | - docker run -d --name=splunk -p8000:8000 -p8088:8088 -p8089:8089 --env SPLUNK_START_ARGS="--accept-license" fuyufjh/docker-splunk-hec:6.5.0 16 | - sleep 10 17 | script: 18 | - go test 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Splunk HEC Golang Library 2 | ========================= 3 | 4 | [![Build Status](https://travis-ci.org/fuyufjh/splunk-hec-go.svg?branch=master)](https://travis-ci.org/fuyufjh/splunk-hec-go) 5 | 6 | Golang library for Splunk HTTP Event Collector (HEC). 7 | 8 | ## Build 9 | 10 | You need install [glide](https://github.com/Masterminds/glide) before build. 11 | 12 | Install all dependencies 13 | 14 | ```bash 15 | glide install 16 | ``` 17 | 18 | Build the example 19 | 20 | ```bash 21 | go build -o build/example ./example/main.go 22 | ``` 23 | 24 | ## Features 25 | 26 | - [x] Support HEC JSON mode and Raw mode 27 | - [x] Send batch of events 28 | - [x] Customize retrying times 29 | - [x] Cut big batch into chunk less than MaxContentLength 30 | - [x] Indexer acknowledgement 31 | - [ ] Streaming data via HEC Raw 32 | 33 | ## Example 34 | 35 | ```go 36 | client := hec.NewCluster( 37 | []string{"https://127.0.0.1:8088", "https://localhost:8088"}, 38 | "00000000-0000-0000-0000-000000000000", 39 | ) 40 | client.SetHTTPClient(&http.Client{Transport: &http.Transport{ 41 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 42 | }}) 43 | 44 | event1 := hec.NewEvent("event one") 45 | event1.SetTime(time.Now()) 46 | event2 := hec.NewEvent("event two") 47 | event2.SetTime(time.Now().Add(-time.Minute)) 48 | 49 | err := client.WriteBatch([]*hec.Event{event1, event2}) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | ``` 54 | 55 | See `hec.go` for more usages. 56 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "strconv" 13 | "sync" 14 | "time" 15 | 16 | "github.com/google/uuid" 17 | ) 18 | 19 | const ( 20 | retryWaitTime = 1 * time.Second 21 | 22 | defaultMaxContentLength = 1000000 23 | 24 | defaultAcknowledgementTimeout = 90 * time.Second 25 | ) 26 | 27 | type Client struct { 28 | HEC 29 | 30 | // HTTP Client for communication with (optional) 31 | httpClient *http.Client 32 | 33 | // Splunk Server URL for API requests (required) 34 | serverURL string 35 | 36 | // HEC Token (required) 37 | token string 38 | 39 | // Keep-Alive (optional, default: true) 40 | keepAlive bool 41 | 42 | // Channel (required for Raw mode) 43 | channel string 44 | 45 | // Max retrying times (optional, default: 2) 46 | retries int 47 | 48 | // Max content length (optional, default: 1000000) 49 | maxLength int 50 | 51 | // List of acknowledgement IDs provided by Splunk 52 | ackIDs []int 53 | 54 | // Mutex to allow threadsafe acknowledgement checking 55 | ackMux sync.Mutex 56 | 57 | // Compression type, "" and "gzip" are supported 58 | compression string 59 | } 60 | 61 | func NewClient(serverURL string, token string) HEC { 62 | id := uuid.New() 63 | 64 | return &Client{ 65 | httpClient: http.DefaultClient, 66 | serverURL: serverURL, 67 | token: token, 68 | keepAlive: true, 69 | channel: id.String(), 70 | retries: 2, 71 | maxLength: defaultMaxContentLength, 72 | } 73 | } 74 | 75 | func (hec *Client) SetHTTPClient(client *http.Client) { 76 | hec.httpClient = client 77 | } 78 | 79 | func (hec *Client) SetKeepAlive(enable bool) { 80 | hec.keepAlive = enable 81 | } 82 | 83 | func (hec *Client) SetChannel(channel string) { 84 | hec.channel = channel 85 | } 86 | 87 | func (hec *Client) SetMaxRetry(retries int) { 88 | hec.retries = retries 89 | } 90 | 91 | func (hec *Client) SetMaxContentLength(size int) { 92 | hec.maxLength = size 93 | } 94 | 95 | func (hec *Client) SetCompression(compression string) { 96 | hec.compression = compression 97 | } 98 | 99 | func (hec *Client) WriteEventWithContext(ctx context.Context, event *Event) error { 100 | if event.empty() { 101 | return nil // skip empty events 102 | } 103 | 104 | endpoint := "/services/collector?channel=" + hec.channel 105 | data, _ := json.Marshal(event) 106 | 107 | if len(data) > hec.maxLength { 108 | return ErrEventTooLong 109 | } 110 | return hec.write(ctx, endpoint, data) 111 | } 112 | 113 | func (hec *Client) WriteEvent(event *Event) error { 114 | return hec.WriteEventWithContext(context.Background(), event) 115 | } 116 | 117 | func (hec *Client) WriteBatchWithContext(ctx context.Context, events []*Event) error { 118 | if len(events) == 0 { 119 | return nil 120 | } 121 | 122 | endpoint := "/services/collector?channel=" + hec.channel 123 | var buffer bytes.Buffer 124 | var tooLongs []int 125 | 126 | for index, event := range events { 127 | if event.empty() { 128 | continue // skip empty events 129 | } 130 | 131 | data, _ := json.Marshal(event) 132 | if len(data) > hec.maxLength { 133 | tooLongs = append(tooLongs, index) 134 | continue 135 | } 136 | // Send out bytes in buffer immediately if the limit exceeded after adding this event 137 | if buffer.Len()+len(data) > hec.maxLength { 138 | if err := hec.write(ctx, endpoint, buffer.Bytes()); err != nil { 139 | return err 140 | } 141 | buffer.Reset() 142 | } 143 | buffer.Write(data) 144 | } 145 | 146 | if buffer.Len() > 0 { 147 | if err := hec.write(ctx, endpoint, buffer.Bytes()); err != nil { 148 | return err 149 | } 150 | } 151 | if len(tooLongs) > 0 { 152 | return ErrEventTooLong 153 | } 154 | return nil 155 | } 156 | 157 | func (hec *Client) WriteBatch(events []*Event) error { 158 | return hec.WriteBatchWithContext(context.Background(), events) 159 | } 160 | 161 | type EventMetadata struct { 162 | Host *string 163 | Index *string 164 | Source *string 165 | SourceType *string 166 | Time *time.Time 167 | } 168 | 169 | func (hec *Client) WriteRawWithContext(ctx context.Context, reader io.ReadSeeker, metadata *EventMetadata) error { 170 | endpoint := rawHecEndpoint(hec.channel, metadata) 171 | 172 | return breakStream(reader, hec.maxLength, func(chunk []byte) error { 173 | if err := hec.write(ctx, endpoint, chunk); err != nil { 174 | // Ignore NoData error (e.g. "\n\n" will cause NoData error) 175 | if res, ok := err.(*Response); !ok || res.Code != StatusNoData { 176 | return err 177 | } 178 | } 179 | return nil 180 | }) 181 | } 182 | 183 | func (hec *Client) WriteRaw(reader io.ReadSeeker, metadata *EventMetadata) error { 184 | return hec.WriteRawWithContext(context.Background(), reader, metadata) 185 | } 186 | 187 | type acknowledgementRequest struct { 188 | Acks []int `json:"acks"` 189 | } 190 | 191 | // WaitForAcknowledgementWithContext blocks until the Splunk indexer has 192 | // acknowledged that all previously submitted data has been successfully 193 | // indexed or if the provided context is cancelled. This requires the HEC token 194 | // configuration in Splunk to have indexer acknowledgement enabled. 195 | func (hec *Client) WaitForAcknowledgementWithContext(ctx context.Context) error { 196 | // Make our own copy of the list of acknowledgement IDs and remove them 197 | // from the client while we check them. 198 | hec.ackMux.Lock() 199 | ackIDs := hec.ackIDs 200 | hec.ackIDs = nil 201 | hec.ackMux.Unlock() 202 | 203 | if len(ackIDs) == 0 { 204 | return nil 205 | } 206 | 207 | endpoint := "/services/collector/ack?channel=" + hec.channel 208 | 209 | for { 210 | ackRequestData, _ := json.Marshal(acknowledgementRequest{Acks: ackIDs}) 211 | 212 | response, err := hec.makeRequest(ctx, endpoint, ackRequestData) 213 | if err != nil { 214 | // Put the remaining unacknowledged IDs back 215 | hec.ackMux.Lock() 216 | hec.ackIDs = append(hec.ackIDs, ackIDs...) 217 | hec.ackMux.Unlock() 218 | return err 219 | } 220 | 221 | for ackIDString, status := range response.Acks { 222 | if status { 223 | ackID, err := strconv.Atoi(ackIDString) 224 | if err != nil { 225 | return fmt.Errorf("could not convert ack ID to int: %v", err) 226 | } 227 | 228 | ackIDs = remove(ackIDs, ackID) 229 | } 230 | } 231 | 232 | if len(ackIDs) == 0 { 233 | break 234 | } 235 | 236 | // If the server did not indicate that all acknowledgements have been 237 | // made, check again after a short delay. 238 | select { 239 | case <-time.After(retryWaitTime): 240 | continue 241 | case <-ctx.Done(): 242 | // Put the remaining unacknowledged IDs back 243 | hec.ackMux.Lock() 244 | hec.ackIDs = append(hec.ackIDs, ackIDs...) 245 | hec.ackMux.Unlock() 246 | return ctx.Err() 247 | } 248 | } 249 | 250 | return nil 251 | } 252 | 253 | // WaitForAcknowledgement blocks until the Splunk indexer has acknowledged 254 | // that all previously submitted data has been successfully indexed or if the 255 | // default acknowledgement timeout is reached. This requires the HEC token 256 | // configuration in Splunk to have indexer acknowledgement enabled. 257 | func (hec *Client) WaitForAcknowledgement() error { 258 | ctx, cancel := context.WithTimeout(context.Background(), defaultAcknowledgementTimeout) 259 | defer cancel() 260 | return hec.WaitForAcknowledgementWithContext(ctx) 261 | } 262 | 263 | // breakStream breaks text from reader into chunks, with every chunk less than max. 264 | // Unless a single line is longer than max, it always cut at end of lines ("\n") 265 | func breakStream(reader io.ReadSeeker, max int, callback func(chunk []byte) error) error { 266 | 267 | var buf []byte = make([]byte, max+1) 268 | var writeAt int 269 | for { 270 | n, err := reader.Read(buf[writeAt:max]) 271 | if n == 0 && err == io.EOF { 272 | break 273 | } 274 | 275 | // If last line does not end with LF, add one for it 276 | if err == io.EOF && buf[writeAt+n-1] != '\n' { 277 | n++ 278 | buf[writeAt+n-1] = '\n' 279 | } 280 | 281 | data := buf[0 : writeAt+n] 282 | 283 | // Cut after the last LF character 284 | cut := bytes.LastIndexByte(data, '\n') + 1 285 | if cut == 0 { 286 | // This line is too long, but just let it break here 287 | cut = len(data) 288 | } 289 | if err := callback(buf[:cut]); err != nil { 290 | return err 291 | } 292 | 293 | writeAt = copy(buf, data[cut:]) 294 | 295 | if err != nil && err != io.EOF { 296 | return err 297 | } 298 | } 299 | 300 | if writeAt != 0 { 301 | return callback(buf[:writeAt]) 302 | } 303 | 304 | return nil 305 | } 306 | 307 | func responseFrom(body []byte) *Response { 308 | var res Response 309 | json.Unmarshal(body, &res) 310 | return &res 311 | } 312 | 313 | func (res *Response) Error() string { 314 | return res.Text 315 | } 316 | 317 | func (res *Response) String() string { 318 | b, _ := json.Marshal(res) 319 | return string(b) 320 | } 321 | 322 | func (hec *Client) makeRequest(ctx context.Context, endpoint string, data []byte) (*Response, error) { 323 | retries := 0 324 | RETRY: 325 | var reader io.Reader 326 | if hec.compression == "gzip" { 327 | var buffer bytes.Buffer 328 | gzipWriter := gzip.NewWriter(&buffer) 329 | _, err := gzipWriter.Write(data) 330 | gzipWriter.Close() 331 | if err != nil { 332 | return nil, err 333 | } 334 | reader = &buffer 335 | } else { 336 | reader = bytes.NewReader(data) 337 | } 338 | 339 | req, err := http.NewRequest(http.MethodPost, hec.serverURL+endpoint, reader) 340 | if err != nil { 341 | return nil, err 342 | } 343 | req = req.WithContext(ctx) 344 | if hec.keepAlive { 345 | req.Header.Set("Connection", "keep-alive") 346 | } 347 | req.Header.Set("Authorization", "Splunk "+hec.token) 348 | if hec.compression == "gzip" { 349 | req.Header.Set("Content-Encoding", "gzip") 350 | } 351 | res, err := hec.httpClient.Do(req) 352 | if err != nil { 353 | return nil, err 354 | } 355 | 356 | body, err := ioutil.ReadAll(res.Body) 357 | res.Body.Close() 358 | if err != nil { 359 | return nil, err 360 | } 361 | 362 | response := responseFrom(body) 363 | 364 | if res.StatusCode != http.StatusOK { 365 | if retriable(response.Code) && retries < hec.retries { 366 | retries++ 367 | time.Sleep(retryWaitTime) 368 | goto RETRY 369 | } 370 | } 371 | 372 | return response, nil 373 | } 374 | 375 | func (hec *Client) write(ctx context.Context, endpoint string, data []byte) error { 376 | response, err := hec.makeRequest(ctx, endpoint, data) 377 | if err != nil { 378 | return err 379 | } 380 | 381 | // TODO: find out the correct code 382 | if response.Text != "Success" { 383 | return response 384 | } 385 | 386 | // Check for acknowledgement IDs and store them if provided 387 | if response.AckID != nil { 388 | hec.ackMux.Lock() 389 | defer hec.ackMux.Unlock() 390 | 391 | hec.ackIDs = append(hec.ackIDs, *response.AckID) 392 | } 393 | 394 | return nil 395 | } 396 | 397 | func rawHecEndpoint(channel string, metadata *EventMetadata) string { 398 | var buffer bytes.Buffer 399 | buffer.WriteString("/services/collector/raw?channel=" + channel) 400 | if metadata == nil { 401 | return buffer.String() 402 | } 403 | if metadata.Host != nil { 404 | buffer.WriteString("&host=" + *metadata.Host) 405 | } 406 | if metadata.Index != nil { 407 | buffer.WriteString("&index=" + *metadata.Index) 408 | } 409 | if metadata.Source != nil { 410 | buffer.WriteString("&source=" + *metadata.Source) 411 | } 412 | if metadata.SourceType != nil { 413 | buffer.WriteString("&sourcetype=" + *metadata.SourceType) 414 | } 415 | if metadata.Time != nil { 416 | buffer.WriteString("&time=" + epochTime(metadata.Time)) 417 | } 418 | return buffer.String() 419 | } 420 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | import ( 4 | "compress/gzip" 5 | "crypto/tls" 6 | "encoding/json" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | const ( 17 | testSplunkURL = "http://localhost:8088" 18 | testSplunkToken = "00000000-0000-0000-0000-000000000000" 19 | ) 20 | 21 | var testHttpClient *http.Client = &http.Client{ 22 | Transport: &http.Transport{ 23 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 24 | }, 25 | Timeout: 100 * time.Millisecond, 26 | } 27 | 28 | func jsonEndpoint(t *testing.T, compression string) http.Handler { 29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | failed := false 31 | input := make(map[string]interface{}) 32 | content := r.Body 33 | if compression == "gzip" { 34 | var err error 35 | content, err = gzip.NewReader(r.Body) 36 | if err != nil { 37 | t.Errorf("Unexpected error in gzip: %v", err) 38 | } 39 | header := r.Header.Get("Content-Encoding") 40 | if header != "gzip" { 41 | t.Errorf("Content-Encoding header wasn't sent for gzip") 42 | } 43 | } 44 | j := json.NewDecoder(content) 45 | err := j.Decode(&input) 46 | if err != nil { 47 | t.Errorf("Decoding JSON: %v", err) 48 | failed = true 49 | } 50 | 51 | requiredFields := []string{"event"} 52 | allowedFields := map[string]struct{}{ 53 | "channel": struct{}{}, 54 | "event": struct{}{}, 55 | "fields": struct{}{}, 56 | "host": struct{}{}, 57 | "index": struct{}{}, 58 | "source": struct{}{}, 59 | "sourcetype": struct{}{}, 60 | "time": struct{}{}, 61 | } 62 | for _, f := range requiredFields { 63 | if _, ok := input[f]; !ok { 64 | t.Errorf("Required field %q missing in %v", f, input) 65 | } 66 | } 67 | for f := range input { 68 | if _, ok := allowedFields[f]; !ok { 69 | t.Errorf("Unexpected field %q in %v", f, input) 70 | } 71 | } 72 | if failed { 73 | w.WriteHeader(400) 74 | w.Write([]byte(`{"text": "Error processing event", "code": 90}`)) 75 | } else { 76 | w.Write([]byte(`{"text":"Success","code":0}`)) 77 | } 78 | }) 79 | } 80 | 81 | func TestHEC_WriteEvent(t *testing.T) { 82 | for _, compression := range []string{"", "gzip"} { 83 | event := &Event{ 84 | Index: String("main"), 85 | Source: String("test-hec-raw"), 86 | SourceType: String("manual"), 87 | Host: String("localhost"), 88 | Time: String("1485237827.123"), 89 | Event: "hello, world", 90 | } 91 | 92 | ts := httptest.NewServer(jsonEndpoint(t, compression)) 93 | c := NewClient(ts.URL, testSplunkToken) 94 | c.SetHTTPClient(testHttpClient) 95 | c.SetCompression(compression) 96 | err := c.WriteEvent(event) 97 | assert.NoError(t, err) 98 | } 99 | } 100 | 101 | func TestHEC_WriteEventServerFailure(t *testing.T) { 102 | event := &Event{ 103 | Index: String("main"), 104 | Source: String("test-hec-raw"), 105 | SourceType: String("manual"), 106 | Host: String("localhost"), 107 | Time: String("1485237827.123"), 108 | Event: "hello, world", 109 | } 110 | 111 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 | w.WriteHeader(400) 113 | w.Write([]byte(`{"text":"Data channel is missing","code":10}`)) 114 | })) 115 | c := NewClient(ts.URL, testSplunkToken) 116 | c.SetHTTPClient(testHttpClient) 117 | err := c.WriteEvent(event) 118 | assert.Error(t, err) 119 | } 120 | 121 | func TestHEC_WriteObjectEvent(t *testing.T) { 122 | for _, compression := range []string{"", "gzip"} { 123 | event := &Event{ 124 | Index: String("main"), 125 | Source: String("test-hec-raw"), 126 | SourceType: String("manual"), 127 | Host: String("localhost"), 128 | Time: String("1485237827.123"), 129 | Event: map[string]interface{}{ 130 | "str": "hello", 131 | "time": time.Now(), 132 | }, 133 | } 134 | 135 | ts := httptest.NewServer(jsonEndpoint(t, compression)) 136 | c := NewClient(ts.URL, testSplunkToken) 137 | c.SetHTTPClient(testHttpClient) 138 | c.SetCompression(compression) 139 | err := c.WriteEvent(event) 140 | assert.NoError(t, err) 141 | } 142 | } 143 | 144 | func TestHEC_WriteLongEvent(t *testing.T) { 145 | event := &Event{ 146 | Index: String("main"), 147 | Source: String("test-hec-raw"), 148 | SourceType: String("manual"), 149 | Host: String("localhost"), 150 | Time: String("1485237827.123"), 151 | Event: "hello, world", 152 | } 153 | 154 | ts := httptest.NewServer(jsonEndpoint(t, "")) 155 | c := NewClient(ts.URL, testSplunkToken) 156 | 157 | c.SetHTTPClient(testHttpClient) 158 | c.SetMaxContentLength(20) // less than full event 159 | err := c.WriteEvent(event) 160 | assert.NotNil(t, err) 161 | assert.Contains(t, err.Error(), "too long") 162 | } 163 | 164 | func TestHEC_WriteEventBatch(t *testing.T) { 165 | for _, compression := range []string{"", "gzip"} { 166 | events := []*Event{ 167 | {Event: "event one"}, 168 | {Event: "event two"}, 169 | } 170 | 171 | ts := httptest.NewServer(jsonEndpoint(t, compression)) 172 | c := NewClient(ts.URL, testSplunkToken) 173 | 174 | c.SetHTTPClient(testHttpClient) 175 | c.SetCompression(compression) 176 | err := c.WriteBatch(events) 177 | assert.NoError(t, err) 178 | } 179 | } 180 | 181 | func TestHEC_WriteLongEventBatch(t *testing.T) { 182 | for _, compression := range []string{"", "gzip"} { 183 | events := []*Event{ 184 | {Event: "event one"}, 185 | {Event: "event two"}, 186 | } 187 | 188 | ts := httptest.NewServer(jsonEndpoint(t, compression)) 189 | c := NewClient(ts.URL, testSplunkToken) 190 | c.SetHTTPClient(testHttpClient) 191 | c.SetMaxContentLength(25) 192 | c.SetCompression(compression) 193 | err := c.WriteBatch(events) 194 | assert.NoError(t, err) 195 | } 196 | } 197 | 198 | func TestHEC_WriteEventRaw(t *testing.T) { 199 | for _, compression := range []string{"", "gzip"} { 200 | events := `2017-01-24T06:07:10.488Z Raw event one 201 | 2017-01-24T06:07:12.434Z Raw event two` 202 | metadata := EventMetadata{ 203 | Source: String("test-hec-raw"), 204 | } 205 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 206 | w.Write([]byte(`{"text":"Success","code":0}`)) 207 | })) 208 | c := NewClient(ts.URL, testSplunkToken) 209 | c.SetHTTPClient(testHttpClient) 210 | c.SetCompression(compression) 211 | err := c.WriteRaw(strings.NewReader(events), &metadata) 212 | assert.NoError(t, err) 213 | } 214 | } 215 | 216 | func TestHEC_WriteLongEventRaw(t *testing.T) { 217 | for _, compression := range []string{"", "gzip"} { 218 | events := `2017-01-24T06:07:10.488Z Raw event one 219 | 2017-01-24T06:07:12.434Z Raw event two` 220 | metadata := EventMetadata{ 221 | Source: String("test-hec-raw"), 222 | } 223 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 224 | w.Write([]byte(`{"text":"Success","code":0}`)) 225 | })) 226 | c := NewClient(ts.URL, testSplunkToken) 227 | c.SetMaxContentLength(40) 228 | c.SetHTTPClient(testHttpClient) 229 | c.SetCompression(compression) 230 | err := c.WriteRaw(strings.NewReader(events), &metadata) 231 | assert.NoError(t, err) 232 | } 233 | } 234 | 235 | func TestHEC_WriteRawFailure(t *testing.T) { 236 | events := `2017-01-24T06:07:10.488Z Raw event one 237 | 2017-01-24T06:07:12.434Z Raw event two` 238 | metadata := EventMetadata{ 239 | Source: String("test-hec-raw"), 240 | } 241 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 242 | w.WriteHeader(400) 243 | w.Write([]byte(`{"text":"Oh no","code":90}`)) 244 | })) 245 | c := NewClient(ts.URL, testSplunkToken) 246 | c.SetMaxContentLength(40) 247 | c.SetHTTPClient(testHttpClient) 248 | err := c.WriteRaw(strings.NewReader(events), &metadata) 249 | assert.Error(t, err) 250 | } 251 | 252 | func TestBreakStream(t *testing.T) { 253 | text := "This is line A\nThis is line B" // length of every line is 14 254 | 255 | getCountFunc := func(counter *int) func(chunk []byte) error { 256 | // returned function adds count of all character except "\n" 257 | return func(chunk []byte) error { 258 | for _, b := range chunk { 259 | if b != '\n' { 260 | *counter++ 261 | } 262 | } 263 | return nil 264 | } 265 | } 266 | 267 | for _, max := range []int{13, 14, 15, 28, 5, 30} { 268 | var counter int = 0 269 | err := breakStream(strings.NewReader(text), max, getCountFunc(&counter)) 270 | assert.NoError(t, err) 271 | assert.Equal(t, 28, counter) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /cluster.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | import ( 4 | "io" 5 | "math/rand" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type Cluster struct { 13 | HEC 14 | 15 | // Inner clients 16 | clients []*Client 17 | 18 | mtx sync.Mutex 19 | 20 | maxRetries int 21 | } 22 | 23 | func NewCluster(serverURLs []string, token string) HEC { 24 | id := uuid.New() 25 | 26 | channel := id.String() 27 | clients := make([]*Client, len(serverURLs)) 28 | for i, serverURL := range serverURLs { 29 | clients[i] = &Client{ 30 | httpClient: http.DefaultClient, 31 | serverURL: serverURL, 32 | token: token, 33 | keepAlive: true, 34 | channel: channel, 35 | retries: 0, // try only once for each client 36 | maxLength: defaultMaxContentLength, 37 | } 38 | } 39 | return &Cluster{ 40 | clients: clients, 41 | maxRetries: -1, // default: try all clients 42 | } 43 | } 44 | 45 | func (c *Cluster) SetHTTPClient(httpClient *http.Client) { 46 | c.mtx.Lock() 47 | for _, client := range c.clients { 48 | client.SetHTTPClient(httpClient) 49 | } 50 | c.mtx.Unlock() 51 | } 52 | 53 | func (c *Cluster) SetKeepAlive(enable bool) { 54 | c.mtx.Lock() 55 | for _, client := range c.clients { 56 | client.SetKeepAlive(enable) 57 | } 58 | c.mtx.Unlock() 59 | } 60 | 61 | func (c *Cluster) SetChannel(channel string) { 62 | c.mtx.Lock() 63 | for _, client := range c.clients { 64 | client.SetChannel(channel) 65 | } 66 | c.mtx.Unlock() 67 | } 68 | 69 | func (c *Cluster) SetMaxRetry(retries int) { 70 | c.maxRetries = retries 71 | } 72 | 73 | func (c *Cluster) SetMaxContentLength(size int) { 74 | c.mtx.Lock() 75 | for _, client := range c.clients { 76 | client.SetMaxContentLength(size) 77 | } 78 | c.mtx.Unlock() 79 | } 80 | 81 | func (c *Cluster) SetCompression(compression string) { 82 | c.mtx.Lock() 83 | for _, client := range c.clients { 84 | client.SetCompression(compression) 85 | } 86 | c.mtx.Unlock() 87 | } 88 | 89 | func (c *Cluster) WriteEvent(event *Event) error { 90 | return c.retry(func(client *Client) error { 91 | return client.WriteEvent(event) 92 | }) 93 | } 94 | 95 | func (c *Cluster) WriteBatch(events []*Event) error { 96 | return c.retry(func(client *Client) error { 97 | return client.WriteBatch(events) 98 | }) 99 | } 100 | 101 | func (c *Cluster) WriteRaw(reader io.ReadSeeker, metadata *EventMetadata) error { 102 | startAt, _ := reader.Seek(0, io.SeekCurrent) 103 | return c.retry(func(client *Client) error { 104 | reader.Seek(startAt, io.SeekStart) 105 | return client.WriteRaw(reader, metadata) 106 | }) 107 | } 108 | 109 | func (c *Cluster) retry(writeFunc func(*Client) error) error { 110 | exclude := make([]*Client, 0) 111 | var err error 112 | for t := 0; t < len(c.clients) && t != c.maxRetries; t++ { 113 | client := pick(c.clients, exclude) 114 | if err = writeFunc(client); err != nil { 115 | if err == ErrEventTooLong { 116 | return err 117 | } else if res, ok := err.(*Response); !ok || retriable(res.Code) { 118 | // If failed to write into this client, exclude it and try others 119 | exclude = append(exclude, client) 120 | continue 121 | } 122 | } else { 123 | return nil 124 | } 125 | } 126 | return err 127 | } 128 | 129 | func pick(clients []*Client, exclude []*Client) *Client { 130 | var choice *Client 131 | for choice == nil { 132 | choice = clients[rand.Int()%len(clients)] 133 | if exclude == nil { 134 | break 135 | } 136 | for _, bad := range exclude { 137 | if bad == choice { 138 | choice = nil 139 | break 140 | } 141 | } 142 | } 143 | return choice 144 | } 145 | -------------------------------------------------------------------------------- /cluster_test.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var ( 13 | testSplunkURLs = []string{"http://127.0.0.1:8088", "http://localhost:8088"} 14 | ) 15 | 16 | func TestCluster_WriteEvent(t *testing.T) { 17 | for _, compression := range []string{"", "gzip"} { 18 | event := &Event{ 19 | Index: String("main"), 20 | Source: String("test-hec-raw"), 21 | SourceType: String("manual"), 22 | Host: String("localhost"), 23 | Time: String("1485237827.123"), 24 | Event: String("hello, world"), 25 | } 26 | 27 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | w.Write([]byte(`{"text":"Success","code":0}`)) 29 | })) 30 | c := NewCluster([]string{ts.URL}, testSplunkToken) 31 | c.SetHTTPClient(testHttpClient) 32 | c.SetCompression(compression) 33 | err := c.WriteEvent(event) 34 | assert.NoError(t, err) 35 | } 36 | } 37 | 38 | func TestCluster_WriteEventBatch(t *testing.T) { 39 | for _, compression := range []string{"", "gzip"} { 40 | eventBatches := [][]*Event{ 41 | { 42 | {Event: "event one"}, 43 | {Event: "event two"}, 44 | }, 45 | { 46 | {Event: "event foo"}, 47 | {Event: "event bar"}, 48 | }, 49 | } 50 | 51 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | w.Write([]byte(`{"text":"Success","code":0}`)) 53 | })) 54 | c := NewCluster([]string{ts.URL}, testSplunkToken) 55 | c.SetHTTPClient(testHttpClient) 56 | c.SetCompression(compression) 57 | for _, batch := range eventBatches { 58 | err := c.WriteBatch(batch) 59 | assert.NoError(t, err) 60 | } 61 | } 62 | } 63 | 64 | func TestCluster_WriteEventRaw(t *testing.T) { 65 | for _, compression := range []string{"", "gzip"} { 66 | eventBlocks := []string{ 67 | `2017-01-24T06:07:10.488Z Raw event one 68 | 2017-01-24T06:07:12.434Z Raw event two`, 69 | `2017-01-24T06:07:10.488Z Raw event foo 70 | 2017-01-24T06:07:12.434Z Raw event bar`, 71 | } 72 | metadata := EventMetadata{ 73 | Source: String("test-hec-raw"), 74 | } 75 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 76 | w.Write([]byte(`{"text":"Success","code":0}`)) 77 | })) 78 | c := NewCluster([]string{ts.URL}, testSplunkToken) 79 | c.SetHTTPClient(testHttpClient) 80 | c.SetCompression(compression) 81 | for _, block := range eventBlocks { 82 | err := c.WriteRaw(strings.NewReader(block), &metadata) 83 | assert.NoError(t, err) 84 | } 85 | } 86 | } 87 | 88 | func TestCluster_Retrying(t *testing.T) { 89 | event := &Event{Event: "test retrying"} 90 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 91 | w.Write([]byte(`{"text":"Success","code":0}`)) 92 | })) 93 | partlyBrokenUrls := []string{ts.URL, "http://example.com:8088", "http://example.com:88"} 94 | c := NewCluster(partlyBrokenUrls, testSplunkToken) 95 | c.SetHTTPClient(testHttpClient) 96 | for i := 0; i < 5; i++ { 97 | err := c.WriteEvent(event) 98 | assert.NoError(t, err) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Response is response message from HEC. For example, `{"text":"Success","code":0}`. 8 | type Response struct { 9 | Text string `json:"text"` 10 | Code int `json:"code"` 11 | AckID *int `json:"ackId"` // Use a pointer so we can differentiate between a 0 and an ack ID not being specified 12 | Acks map[string]bool `json:"acks"` // Splunk returns ack IDs as strings rather than ints 13 | } 14 | 15 | // Response status codes 16 | const ( 17 | StatusSuccess = 0 18 | StatusTokenDisabled = 1 19 | StatusTokenRequired = 2 20 | StatusInvalidAuthorization = 3 21 | StatusInvalidToken = 4 22 | StatusNoData = 5 23 | StatusInvalidDataFormat = 6 24 | StatusIncorrectIndex = 7 25 | StatusInternalServerError = 8 26 | StatusServerBusy = 9 27 | StatusChannelMissing = 10 28 | StatusInvalidChannel = 11 29 | StatusEventFieldRequired = 12 30 | StatusEventFieldBlank = 13 31 | StatusAckDisabled = 14 32 | ) 33 | 34 | func retriable(code int) bool { 35 | return code == StatusServerBusy || code == StatusInternalServerError 36 | } 37 | 38 | var ErrEventTooLong = errors.New("Event length is too long") 39 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Event struct { 9 | Host *string `json:"host,omitempty"` 10 | Index *string `json:"index,omitempty"` 11 | Source *string `json:"source,omitempty"` 12 | SourceType *string `json:"sourcetype,omitempty"` 13 | Time *string `json:"time,omitempty"` 14 | Fields map[string]interface{} `json:"fields,omitempty"` 15 | Event interface{} `json:"event"` 16 | } 17 | 18 | func NewEvent(data interface{}) *Event { 19 | // Empty event is not allowed, but let HEC complain the error 20 | switch data.(type) { 21 | case *string: 22 | return &Event{Event: *data.(*string)} 23 | case string: 24 | return &Event{Event: data.(string)} 25 | default: 26 | return &Event{Event: data} 27 | } 28 | } 29 | 30 | func (e *Event) SetHost(host string) { 31 | e.Host = &host 32 | } 33 | 34 | func (e *Event) SetIndex(index string) { 35 | e.Index = &index 36 | } 37 | 38 | func (e *Event) SetSourceType(sourcetype string) { 39 | e.SourceType = &sourcetype 40 | } 41 | 42 | func (e *Event) SetSource(source string) { 43 | e.Source = &source 44 | } 45 | 46 | func (e *Event) SetTime(time time.Time) { 47 | e.Time = String(epochTime(&time)) 48 | } 49 | 50 | func (e *Event) SetFields(fields map[string]interface{}) { 51 | e.Fields = fields 52 | } 53 | 54 | func (e *Event) SetField(fieldName string, val interface{}) { 55 | if e.Fields == nil { 56 | e.Fields = make(map[string]interface{}) 57 | } 58 | 59 | e.Fields[fieldName] = val 60 | } 61 | 62 | func (e *Event) empty() bool { 63 | switch e.Event.(type) { 64 | case *string: 65 | return e.Event.(*string) == nil || *e.Event.(*string) == "" 66 | case string: 67 | return e.Event.(string) == "" 68 | default: 69 | return e.Event == nil 70 | } 71 | } 72 | 73 | func epochTime(t *time.Time) string { 74 | millis := t.UnixNano() / 1000000 75 | return fmt.Sprintf("%d.%03d", millis/1000, millis%1000) 76 | } 77 | 78 | func String(str string) *string { 79 | return &str 80 | } 81 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/fuyufjh/splunk-hec-go" 10 | ) 11 | 12 | func main() { 13 | client := hec.NewCluster( 14 | []string{"http://127.0.0.1:8088", "http://localhost:8088"}, 15 | "00000000-0000-0000-0000-000000000000", 16 | ) 17 | client.SetHTTPClient(&http.Client{Transport: &http.Transport{ 18 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 19 | }}) 20 | 21 | event1 := hec.NewEvent("event one") 22 | event1.SetTime(time.Now()) 23 | event2 := hec.NewEvent("event two") 24 | event2.SetTime(time.Now().Add(time.Minute)) 25 | 26 | err := client.WriteBatch([]*hec.Event{event1, event2}) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 58fb71ebba8d29536054a3813d631097b4c115ec0211022e5fd32a7833ad4b82 2 | updated: 2018-11-17T01:34:38.807844-03:00 3 | imports: 4 | - name: github.com/google/uuid 5 | version: d460ce9f8df2e77fb1ba55ca87fafed96c607494 6 | testImports: 7 | - name: github.com/davecgh/go-spew 8 | version: d8f796af33cc11cb798c1aaeb27a4ebc5099927d 9 | subpackages: 10 | - spew 11 | - name: github.com/pmezard/go-difflib 12 | version: 792786c7400a136282c1664665ae0a8db921c6c2 13 | subpackages: 14 | - difflib 15 | - name: github.com/stretchr/testify 16 | version: f35b8ab0b5a2cef36673838d662e249dd9c94686 17 | subpackages: 18 | - assert 19 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/fuyufjh/splunk-hec-go 2 | import: 3 | - package: github.com/google/uuid 4 | version: 1.0.0 5 | testImport: 6 | - package: github.com/stretchr/testify 7 | version: ^1.1.4 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fuyufjh/splunk-hec-go 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.0 // indirect 7 | github.com/google/uuid v1.0.0 8 | github.com/pmezard/go-difflib v1.0.0 // indirect 9 | github.com/stretchr/testify v1.7.0 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 4 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 9 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /hec.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type HEC interface { 10 | SetHTTPClient(client *http.Client) 11 | SetKeepAlive(enable bool) 12 | SetChannel(channel string) 13 | SetMaxRetry(retries int) 14 | SetMaxContentLength(size int) 15 | SetCompression(compression string) 16 | 17 | // WriteEvent writes single event via HEC json mode 18 | WriteEvent(event *Event) error 19 | 20 | // WriteBatch writes multiple events via HCE batch mode 21 | WriteBatch(events []*Event) error 22 | 23 | // WriteBatchWithContext writes multiple events via HEC batch mode with a context for cancellation 24 | WriteBatchWithContext(ctx context.Context, events []*Event) error 25 | 26 | // WriteRaw writes raw data stream via HEC raw mode 27 | WriteRaw(reader io.ReadSeeker, metadata *EventMetadata) error 28 | 29 | // WriteRawWithContext writes raw data stream via HEC raw mode with a context for cancellation 30 | WriteRawWithContext(ctx context.Context, reader io.ReadSeeker, metadata *EventMetadata) error 31 | 32 | // WaitForAcknowledgement blocks until the Splunk indexer acknowledges data sent to it 33 | WaitForAcknowledgement() error 34 | 35 | // WaitForAcknowledgementWithContext blocks until the Splunk indexer acknowledges data sent to it with a context for cancellation 36 | WaitForAcknowledgementWithContext(ctx context.Context) error 37 | } 38 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package hec 2 | 3 | func remove(l []int, item int) []int { 4 | for i, other := range l { 5 | if other == item { 6 | return append(l[:i], l[i+1:]...) 7 | } 8 | } 9 | 10 | return l 11 | } 12 | --------------------------------------------------------------------------------