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