github.com/etecs-ru/gnomock@v0.13.2/preset/splunk/init.go (about) 1 package splunk 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "net/url" 13 "os" 14 "strconv" 15 "time" 16 17 "github.com/etecs-ru/gnomock" 18 ) 19 20 var errConflict = fmt.Errorf("409: conflict") 21 22 // Event is a type used during Splunk initialization. Pass events to WithValues 23 // to ingest them into the container before the control over it is passed to 24 // the caller 25 type Event struct { 26 // Event is the actual log entry. Can be any format 27 Event string `json:"event"` 28 29 // Index is the name of index to ingest the log into. If the index does not 30 // exist, it will be created 31 Index string `json:"index"` 32 33 // Source will be used as "source" value of this event in Splunk 34 Source string `json:"source"` 35 36 // SourceType will be used as "sourcetype" value of this event in Splunk 37 SourceType string `json:"sourcetype"` 38 39 // Time represents event timestamp in seconds, milliseconds or nanoseconds 40 // (and maybe even in microseconds, whatever splunk recognizes) 41 Time int64 `json:"time"` 42 } 43 44 func (p *P) initf() gnomock.InitFunc { 45 return func(ctx context.Context, c *gnomock.Container) (err error) { 46 if p.ValuesFile != "" { 47 f, err := os.Open(p.ValuesFile) 48 if err != nil { 49 return fmt.Errorf("can't open values file '%s': %w", p.ValuesFile, err) 50 } 51 52 defer func() { 53 closeErr := f.Close() 54 if err == nil && closeErr != nil { 55 err = closeErr 56 } 57 }() 58 59 events := make([]Event, 0) 60 decoder := json.NewDecoder(f) 61 62 for { 63 var e Event 64 65 err = decoder.Decode(&e) 66 if errors.Is(err, io.EOF) { 67 break 68 } 69 70 if err != nil { 71 return fmt.Errorf("can't read initial event: %w", err) 72 } 73 74 events = append(events, e) 75 } 76 77 p.Values = append(events, p.Values...) 78 } 79 80 err = Ingest(ctx, c, p.AdminPassword, p.Values...) 81 if err != nil { 82 return fmt.Errorf("can't ingest events: %w", err) 83 } 84 85 return nil 86 } 87 } 88 89 // Ingest adds the provided events to splunk container. Use the same password 90 // you provided in WithPassword. Send as many events as you like, this function 91 // only returns when all the events were indexed, or when the context is timed 92 // out 93 func Ingest(ctx context.Context, c *gnomock.Container, password string, events ...Event) error { 94 postForm := requestWithPassword(http.MethodPost, password, false) 95 apiAddr := c.Address(APIPort) 96 97 token, err := issueToken(postForm, apiAddr) 98 if err != nil { 99 return fmt.Errorf("can't issue new HEC token: %w", err) 100 } 101 102 ensureIndex := indexRegistry(postForm, apiAddr) 103 ingestEvent := eventForwarder( 104 requestWithPassword(http.MethodPost, token, true), 105 c.Address(CollectorPort), 106 ) 107 108 initialCount, err := countEvents(postForm, apiAddr) 109 if err != nil { 110 return fmt.Errorf("can't get initial event count: %w", err) 111 } 112 113 for _, e := range events { 114 select { 115 case <-ctx.Done(): 116 return context.Canceled 117 default: 118 err := ensureIndex(e.Index) 119 if err != nil { 120 return err 121 } 122 123 err = ingestEvent(e) 124 if err != nil { 125 return err 126 } 127 } 128 } 129 130 var ( 131 lastErr error 132 lastCount int 133 expectedCount int = initialCount + len(events) 134 ) 135 136 for { 137 select { 138 case <-ctx.Done(): 139 return fmt.Errorf("event count didn't match: want %d, got %d; last error: %v: %w", 140 len(events), lastCount, lastErr, context.Canceled) 141 default: 142 lastCount, lastErr = countEvents(postForm, apiAddr) 143 if lastErr == nil && lastCount == expectedCount { 144 return nil 145 } 146 147 time.Sleep(time.Millisecond * 250) 148 } 149 } 150 } 151 152 func issueToken(post postFunc, addr string) (string, error) { 153 newTokenURL := fmt.Sprintf("https://%s/services/data/inputs/http?output_mode=json", addr) 154 tokenName := fmt.Sprintf("gnomock-%d", time.Now().UnixNano()) 155 156 data := url.Values{} 157 data.Set("name", tokenName) 158 buf := bytes.NewBufferString(data.Encode()) 159 160 bs, err := post(newTokenURL, buf) 161 if err != nil { 162 return "", fmt.Errorf("can't create new HEC token: %w", err) 163 } 164 165 r := splunkTokenResponse{} 166 167 err = json.Unmarshal(bs, &r) 168 if err != nil { 169 return "", fmt.Errorf("can't unmarshal HEC token: %w", err) 170 } 171 172 return r.Entry[0].Content.Token, nil 173 } 174 175 func indexRegistry(post postFunc, addr string) func(string) error { 176 indexes := map[string]bool{"main": true} 177 uri := fmt.Sprintf("https://%s/services/data/indexes?output_mode=json", addr) 178 179 return func(indexName string) error { 180 if _, ok := indexes[indexName]; !ok { 181 _, err := post(uri, bytes.NewBufferString("name="+indexName)) 182 if err != nil && !errors.Is(err, errConflict) { 183 return fmt.Errorf("can't create index: %w", err) 184 } 185 186 indexes[indexName] = true 187 } 188 189 return nil 190 } 191 } 192 193 func eventForwarder(post postFunc, addr string) func(Event) error { 194 return func(e Event) error { 195 uri := fmt.Sprintf("https://%s/services/collector?output_mode=json", addr) 196 197 eventBytes, err := json.Marshal(e) 198 if err != nil { 199 return fmt.Errorf("can't marshal event to json: %w", err) 200 } 201 202 _, err = post(uri, bytes.NewBuffer(eventBytes)) 203 if err != nil { 204 return err 205 } 206 207 return nil 208 } 209 } 210 211 func countEvents(post postFunc, addr string) (int, error) { 212 uri := fmt.Sprintf("https://%s/services/search/jobs/export", addr) 213 data := url.Values{} 214 data.Add("search", "search index=* | stats count") 215 data.Add("output_mode", "json") 216 217 bs, err := post(uri, bytes.NewBufferString(data.Encode())) 218 if err != nil { 219 return 0, err 220 } 221 222 var response splunkSearchcResponse 223 224 err = json.Unmarshal(bs, &response) 225 if err != nil { 226 return 0, err 227 } 228 229 countStr, ok := response.Result["count"] 230 if !ok { 231 return 0, err 232 } 233 234 count, err := strconv.Atoi(fmt.Sprintf("%s", countStr)) 235 if err != nil { 236 return 0, err 237 } 238 239 return count, nil 240 } 241 242 type postFunc func(string, *bytes.Buffer) ([]byte, error) 243 244 func requestWithPassword(method, password string, isJSON bool) postFunc { 245 client := insecureClient() 246 247 return func(uri string, buf *bytes.Buffer) (bs []byte, err error) { 248 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 249 defer cancel() 250 251 req, err := http.NewRequestWithContext(ctx, method, uri, buf) 252 if err != nil { 253 return nil, fmt.Errorf("can't create request: %w", err) 254 } 255 256 req.SetBasicAuth("admin", password) 257 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 258 259 if isJSON { 260 req.Header.Set("Content-Type", "application/json") 261 } 262 263 resp, err := client.Do(req) 264 if err != nil { 265 return nil, fmt.Errorf("request failed: %w", err) 266 } 267 268 defer func() { 269 closeErr := resp.Body.Close() 270 if err == nil && closeErr != nil { 271 err = fmt.Errorf("can't close response body: %w", closeErr) 272 } 273 }() 274 275 bs, err = ioutil.ReadAll(resp.Body) 276 if err != nil { 277 return nil, fmt.Errorf("can't read response body: %w", err) 278 } 279 280 if resp.StatusCode == http.StatusConflict { 281 return nil, errConflict 282 } 283 284 if resp.StatusCode >= http.StatusBadRequest { 285 return nil, errors.New(resp.Status + ": " + string(bs)) 286 } 287 288 return bs, nil 289 } 290 } 291 292 type splunkTokenResponse struct { 293 Entry []struct { 294 Content struct { 295 Token string `json:"token"` 296 } `json:"content"` 297 } `json:"entry"` 298 } 299 300 type splunkSearchcResponse struct { 301 Result map[string]interface{} `json:"result"` 302 }