github.com/rhatdan/docker@v0.7.7-0.20180119204836-47a0dcbcd20a/daemon/logger/splunk/splunk.go (about) 1 // Package splunk provides the log driver for forwarding server logs to 2 // Splunk HTTP Event Collector endpoint. 3 package splunk 4 5 import ( 6 "bytes" 7 "compress/gzip" 8 "context" 9 "crypto/tls" 10 "crypto/x509" 11 "encoding/json" 12 "fmt" 13 "io" 14 "io/ioutil" 15 "net/http" 16 "net/url" 17 "os" 18 "strconv" 19 "strings" 20 "sync" 21 "time" 22 23 "github.com/docker/docker/daemon/logger" 24 "github.com/docker/docker/daemon/logger/loggerutils" 25 "github.com/docker/docker/pkg/pools" 26 "github.com/docker/docker/pkg/urlutil" 27 "github.com/sirupsen/logrus" 28 ) 29 30 const ( 31 driverName = "splunk" 32 splunkURLKey = "splunk-url" 33 splunkTokenKey = "splunk-token" 34 splunkSourceKey = "splunk-source" 35 splunkSourceTypeKey = "splunk-sourcetype" 36 splunkIndexKey = "splunk-index" 37 splunkCAPathKey = "splunk-capath" 38 splunkCANameKey = "splunk-caname" 39 splunkInsecureSkipVerifyKey = "splunk-insecureskipverify" 40 splunkFormatKey = "splunk-format" 41 splunkVerifyConnectionKey = "splunk-verify-connection" 42 splunkGzipCompressionKey = "splunk-gzip" 43 splunkGzipCompressionLevelKey = "splunk-gzip-level" 44 envKey = "env" 45 envRegexKey = "env-regex" 46 labelsKey = "labels" 47 tagKey = "tag" 48 ) 49 50 const ( 51 // How often do we send messages (if we are not reaching batch size) 52 defaultPostMessagesFrequency = 5 * time.Second 53 // How big can be batch of messages 54 defaultPostMessagesBatchSize = 1000 55 // Maximum number of messages we can store in buffer 56 defaultBufferMaximum = 10 * defaultPostMessagesBatchSize 57 // Number of messages allowed to be queued in the channel 58 defaultStreamChannelSize = 4 * defaultPostMessagesBatchSize 59 // maxResponseSize is the max amount that will be read from an http response 60 maxResponseSize = 1024 61 ) 62 63 const ( 64 envVarPostMessagesFrequency = "SPLUNK_LOGGING_DRIVER_POST_MESSAGES_FREQUENCY" 65 envVarPostMessagesBatchSize = "SPLUNK_LOGGING_DRIVER_POST_MESSAGES_BATCH_SIZE" 66 envVarBufferMaximum = "SPLUNK_LOGGING_DRIVER_BUFFER_MAX" 67 envVarStreamChannelSize = "SPLUNK_LOGGING_DRIVER_CHANNEL_SIZE" 68 ) 69 70 var batchSendTimeout = 30 * time.Second 71 72 type splunkLoggerInterface interface { 73 logger.Logger 74 worker() 75 } 76 77 type splunkLogger struct { 78 client *http.Client 79 transport *http.Transport 80 81 url string 82 auth string 83 nullMessage *splunkMessage 84 85 // http compression 86 gzipCompression bool 87 gzipCompressionLevel int 88 89 // Advanced options 90 postMessagesFrequency time.Duration 91 postMessagesBatchSize int 92 bufferMaximum int 93 94 // For synchronization between background worker and logger. 95 // We use channel to send messages to worker go routine. 96 // All other variables for blocking Close call before we flush all messages to HEC 97 stream chan *splunkMessage 98 lock sync.RWMutex 99 closed bool 100 closedCond *sync.Cond 101 } 102 103 type splunkLoggerInline struct { 104 *splunkLogger 105 106 nullEvent *splunkMessageEvent 107 } 108 109 type splunkLoggerJSON struct { 110 *splunkLoggerInline 111 } 112 113 type splunkLoggerRaw struct { 114 *splunkLogger 115 116 prefix []byte 117 } 118 119 type splunkMessage struct { 120 Event interface{} `json:"event"` 121 Time string `json:"time"` 122 Host string `json:"host"` 123 Source string `json:"source,omitempty"` 124 SourceType string `json:"sourcetype,omitempty"` 125 Index string `json:"index,omitempty"` 126 } 127 128 type splunkMessageEvent struct { 129 Line interface{} `json:"line"` 130 Source string `json:"source"` 131 Tag string `json:"tag,omitempty"` 132 Attrs map[string]string `json:"attrs,omitempty"` 133 } 134 135 const ( 136 splunkFormatRaw = "raw" 137 splunkFormatJSON = "json" 138 splunkFormatInline = "inline" 139 ) 140 141 func init() { 142 if err := logger.RegisterLogDriver(driverName, New); err != nil { 143 logrus.Fatal(err) 144 } 145 if err := logger.RegisterLogOptValidator(driverName, ValidateLogOpt); err != nil { 146 logrus.Fatal(err) 147 } 148 } 149 150 // New creates splunk logger driver using configuration passed in context 151 func New(info logger.Info) (logger.Logger, error) { 152 hostname, err := info.Hostname() 153 if err != nil { 154 return nil, fmt.Errorf("%s: cannot access hostname to set source field", driverName) 155 } 156 157 // Parse and validate Splunk URL 158 splunkURL, err := parseURL(info) 159 if err != nil { 160 return nil, err 161 } 162 163 // Splunk Token is required parameter 164 splunkToken, ok := info.Config[splunkTokenKey] 165 if !ok { 166 return nil, fmt.Errorf("%s: %s is expected", driverName, splunkTokenKey) 167 } 168 169 tlsConfig := &tls.Config{} 170 171 // Splunk is using autogenerated certificates by default, 172 // allow users to trust them with skipping verification 173 if insecureSkipVerifyStr, ok := info.Config[splunkInsecureSkipVerifyKey]; ok { 174 insecureSkipVerify, err := strconv.ParseBool(insecureSkipVerifyStr) 175 if err != nil { 176 return nil, err 177 } 178 tlsConfig.InsecureSkipVerify = insecureSkipVerify 179 } 180 181 // If path to the root certificate is provided - load it 182 if caPath, ok := info.Config[splunkCAPathKey]; ok { 183 caCert, err := ioutil.ReadFile(caPath) 184 if err != nil { 185 return nil, err 186 } 187 caPool := x509.NewCertPool() 188 caPool.AppendCertsFromPEM(caCert) 189 tlsConfig.RootCAs = caPool 190 } 191 192 if caName, ok := info.Config[splunkCANameKey]; ok { 193 tlsConfig.ServerName = caName 194 } 195 196 gzipCompression := false 197 if gzipCompressionStr, ok := info.Config[splunkGzipCompressionKey]; ok { 198 gzipCompression, err = strconv.ParseBool(gzipCompressionStr) 199 if err != nil { 200 return nil, err 201 } 202 } 203 204 gzipCompressionLevel := gzip.DefaultCompression 205 if gzipCompressionLevelStr, ok := info.Config[splunkGzipCompressionLevelKey]; ok { 206 var err error 207 gzipCompressionLevel64, err := strconv.ParseInt(gzipCompressionLevelStr, 10, 32) 208 if err != nil { 209 return nil, err 210 } 211 gzipCompressionLevel = int(gzipCompressionLevel64) 212 if gzipCompressionLevel < gzip.DefaultCompression || gzipCompressionLevel > gzip.BestCompression { 213 err := fmt.Errorf("not supported level '%s' for %s (supported values between %d and %d)", 214 gzipCompressionLevelStr, splunkGzipCompressionLevelKey, gzip.DefaultCompression, gzip.BestCompression) 215 return nil, err 216 } 217 } 218 219 transport := &http.Transport{ 220 TLSClientConfig: tlsConfig, 221 } 222 client := &http.Client{ 223 Transport: transport, 224 } 225 226 source := info.Config[splunkSourceKey] 227 sourceType := info.Config[splunkSourceTypeKey] 228 index := info.Config[splunkIndexKey] 229 230 var nullMessage = &splunkMessage{ 231 Host: hostname, 232 Source: source, 233 SourceType: sourceType, 234 Index: index, 235 } 236 237 // Allow user to remove tag from the messages by setting tag to empty string 238 tag := "" 239 if tagTemplate, ok := info.Config[tagKey]; !ok || tagTemplate != "" { 240 tag, err = loggerutils.ParseLogTag(info, loggerutils.DefaultTemplate) 241 if err != nil { 242 return nil, err 243 } 244 } 245 246 attrs, err := info.ExtraAttributes(nil) 247 if err != nil { 248 return nil, err 249 } 250 251 var ( 252 postMessagesFrequency = getAdvancedOptionDuration(envVarPostMessagesFrequency, defaultPostMessagesFrequency) 253 postMessagesBatchSize = getAdvancedOptionInt(envVarPostMessagesBatchSize, defaultPostMessagesBatchSize) 254 bufferMaximum = getAdvancedOptionInt(envVarBufferMaximum, defaultBufferMaximum) 255 streamChannelSize = getAdvancedOptionInt(envVarStreamChannelSize, defaultStreamChannelSize) 256 ) 257 258 logger := &splunkLogger{ 259 client: client, 260 transport: transport, 261 url: splunkURL.String(), 262 auth: "Splunk " + splunkToken, 263 nullMessage: nullMessage, 264 gzipCompression: gzipCompression, 265 gzipCompressionLevel: gzipCompressionLevel, 266 stream: make(chan *splunkMessage, streamChannelSize), 267 postMessagesFrequency: postMessagesFrequency, 268 postMessagesBatchSize: postMessagesBatchSize, 269 bufferMaximum: bufferMaximum, 270 } 271 272 // By default we verify connection, but we allow use to skip that 273 verifyConnection := true 274 if verifyConnectionStr, ok := info.Config[splunkVerifyConnectionKey]; ok { 275 var err error 276 verifyConnection, err = strconv.ParseBool(verifyConnectionStr) 277 if err != nil { 278 return nil, err 279 } 280 } 281 if verifyConnection { 282 err = verifySplunkConnection(logger) 283 if err != nil { 284 return nil, err 285 } 286 } 287 288 var splunkFormat string 289 if splunkFormatParsed, ok := info.Config[splunkFormatKey]; ok { 290 switch splunkFormatParsed { 291 case splunkFormatInline: 292 case splunkFormatJSON: 293 case splunkFormatRaw: 294 default: 295 return nil, fmt.Errorf("Unknown format specified %s, supported formats are inline, json and raw", splunkFormat) 296 } 297 splunkFormat = splunkFormatParsed 298 } else { 299 splunkFormat = splunkFormatInline 300 } 301 302 var loggerWrapper splunkLoggerInterface 303 304 switch splunkFormat { 305 case splunkFormatInline: 306 nullEvent := &splunkMessageEvent{ 307 Tag: tag, 308 Attrs: attrs, 309 } 310 311 loggerWrapper = &splunkLoggerInline{logger, nullEvent} 312 case splunkFormatJSON: 313 nullEvent := &splunkMessageEvent{ 314 Tag: tag, 315 Attrs: attrs, 316 } 317 318 loggerWrapper = &splunkLoggerJSON{&splunkLoggerInline{logger, nullEvent}} 319 case splunkFormatRaw: 320 var prefix bytes.Buffer 321 if tag != "" { 322 prefix.WriteString(tag) 323 prefix.WriteString(" ") 324 } 325 for key, value := range attrs { 326 prefix.WriteString(key) 327 prefix.WriteString("=") 328 prefix.WriteString(value) 329 prefix.WriteString(" ") 330 } 331 332 loggerWrapper = &splunkLoggerRaw{logger, prefix.Bytes()} 333 default: 334 return nil, fmt.Errorf("Unexpected format %s", splunkFormat) 335 } 336 337 go loggerWrapper.worker() 338 339 return loggerWrapper, nil 340 } 341 342 func (l *splunkLoggerInline) Log(msg *logger.Message) error { 343 message := l.createSplunkMessage(msg) 344 345 event := *l.nullEvent 346 event.Line = string(msg.Line) 347 event.Source = msg.Source 348 349 message.Event = &event 350 logger.PutMessage(msg) 351 return l.queueMessageAsync(message) 352 } 353 354 func (l *splunkLoggerJSON) Log(msg *logger.Message) error { 355 message := l.createSplunkMessage(msg) 356 event := *l.nullEvent 357 358 var rawJSONMessage json.RawMessage 359 if err := json.Unmarshal(msg.Line, &rawJSONMessage); err == nil { 360 event.Line = &rawJSONMessage 361 } else { 362 event.Line = string(msg.Line) 363 } 364 365 event.Source = msg.Source 366 367 message.Event = &event 368 logger.PutMessage(msg) 369 return l.queueMessageAsync(message) 370 } 371 372 func (l *splunkLoggerRaw) Log(msg *logger.Message) error { 373 // empty or whitespace-only messages are not accepted by HEC 374 if strings.TrimSpace(string(msg.Line)) == "" { 375 return nil 376 } 377 378 message := l.createSplunkMessage(msg) 379 380 message.Event = string(append(l.prefix, msg.Line...)) 381 logger.PutMessage(msg) 382 return l.queueMessageAsync(message) 383 } 384 385 func (l *splunkLogger) queueMessageAsync(message *splunkMessage) error { 386 l.lock.RLock() 387 defer l.lock.RUnlock() 388 if l.closedCond != nil { 389 return fmt.Errorf("%s: driver is closed", driverName) 390 } 391 l.stream <- message 392 return nil 393 } 394 395 func (l *splunkLogger) worker() { 396 timer := time.NewTicker(l.postMessagesFrequency) 397 var messages []*splunkMessage 398 for { 399 select { 400 case message, open := <-l.stream: 401 if !open { 402 l.postMessages(messages, true) 403 l.lock.Lock() 404 defer l.lock.Unlock() 405 l.transport.CloseIdleConnections() 406 l.closed = true 407 l.closedCond.Signal() 408 return 409 } 410 messages = append(messages, message) 411 // Only sending when we get exactly to the batch size, 412 // This also helps not to fire postMessages on every new message, 413 // when previous try failed. 414 if len(messages)%l.postMessagesBatchSize == 0 { 415 messages = l.postMessages(messages, false) 416 } 417 case <-timer.C: 418 messages = l.postMessages(messages, false) 419 } 420 } 421 } 422 423 func (l *splunkLogger) postMessages(messages []*splunkMessage, lastChance bool) []*splunkMessage { 424 messagesLen := len(messages) 425 426 ctx, cancel := context.WithTimeout(context.Background(), batchSendTimeout) 427 defer cancel() 428 429 for i := 0; i < messagesLen; i += l.postMessagesBatchSize { 430 upperBound := i + l.postMessagesBatchSize 431 if upperBound > messagesLen { 432 upperBound = messagesLen 433 } 434 435 if err := l.tryPostMessages(ctx, messages[i:upperBound]); err != nil { 436 logrus.WithError(err).WithField("module", "logger/splunk").Warn("Error while sending logs") 437 if messagesLen-i >= l.bufferMaximum || lastChance { 438 // If this is last chance - print them all to the daemon log 439 if lastChance { 440 upperBound = messagesLen 441 } 442 // Not all sent, but buffer has got to its maximum, let's log all messages 443 // we could not send and return buffer minus one batch size 444 for j := i; j < upperBound; j++ { 445 if jsonEvent, err := json.Marshal(messages[j]); err != nil { 446 logrus.Error(err) 447 } else { 448 logrus.Error(fmt.Errorf("Failed to send a message '%s'", string(jsonEvent))) 449 } 450 } 451 return messages[upperBound:messagesLen] 452 } 453 // Not all sent, returning buffer from where we have not sent messages 454 return messages[i:messagesLen] 455 } 456 } 457 // All sent, return empty buffer 458 return messages[:0] 459 } 460 461 func (l *splunkLogger) tryPostMessages(ctx context.Context, messages []*splunkMessage) error { 462 if len(messages) == 0 { 463 return nil 464 } 465 var buffer bytes.Buffer 466 var writer io.Writer 467 var gzipWriter *gzip.Writer 468 var err error 469 // If gzip compression is enabled - create gzip writer with specified compression 470 // level. If gzip compression is disabled, use standard buffer as a writer 471 if l.gzipCompression { 472 gzipWriter, err = gzip.NewWriterLevel(&buffer, l.gzipCompressionLevel) 473 if err != nil { 474 return err 475 } 476 writer = gzipWriter 477 } else { 478 writer = &buffer 479 } 480 for _, message := range messages { 481 jsonEvent, err := json.Marshal(message) 482 if err != nil { 483 return err 484 } 485 if _, err := writer.Write(jsonEvent); err != nil { 486 return err 487 } 488 } 489 // If gzip compression is enabled, tell it, that we are done 490 if l.gzipCompression { 491 err = gzipWriter.Close() 492 if err != nil { 493 return err 494 } 495 } 496 req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(buffer.Bytes())) 497 if err != nil { 498 return err 499 } 500 req = req.WithContext(ctx) 501 req.Header.Set("Authorization", l.auth) 502 // Tell if we are sending gzip compressed body 503 if l.gzipCompression { 504 req.Header.Set("Content-Encoding", "gzip") 505 } 506 resp, err := l.client.Do(req) 507 if err != nil { 508 return err 509 } 510 defer func() { 511 pools.Copy(ioutil.Discard, resp.Body) 512 resp.Body.Close() 513 }() 514 if resp.StatusCode != http.StatusOK { 515 rdr := io.LimitReader(resp.Body, maxResponseSize) 516 body, err := ioutil.ReadAll(rdr) 517 if err != nil { 518 return err 519 } 520 return fmt.Errorf("%s: failed to send event - %s - %s", driverName, resp.Status, string(body)) 521 } 522 return nil 523 } 524 525 func (l *splunkLogger) Close() error { 526 l.lock.Lock() 527 defer l.lock.Unlock() 528 if l.closedCond == nil { 529 l.closedCond = sync.NewCond(&l.lock) 530 close(l.stream) 531 for !l.closed { 532 l.closedCond.Wait() 533 } 534 } 535 return nil 536 } 537 538 func (l *splunkLogger) Name() string { 539 return driverName 540 } 541 542 func (l *splunkLogger) createSplunkMessage(msg *logger.Message) *splunkMessage { 543 message := *l.nullMessage 544 message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/float64(time.Second)) 545 return &message 546 } 547 548 // ValidateLogOpt looks for all supported by splunk driver options 549 func ValidateLogOpt(cfg map[string]string) error { 550 for key := range cfg { 551 switch key { 552 case splunkURLKey: 553 case splunkTokenKey: 554 case splunkSourceKey: 555 case splunkSourceTypeKey: 556 case splunkIndexKey: 557 case splunkCAPathKey: 558 case splunkCANameKey: 559 case splunkInsecureSkipVerifyKey: 560 case splunkFormatKey: 561 case splunkVerifyConnectionKey: 562 case splunkGzipCompressionKey: 563 case splunkGzipCompressionLevelKey: 564 case envKey: 565 case envRegexKey: 566 case labelsKey: 567 case tagKey: 568 default: 569 return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName) 570 } 571 } 572 return nil 573 } 574 575 func parseURL(info logger.Info) (*url.URL, error) { 576 splunkURLStr, ok := info.Config[splunkURLKey] 577 if !ok { 578 return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey) 579 } 580 581 splunkURL, err := url.Parse(splunkURLStr) 582 if err != nil { 583 return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey) 584 } 585 586 if !urlutil.IsURL(splunkURLStr) || 587 !splunkURL.IsAbs() || 588 (splunkURL.Path != "" && splunkURL.Path != "/") || 589 splunkURL.RawQuery != "" || 590 splunkURL.Fragment != "" { 591 return nil, fmt.Errorf("%s: expected format scheme://dns_name_or_ip:port for %s", driverName, splunkURLKey) 592 } 593 594 splunkURL.Path = "/services/collector/event/1.0" 595 596 return splunkURL, nil 597 } 598 599 func verifySplunkConnection(l *splunkLogger) error { 600 req, err := http.NewRequest(http.MethodOptions, l.url, nil) 601 if err != nil { 602 return err 603 } 604 resp, err := l.client.Do(req) 605 if err != nil { 606 return err 607 } 608 defer func() { 609 pools.Copy(ioutil.Discard, resp.Body) 610 resp.Body.Close() 611 }() 612 613 if resp.StatusCode != http.StatusOK { 614 rdr := io.LimitReader(resp.Body, maxResponseSize) 615 body, err := ioutil.ReadAll(rdr) 616 if err != nil { 617 return err 618 } 619 return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, resp.Status, string(body)) 620 } 621 return nil 622 } 623 624 func getAdvancedOptionDuration(envName string, defaultValue time.Duration) time.Duration { 625 valueStr := os.Getenv(envName) 626 if valueStr == "" { 627 return defaultValue 628 } 629 parsedValue, err := time.ParseDuration(valueStr) 630 if err != nil { 631 logrus.Error(fmt.Sprintf("Failed to parse value of %s as duration. Using default %v. %v", envName, defaultValue, err)) 632 return defaultValue 633 } 634 return parsedValue 635 } 636 637 func getAdvancedOptionInt(envName string, defaultValue int) int { 638 valueStr := os.Getenv(envName) 639 if valueStr == "" { 640 return defaultValue 641 } 642 parsedValue, err := strconv.ParseInt(valueStr, 10, 32) 643 if err != nil { 644 logrus.Error(fmt.Sprintf("Failed to parse value of %s as integer. Using default %d. %v", envName, defaultValue, err)) 645 return defaultValue 646 } 647 return int(parsedValue) 648 }