github.com/dpiddy/docker@v1.12.2-rc1/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  	"crypto/tls"
     8  	"crypto/x509"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/http"
    14  	"net/url"
    15  	"strconv"
    16  
    17  	"github.com/Sirupsen/logrus"
    18  	"github.com/docker/docker/daemon/logger"
    19  	"github.com/docker/docker/daemon/logger/loggerutils"
    20  	"github.com/docker/docker/pkg/urlutil"
    21  )
    22  
    23  const (
    24  	driverName                  = "splunk"
    25  	splunkURLKey                = "splunk-url"
    26  	splunkTokenKey              = "splunk-token"
    27  	splunkSourceKey             = "splunk-source"
    28  	splunkSourceTypeKey         = "splunk-sourcetype"
    29  	splunkIndexKey              = "splunk-index"
    30  	splunkCAPathKey             = "splunk-capath"
    31  	splunkCANameKey             = "splunk-caname"
    32  	splunkInsecureSkipVerifyKey = "splunk-insecureskipverify"
    33  	envKey                      = "env"
    34  	labelsKey                   = "labels"
    35  	tagKey                      = "tag"
    36  )
    37  
    38  type splunkLogger struct {
    39  	client    *http.Client
    40  	transport *http.Transport
    41  
    42  	url         string
    43  	auth        string
    44  	nullMessage *splunkMessage
    45  }
    46  
    47  type splunkMessage struct {
    48  	Event      splunkMessageEvent `json:"event"`
    49  	Time       string             `json:"time"`
    50  	Host       string             `json:"host"`
    51  	Source     string             `json:"source,omitempty"`
    52  	SourceType string             `json:"sourcetype,omitempty"`
    53  	Index      string             `json:"index,omitempty"`
    54  }
    55  
    56  type splunkMessageEvent struct {
    57  	Line   string            `json:"line"`
    58  	Source string            `json:"source"`
    59  	Tag    string            `json:"tag,omitempty"`
    60  	Attrs  map[string]string `json:"attrs,omitempty"`
    61  }
    62  
    63  func init() {
    64  	if err := logger.RegisterLogDriver(driverName, New); err != nil {
    65  		logrus.Fatal(err)
    66  	}
    67  	if err := logger.RegisterLogOptValidator(driverName, ValidateLogOpt); err != nil {
    68  		logrus.Fatal(err)
    69  	}
    70  }
    71  
    72  // New creates splunk logger driver using configuration passed in context
    73  func New(ctx logger.Context) (logger.Logger, error) {
    74  	hostname, err := ctx.Hostname()
    75  	if err != nil {
    76  		return nil, fmt.Errorf("%s: cannot access hostname to set source field", driverName)
    77  	}
    78  
    79  	// Parse and validate Splunk URL
    80  	splunkURL, err := parseURL(ctx)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	// Splunk Token is required parameter
    86  	splunkToken, ok := ctx.Config[splunkTokenKey]
    87  	if !ok {
    88  		return nil, fmt.Errorf("%s: %s is expected", driverName, splunkTokenKey)
    89  	}
    90  
    91  	tlsConfig := &tls.Config{}
    92  
    93  	// Splunk is using autogenerated certificates by default,
    94  	// allow users to trust them with skipping verification
    95  	if insecureSkipVerifyStr, ok := ctx.Config[splunkInsecureSkipVerifyKey]; ok {
    96  		insecureSkipVerify, err := strconv.ParseBool(insecureSkipVerifyStr)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  		tlsConfig.InsecureSkipVerify = insecureSkipVerify
   101  	}
   102  
   103  	// If path to the root certificate is provided - load it
   104  	if caPath, ok := ctx.Config[splunkCAPathKey]; ok {
   105  		caCert, err := ioutil.ReadFile(caPath)
   106  		if err != nil {
   107  			return nil, err
   108  		}
   109  		caPool := x509.NewCertPool()
   110  		caPool.AppendCertsFromPEM(caCert)
   111  		tlsConfig.RootCAs = caPool
   112  	}
   113  
   114  	if caName, ok := ctx.Config[splunkCANameKey]; ok {
   115  		tlsConfig.ServerName = caName
   116  	}
   117  
   118  	transport := &http.Transport{
   119  		TLSClientConfig: tlsConfig,
   120  	}
   121  	client := &http.Client{
   122  		Transport: transport,
   123  	}
   124  
   125  	var nullMessage = &splunkMessage{
   126  		Host: hostname,
   127  	}
   128  
   129  	// Optional parameters for messages
   130  	nullMessage.Source = ctx.Config[splunkSourceKey]
   131  	nullMessage.SourceType = ctx.Config[splunkSourceTypeKey]
   132  	nullMessage.Index = ctx.Config[splunkIndexKey]
   133  
   134  	tag, err := loggerutils.ParseLogTag(ctx, "{{.ID}}")
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	nullMessage.Event.Tag = tag
   139  	nullMessage.Event.Attrs = ctx.ExtraAttributes(nil)
   140  
   141  	logger := &splunkLogger{
   142  		client:      client,
   143  		transport:   transport,
   144  		url:         splunkURL.String(),
   145  		auth:        "Splunk " + splunkToken,
   146  		nullMessage: nullMessage,
   147  	}
   148  
   149  	err = verifySplunkConnection(logger)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	return logger, nil
   155  }
   156  
   157  func (l *splunkLogger) Log(msg *logger.Message) error {
   158  	// Construct message as a copy of nullMessage
   159  	message := *l.nullMessage
   160  	message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/1000000000)
   161  	message.Event.Line = string(msg.Line)
   162  	message.Event.Source = msg.Source
   163  
   164  	jsonEvent, err := json.Marshal(&message)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(jsonEvent))
   169  	if err != nil {
   170  		return err
   171  	}
   172  	req.Header.Set("Authorization", l.auth)
   173  	res, err := l.client.Do(req)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	defer res.Body.Close()
   178  	if res.StatusCode != http.StatusOK {
   179  		var body []byte
   180  		body, err = ioutil.ReadAll(res.Body)
   181  		if err != nil {
   182  			return err
   183  		}
   184  		return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body)
   185  	}
   186  	io.Copy(ioutil.Discard, res.Body)
   187  	return nil
   188  }
   189  
   190  func (l *splunkLogger) Close() error {
   191  	l.transport.CloseIdleConnections()
   192  	return nil
   193  }
   194  
   195  func (l *splunkLogger) Name() string {
   196  	return driverName
   197  }
   198  
   199  // ValidateLogOpt looks for all supported by splunk driver options
   200  func ValidateLogOpt(cfg map[string]string) error {
   201  	for key := range cfg {
   202  		switch key {
   203  		case splunkURLKey:
   204  		case splunkTokenKey:
   205  		case splunkSourceKey:
   206  		case splunkSourceTypeKey:
   207  		case splunkIndexKey:
   208  		case splunkCAPathKey:
   209  		case splunkCANameKey:
   210  		case splunkInsecureSkipVerifyKey:
   211  		case envKey:
   212  		case labelsKey:
   213  		case tagKey:
   214  		default:
   215  			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName)
   216  		}
   217  	}
   218  	return nil
   219  }
   220  
   221  func parseURL(ctx logger.Context) (*url.URL, error) {
   222  	splunkURLStr, ok := ctx.Config[splunkURLKey]
   223  	if !ok {
   224  		return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey)
   225  	}
   226  
   227  	splunkURL, err := url.Parse(splunkURLStr)
   228  	if err != nil {
   229  		return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey)
   230  	}
   231  
   232  	if !urlutil.IsURL(splunkURLStr) ||
   233  		!splunkURL.IsAbs() ||
   234  		(splunkURL.Path != "" && splunkURL.Path != "/") ||
   235  		splunkURL.RawQuery != "" ||
   236  		splunkURL.Fragment != "" {
   237  		return nil, fmt.Errorf("%s: expected format scheme://dns_name_or_ip:port for %s", driverName, splunkURLKey)
   238  	}
   239  
   240  	splunkURL.Path = "/services/collector/event/1.0"
   241  
   242  	return splunkURL, nil
   243  }
   244  
   245  func verifySplunkConnection(l *splunkLogger) error {
   246  	req, err := http.NewRequest("OPTIONS", l.url, nil)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	res, err := l.client.Do(req)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	if res.Body != nil {
   255  		defer res.Body.Close()
   256  	}
   257  	if res.StatusCode != http.StatusOK {
   258  		var body []byte
   259  		body, err = ioutil.ReadAll(res.Body)
   260  		if err != nil {
   261  			return err
   262  		}
   263  		return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body)
   264  	}
   265  	return nil
   266  }