blob: e587e66aa08d8aafdc9e6f5324dd2ee0371830a1 [file] [log] [blame]
// Package splunk provides the log driver for forwarding server logs to
// Splunk HTTP Event Collector endpoint.
package splunk
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"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"
envKey = "env"
labelsKey = "labels"
tagKey = "tag"
)
type splunkLogger struct {
client *http.Client
transport *http.Transport
url string
auth string
nullMessage *splunkMessage
}
type splunkMessage struct {
Event splunkMessageEvent `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 string `json:"line"`
Source string `json:"source"`
Tag string `json:"tag,omitempty"`
Attrs map[string]string `json:"attrs,omitempty"`
}
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
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
client := &http.Client{
Transport: transport,
}
var nullMessage = &splunkMessage{
Host: hostname,
}
// Optional parameters for messages
nullMessage.Source = ctx.Config[splunkSourceKey]
nullMessage.SourceType = ctx.Config[splunkSourceTypeKey]
nullMessage.Index = ctx.Config[splunkIndexKey]
tag, err := loggerutils.ParseLogTag(ctx, "{{.ID}}")
if err != nil {
return nil, err
}
nullMessage.Event.Tag = tag
nullMessage.Event.Attrs = ctx.ExtraAttributes(nil)
logger := &splunkLogger{
client: client,
transport: transport,
url: splunkURL.String(),
auth: "Splunk " + splunkToken,
nullMessage: nullMessage,
}
err = verifySplunkConnection(logger)
if err != nil {
return nil, err
}
return logger, nil
}
func (l *splunkLogger) Log(msg *logger.Message) error {
// Construct message as a copy of nullMessage
message := *l.nullMessage
message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/1000000000)
message.Event.Line = string(msg.Line)
message.Event.Source = msg.Source
jsonEvent, err := json.Marshal(&message)
if err != nil {
return err
}
req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(jsonEvent))
if err != nil {
return err
}
req.Header.Set("Authorization", l.auth)
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.transport.CloseIdleConnections()
return nil
}
func (l *splunkLogger) Name() string {
return driverName
}
// 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 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("OPTIONS", 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
}