mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	New driver options: - `splunk-gzip` - gzip compress all requests to Splunk HEC (enabled by default) - `splunk-gzip-level` - change compression level. Messages are sent in batches by 1000, with frequency of 5 seconds. Maximum buffer is 10,000 events. If HEC will not be available, Splunk Logging Driver will keep retrying while it can hold messages in buffer. Added unit tests for driver. Signed-off-by: Denis Gladkikh <denis@gladkikh.email>
		
			
				
	
	
		
			621 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			621 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Package splunk provides the log driver for forwarding server logs to
 | 
						|
// Splunk HTTP Event Collector endpoint.
 | 
						|
package splunk
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"compress/gzip"
 | 
						|
	"crypto/tls"
 | 
						|
	"crypto/x509"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"io/ioutil"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"strconv"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/Sirupsen/logrus"
 | 
						|
	"github.com/docker/docker/daemon/logger"
 | 
						|
	"github.com/docker/docker/daemon/logger/loggerutils"
 | 
						|
	"github.com/docker/docker/pkg/urlutil"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	driverName                    = "splunk"
 | 
						|
	splunkURLKey                  = "splunk-url"
 | 
						|
	splunkTokenKey                = "splunk-token"
 | 
						|
	splunkSourceKey               = "splunk-source"
 | 
						|
	splunkSourceTypeKey           = "splunk-sourcetype"
 | 
						|
	splunkIndexKey                = "splunk-index"
 | 
						|
	splunkCAPathKey               = "splunk-capath"
 | 
						|
	splunkCANameKey               = "splunk-caname"
 | 
						|
	splunkInsecureSkipVerifyKey   = "splunk-insecureskipverify"
 | 
						|
	splunkFormatKey               = "splunk-format"
 | 
						|
	splunkVerifyConnectionKey     = "splunk-verify-connection"
 | 
						|
	splunkGzipCompressionKey      = "splunk-gzip"
 | 
						|
	splunkGzipCompressionLevelKey = "splunk-gzip-level"
 | 
						|
	envKey                        = "env"
 | 
						|
	labelsKey                     = "labels"
 | 
						|
	tagKey                        = "tag"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	// How often do we send messages (if we are not reaching batch size)
 | 
						|
	defaultPostMessagesFrequency = 5 * time.Second
 | 
						|
	// How big can be batch of messages
 | 
						|
	defaultPostMessagesBatchSize = 1000
 | 
						|
	// Maximum number of messages we can store in buffer
 | 
						|
	defaultBufferMaximum = 10 * defaultPostMessagesBatchSize
 | 
						|
	// Number of messages allowed to be queued in the channel
 | 
						|
	defaultStreamChannelSize = 4 * defaultPostMessagesBatchSize
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	envVarPostMessagesFrequency = "SPLUNK_LOGGING_DRIVER_POST_MESSAGES_FREQUENCY"
 | 
						|
	envVarPostMessagesBatchSize = "SPLUNK_LOGGING_DRIVER_POST_MESSAGES_BATCH_SIZE"
 | 
						|
	envVarBufferMaximum         = "SPLUNK_LOGGING_DRIVER_BUFFER_MAX"
 | 
						|
	envVarStreamChannelSize     = "SPLUNK_LOGGING_DRIVER_CHANNEL_SIZE"
 | 
						|
)
 | 
						|
 | 
						|
type splunkLoggerInterface interface {
 | 
						|
	logger.Logger
 | 
						|
	worker()
 | 
						|
}
 | 
						|
 | 
						|
type splunkLogger struct {
 | 
						|
	client    *http.Client
 | 
						|
	transport *http.Transport
 | 
						|
 | 
						|
	url         string
 | 
						|
	auth        string
 | 
						|
	nullMessage *splunkMessage
 | 
						|
 | 
						|
	// http compression
 | 
						|
	gzipCompression      bool
 | 
						|
	gzipCompressionLevel int
 | 
						|
 | 
						|
	// Advanced options
 | 
						|
	postMessagesFrequency time.Duration
 | 
						|
	postMessagesBatchSize int
 | 
						|
	bufferMaximum         int
 | 
						|
 | 
						|
	// For synchronization between background worker and logger.
 | 
						|
	// We use channel to send messages to worker go routine.
 | 
						|
	// All other variables for blocking Close call before we flush all messages to HEC
 | 
						|
	stream     chan *splunkMessage
 | 
						|
	lock       sync.RWMutex
 | 
						|
	closed     bool
 | 
						|
	closedCond *sync.Cond
 | 
						|
}
 | 
						|
 | 
						|
type splunkLoggerInline struct {
 | 
						|
	*splunkLogger
 | 
						|
 | 
						|
	nullEvent *splunkMessageEvent
 | 
						|
}
 | 
						|
 | 
						|
type splunkLoggerJSON struct {
 | 
						|
	*splunkLoggerInline
 | 
						|
}
 | 
						|
 | 
						|
type splunkLoggerRaw struct {
 | 
						|
	*splunkLogger
 | 
						|
 | 
						|
	prefix []byte
 | 
						|
}
 | 
						|
 | 
						|
type splunkMessage struct {
 | 
						|
	Event      interface{} `json:"event"`
 | 
						|
	Time       string      `json:"time"`
 | 
						|
	Host       string      `json:"host"`
 | 
						|
	Source     string      `json:"source,omitempty"`
 | 
						|
	SourceType string      `json:"sourcetype,omitempty"`
 | 
						|
	Index      string      `json:"index,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
type splunkMessageEvent struct {
 | 
						|
	Line   interface{}       `json:"line"`
 | 
						|
	Source string            `json:"source"`
 | 
						|
	Tag    string            `json:"tag,omitempty"`
 | 
						|
	Attrs  map[string]string `json:"attrs,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
const (
 | 
						|
	splunkFormatRaw    = "raw"
 | 
						|
	splunkFormatJSON   = "json"
 | 
						|
	splunkFormatInline = "inline"
 | 
						|
)
 | 
						|
 | 
						|
func init() {
 | 
						|
	if err := logger.RegisterLogDriver(driverName, New); err != nil {
 | 
						|
		logrus.Fatal(err)
 | 
						|
	}
 | 
						|
	if err := logger.RegisterLogOptValidator(driverName, ValidateLogOpt); err != nil {
 | 
						|
		logrus.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// New creates splunk logger driver using configuration passed in context
 | 
						|
func New(ctx logger.Context) (logger.Logger, error) {
 | 
						|
	hostname, err := ctx.Hostname()
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("%s: cannot access hostname to set source field", driverName)
 | 
						|
	}
 | 
						|
 | 
						|
	// Parse and validate Splunk URL
 | 
						|
	splunkURL, err := parseURL(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Splunk Token is required parameter
 | 
						|
	splunkToken, ok := ctx.Config[splunkTokenKey]
 | 
						|
	if !ok {
 | 
						|
		return nil, fmt.Errorf("%s: %s is expected", driverName, splunkTokenKey)
 | 
						|
	}
 | 
						|
 | 
						|
	tlsConfig := &tls.Config{}
 | 
						|
 | 
						|
	// Splunk is using autogenerated certificates by default,
 | 
						|
	// allow users to trust them with skipping verification
 | 
						|
	if insecureSkipVerifyStr, ok := ctx.Config[splunkInsecureSkipVerifyKey]; ok {
 | 
						|
		insecureSkipVerify, err := strconv.ParseBool(insecureSkipVerifyStr)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		tlsConfig.InsecureSkipVerify = insecureSkipVerify
 | 
						|
	}
 | 
						|
 | 
						|
	// If path to the root certificate is provided - load it
 | 
						|
	if caPath, ok := ctx.Config[splunkCAPathKey]; ok {
 | 
						|
		caCert, err := ioutil.ReadFile(caPath)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		caPool := x509.NewCertPool()
 | 
						|
		caPool.AppendCertsFromPEM(caCert)
 | 
						|
		tlsConfig.RootCAs = caPool
 | 
						|
	}
 | 
						|
 | 
						|
	if caName, ok := ctx.Config[splunkCANameKey]; ok {
 | 
						|
		tlsConfig.ServerName = caName
 | 
						|
	}
 | 
						|
 | 
						|
	gzipCompression := false
 | 
						|
	if gzipCompressionStr, ok := ctx.Config[splunkGzipCompressionKey]; ok {
 | 
						|
		gzipCompression, err = strconv.ParseBool(gzipCompressionStr)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	gzipCompressionLevel := gzip.DefaultCompression
 | 
						|
	if gzipCompressionLevelStr, ok := ctx.Config[splunkGzipCompressionLevelKey]; ok {
 | 
						|
		var err error
 | 
						|
		gzipCompressionLevel64, err := strconv.ParseInt(gzipCompressionLevelStr, 10, 32)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		gzipCompressionLevel = int(gzipCompressionLevel64)
 | 
						|
		if gzipCompressionLevel < gzip.DefaultCompression || gzipCompressionLevel > gzip.BestCompression {
 | 
						|
			err := fmt.Errorf("Not supported level '%s' for %s (supported values between %d and %d).",
 | 
						|
				gzipCompressionLevelStr, splunkGzipCompressionLevelKey, gzip.DefaultCompression, gzip.BestCompression)
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	transport := &http.Transport{
 | 
						|
		TLSClientConfig: tlsConfig,
 | 
						|
	}
 | 
						|
	client := &http.Client{
 | 
						|
		Transport: transport,
 | 
						|
	}
 | 
						|
 | 
						|
	source := ctx.Config[splunkSourceKey]
 | 
						|
	sourceType := ctx.Config[splunkSourceTypeKey]
 | 
						|
	index := ctx.Config[splunkIndexKey]
 | 
						|
 | 
						|
	var nullMessage = &splunkMessage{
 | 
						|
		Host:       hostname,
 | 
						|
		Source:     source,
 | 
						|
		SourceType: sourceType,
 | 
						|
		Index:      index,
 | 
						|
	}
 | 
						|
 | 
						|
	// Allow user to remove tag from the messages by setting tag to empty string
 | 
						|
	tag := ""
 | 
						|
	if tagTemplate, ok := ctx.Config[tagKey]; !ok || tagTemplate != "" {
 | 
						|
		tag, err = loggerutils.ParseLogTag(ctx, loggerutils.DefaultTemplate)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	attrs := ctx.ExtraAttributes(nil)
 | 
						|
 | 
						|
	var (
 | 
						|
		postMessagesFrequency = getAdvancedOptionDuration(envVarPostMessagesFrequency, defaultPostMessagesFrequency)
 | 
						|
		postMessagesBatchSize = getAdvancedOptionInt(envVarPostMessagesBatchSize, defaultPostMessagesBatchSize)
 | 
						|
		bufferMaximum         = getAdvancedOptionInt(envVarBufferMaximum, defaultBufferMaximum)
 | 
						|
		streamChannelSize     = getAdvancedOptionInt(envVarStreamChannelSize, defaultStreamChannelSize)
 | 
						|
	)
 | 
						|
 | 
						|
	logger := &splunkLogger{
 | 
						|
		client:                client,
 | 
						|
		transport:             transport,
 | 
						|
		url:                   splunkURL.String(),
 | 
						|
		auth:                  "Splunk " + splunkToken,
 | 
						|
		nullMessage:           nullMessage,
 | 
						|
		gzipCompression:       gzipCompression,
 | 
						|
		gzipCompressionLevel:  gzipCompressionLevel,
 | 
						|
		stream:                make(chan *splunkMessage, streamChannelSize),
 | 
						|
		postMessagesFrequency: postMessagesFrequency,
 | 
						|
		postMessagesBatchSize: postMessagesBatchSize,
 | 
						|
		bufferMaximum:         bufferMaximum,
 | 
						|
	}
 | 
						|
 | 
						|
	// By default we verify connection, but we allow use to skip that
 | 
						|
	verifyConnection := true
 | 
						|
	if verifyConnectionStr, ok := ctx.Config[splunkVerifyConnectionKey]; ok {
 | 
						|
		var err error
 | 
						|
		verifyConnection, err = strconv.ParseBool(verifyConnectionStr)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if verifyConnection {
 | 
						|
		err = verifySplunkConnection(logger)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	var splunkFormat string
 | 
						|
	if splunkFormatParsed, ok := ctx.Config[splunkFormatKey]; ok {
 | 
						|
		switch splunkFormatParsed {
 | 
						|
		case splunkFormatInline:
 | 
						|
		case splunkFormatJSON:
 | 
						|
		case splunkFormatRaw:
 | 
						|
		default:
 | 
						|
			return nil, fmt.Errorf("Unknown format specified %s, supported formats are inline, json and raw", splunkFormat)
 | 
						|
		}
 | 
						|
		splunkFormat = splunkFormatParsed
 | 
						|
	} else {
 | 
						|
		splunkFormat = splunkFormatInline
 | 
						|
	}
 | 
						|
 | 
						|
	var loggerWrapper splunkLoggerInterface
 | 
						|
 | 
						|
	switch splunkFormat {
 | 
						|
	case splunkFormatInline:
 | 
						|
		nullEvent := &splunkMessageEvent{
 | 
						|
			Tag:   tag,
 | 
						|
			Attrs: attrs,
 | 
						|
		}
 | 
						|
 | 
						|
		loggerWrapper = &splunkLoggerInline{logger, nullEvent}
 | 
						|
	case splunkFormatJSON:
 | 
						|
		nullEvent := &splunkMessageEvent{
 | 
						|
			Tag:   tag,
 | 
						|
			Attrs: attrs,
 | 
						|
		}
 | 
						|
 | 
						|
		loggerWrapper = &splunkLoggerJSON{&splunkLoggerInline{logger, nullEvent}}
 | 
						|
	case splunkFormatRaw:
 | 
						|
		var prefix bytes.Buffer
 | 
						|
		if tag != "" {
 | 
						|
			prefix.WriteString(tag)
 | 
						|
			prefix.WriteString(" ")
 | 
						|
		}
 | 
						|
		for key, value := range attrs {
 | 
						|
			prefix.WriteString(key)
 | 
						|
			prefix.WriteString("=")
 | 
						|
			prefix.WriteString(value)
 | 
						|
			prefix.WriteString(" ")
 | 
						|
		}
 | 
						|
 | 
						|
		loggerWrapper = &splunkLoggerRaw{logger, prefix.Bytes()}
 | 
						|
	default:
 | 
						|
		return nil, fmt.Errorf("Unexpected format %s", splunkFormat)
 | 
						|
	}
 | 
						|
 | 
						|
	go loggerWrapper.worker()
 | 
						|
 | 
						|
	return loggerWrapper, nil
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLoggerInline) Log(msg *logger.Message) error {
 | 
						|
	message := l.createSplunkMessage(msg)
 | 
						|
 | 
						|
	event := *l.nullEvent
 | 
						|
	event.Line = string(msg.Line)
 | 
						|
	event.Source = msg.Source
 | 
						|
 | 
						|
	message.Event = &event
 | 
						|
 | 
						|
	return l.queueMessageAsync(message)
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLoggerJSON) Log(msg *logger.Message) error {
 | 
						|
	message := l.createSplunkMessage(msg)
 | 
						|
	event := *l.nullEvent
 | 
						|
 | 
						|
	var rawJSONMessage json.RawMessage
 | 
						|
	if err := json.Unmarshal(msg.Line, &rawJSONMessage); err == nil {
 | 
						|
		event.Line = &rawJSONMessage
 | 
						|
	} else {
 | 
						|
		event.Line = string(msg.Line)
 | 
						|
	}
 | 
						|
 | 
						|
	event.Source = msg.Source
 | 
						|
 | 
						|
	message.Event = &event
 | 
						|
 | 
						|
	return l.queueMessageAsync(message)
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLoggerRaw) Log(msg *logger.Message) error {
 | 
						|
	message := l.createSplunkMessage(msg)
 | 
						|
 | 
						|
	message.Event = string(append(l.prefix, msg.Line...))
 | 
						|
 | 
						|
	return l.queueMessageAsync(message)
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLogger) queueMessageAsync(message *splunkMessage) error {
 | 
						|
	l.lock.RLock()
 | 
						|
	defer l.lock.RUnlock()
 | 
						|
	if l.closedCond != nil {
 | 
						|
		return fmt.Errorf("%s: driver is closed", driverName)
 | 
						|
	}
 | 
						|
	l.stream <- message
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLogger) worker() {
 | 
						|
	timer := time.NewTicker(l.postMessagesFrequency)
 | 
						|
	var messages []*splunkMessage
 | 
						|
	for {
 | 
						|
		select {
 | 
						|
		case message, open := <-l.stream:
 | 
						|
			if !open {
 | 
						|
				l.postMessages(messages, true)
 | 
						|
				l.lock.Lock()
 | 
						|
				defer l.lock.Unlock()
 | 
						|
				l.transport.CloseIdleConnections()
 | 
						|
				l.closed = true
 | 
						|
				l.closedCond.Signal()
 | 
						|
				return
 | 
						|
			}
 | 
						|
			messages = append(messages, message)
 | 
						|
			// Only sending when we get exactly to the batch size,
 | 
						|
			// This also helps not to fire postMessages on every new message,
 | 
						|
			// when previous try failed.
 | 
						|
			if len(messages)%l.postMessagesBatchSize == 0 {
 | 
						|
				messages = l.postMessages(messages, false)
 | 
						|
			}
 | 
						|
		case <-timer.C:
 | 
						|
			messages = l.postMessages(messages, false)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLogger) postMessages(messages []*splunkMessage, lastChance bool) []*splunkMessage {
 | 
						|
	messagesLen := len(messages)
 | 
						|
	for i := 0; i < messagesLen; i += l.postMessagesBatchSize {
 | 
						|
		upperBound := i + l.postMessagesBatchSize
 | 
						|
		if upperBound > messagesLen {
 | 
						|
			upperBound = messagesLen
 | 
						|
		}
 | 
						|
		if err := l.tryPostMessages(messages[i:upperBound]); err != nil {
 | 
						|
			logrus.Error(err)
 | 
						|
			if messagesLen-i >= l.bufferMaximum || lastChance {
 | 
						|
				// If this is last chance - print them all to the daemon log
 | 
						|
				if lastChance {
 | 
						|
					upperBound = messagesLen
 | 
						|
				}
 | 
						|
				// Not all sent, but buffer has got to its maximum, let's log all messages
 | 
						|
				// we could not send and return buffer minus one batch size
 | 
						|
				for j := i; j < upperBound; j++ {
 | 
						|
					if jsonEvent, err := json.Marshal(messages[j]); err != nil {
 | 
						|
						logrus.Error(err)
 | 
						|
					} else {
 | 
						|
						logrus.Error(fmt.Errorf("Failed to send a message '%s'", string(jsonEvent)))
 | 
						|
					}
 | 
						|
				}
 | 
						|
				return messages[upperBound:messagesLen]
 | 
						|
			}
 | 
						|
			// Not all sent, returning buffer from where we have not sent messages
 | 
						|
			return messages[i:messagesLen]
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// All sent, return empty buffer
 | 
						|
	return messages[:0]
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLogger) tryPostMessages(messages []*splunkMessage) error {
 | 
						|
	if len(messages) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	var buffer bytes.Buffer
 | 
						|
	var writer io.Writer
 | 
						|
	var gzipWriter *gzip.Writer
 | 
						|
	var err error
 | 
						|
	// If gzip compression is enabled - create gzip writer with specified compression
 | 
						|
	// level. If gzip compression is disabled, use standard buffer as a writer
 | 
						|
	if l.gzipCompression {
 | 
						|
		gzipWriter, err = gzip.NewWriterLevel(&buffer, l.gzipCompressionLevel)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		writer = gzipWriter
 | 
						|
	} else {
 | 
						|
		writer = &buffer
 | 
						|
	}
 | 
						|
	for _, message := range messages {
 | 
						|
		jsonEvent, err := json.Marshal(message)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if _, err := writer.Write(jsonEvent); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// If gzip compression is enabled, tell it, that we are done
 | 
						|
	if l.gzipCompression {
 | 
						|
		err = gzipWriter.Close()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(buffer.Bytes()))
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	req.Header.Set("Authorization", l.auth)
 | 
						|
	// Tell if we are sending gzip compressed body
 | 
						|
	if l.gzipCompression {
 | 
						|
		req.Header.Set("Content-Encoding", "gzip")
 | 
						|
	}
 | 
						|
	res, err := l.client.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer res.Body.Close()
 | 
						|
	if res.StatusCode != http.StatusOK {
 | 
						|
		var body []byte
 | 
						|
		body, err = ioutil.ReadAll(res.Body)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body)
 | 
						|
	}
 | 
						|
	io.Copy(ioutil.Discard, res.Body)
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLogger) Close() error {
 | 
						|
	l.lock.Lock()
 | 
						|
	defer l.lock.Unlock()
 | 
						|
	if l.closedCond == nil {
 | 
						|
		l.closedCond = sync.NewCond(&l.lock)
 | 
						|
		close(l.stream)
 | 
						|
		for !l.closed {
 | 
						|
			l.closedCond.Wait()
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLogger) Name() string {
 | 
						|
	return driverName
 | 
						|
}
 | 
						|
 | 
						|
func (l *splunkLogger) createSplunkMessage(msg *logger.Message) *splunkMessage {
 | 
						|
	message := *l.nullMessage
 | 
						|
	message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/float64(time.Second))
 | 
						|
	return &message
 | 
						|
}
 | 
						|
 | 
						|
// ValidateLogOpt looks for all supported by splunk driver options
 | 
						|
func ValidateLogOpt(cfg map[string]string) error {
 | 
						|
	for key := range cfg {
 | 
						|
		switch key {
 | 
						|
		case splunkURLKey:
 | 
						|
		case splunkTokenKey:
 | 
						|
		case splunkSourceKey:
 | 
						|
		case splunkSourceTypeKey:
 | 
						|
		case splunkIndexKey:
 | 
						|
		case splunkCAPathKey:
 | 
						|
		case splunkCANameKey:
 | 
						|
		case splunkInsecureSkipVerifyKey:
 | 
						|
		case splunkFormatKey:
 | 
						|
		case splunkVerifyConnectionKey:
 | 
						|
		case splunkGzipCompressionKey:
 | 
						|
		case splunkGzipCompressionLevelKey:
 | 
						|
		case envKey:
 | 
						|
		case labelsKey:
 | 
						|
		case tagKey:
 | 
						|
		default:
 | 
						|
			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func parseURL(ctx logger.Context) (*url.URL, error) {
 | 
						|
	splunkURLStr, ok := ctx.Config[splunkURLKey]
 | 
						|
	if !ok {
 | 
						|
		return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey)
 | 
						|
	}
 | 
						|
 | 
						|
	splunkURL, err := url.Parse(splunkURLStr)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey)
 | 
						|
	}
 | 
						|
 | 
						|
	if !urlutil.IsURL(splunkURLStr) ||
 | 
						|
		!splunkURL.IsAbs() ||
 | 
						|
		(splunkURL.Path != "" && splunkURL.Path != "/") ||
 | 
						|
		splunkURL.RawQuery != "" ||
 | 
						|
		splunkURL.Fragment != "" {
 | 
						|
		return nil, fmt.Errorf("%s: expected format scheme://dns_name_or_ip:port for %s", driverName, splunkURLKey)
 | 
						|
	}
 | 
						|
 | 
						|
	splunkURL.Path = "/services/collector/event/1.0"
 | 
						|
 | 
						|
	return splunkURL, nil
 | 
						|
}
 | 
						|
 | 
						|
func verifySplunkConnection(l *splunkLogger) error {
 | 
						|
	req, err := http.NewRequest(http.MethodOptions, l.url, nil)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	res, err := l.client.Do(req)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if res.Body != nil {
 | 
						|
		defer res.Body.Close()
 | 
						|
	}
 | 
						|
	if res.StatusCode != http.StatusOK {
 | 
						|
		var body []byte
 | 
						|
		body, err = ioutil.ReadAll(res.Body)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func getAdvancedOptionDuration(envName string, defaultValue time.Duration) time.Duration {
 | 
						|
	valueStr := os.Getenv(envName)
 | 
						|
	if valueStr == "" {
 | 
						|
		return defaultValue
 | 
						|
	}
 | 
						|
	parsedValue, err := time.ParseDuration(valueStr)
 | 
						|
	if err != nil {
 | 
						|
		logrus.Error(fmt.Sprintf("Failed to parse value of %s as duration. Using default %v. %v", envName, defaultValue, err))
 | 
						|
		return defaultValue
 | 
						|
	}
 | 
						|
	return parsedValue
 | 
						|
}
 | 
						|
 | 
						|
func getAdvancedOptionInt(envName string, defaultValue int) int {
 | 
						|
	valueStr := os.Getenv(envName)
 | 
						|
	if valueStr == "" {
 | 
						|
		return defaultValue
 | 
						|
	}
 | 
						|
	parsedValue, err := strconv.ParseInt(valueStr, 10, 32)
 | 
						|
	if err != nil {
 | 
						|
		logrus.Error(fmt.Sprintf("Failed to parse value of %s as integer. Using default %d. %v", envName, defaultValue, err))
 | 
						|
		return defaultValue
 | 
						|
	}
 | 
						|
	return int(parsedValue)
 | 
						|
}
 |