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  }