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