Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Splunk logging driver #16207 #16488

Merged
merged 1 commit into from
Oct 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion contrib/completion/bash/docker
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ __docker_log_drivers() {
journald
json-file
none
splunk
syslog
" -- "$cur" ) )
}
Expand All @@ -333,8 +334,9 @@ __docker_log_driver_options() {
local journald_options="env labels"
local json_file_options="env labels max-file max-size"
local syslog_options="syslog-address syslog-facility tag"
local splunk_options="splunk-caname splunk-capath splunk-index splunk-insecureskipverify splunk-source splunk-sourcetype splunk-token splunk-url"

local all_options="$fluentd_options $gelf_options $journald_options $json_file_options $syslog_options"
local all_options="$fluentd_options $gelf_options $journald_options $json_file_options $syslog_options $splunk_options"

case $(__docker_value_of_option --log-driver) in
'')
Expand All @@ -358,6 +360,9 @@ __docker_log_driver_options() {
syslog)
COMPREPLY=( $( compgen -W "$syslog_options" -S = -- "$cur" ) )
;;
splunk)
COMPREPLY=( $( compgen -W "$splunk_options" -S = -- "$cur" ) )
;;
*)
return
;;
Expand Down Expand Up @@ -405,6 +410,17 @@ __docker_complete_log_driver_options() {
" -- "${cur#=}" ) )
return
;;
*splunk-url=*)
COMPREPLY=( $( compgen -W "http:// https://" -- "${cur#=}" ) )
compopt -o nospace
__ltrim_colon_completions "${cur}"
return
;;
*splunk-insecureskipverify=*)
COMPREPLY=( $( compgen -W "true false" -- "${cur#=}" ) )
compopt -o nospace
return
;;
esac
return 1
}
Expand Down
1 change: 1 addition & 0 deletions daemon/logdrivers_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ import (
_ "github.com/docker/docker/daemon/logger/gelf"
_ "github.com/docker/docker/daemon/logger/journald"
_ "github.com/docker/docker/daemon/logger/jsonfilelog"
_ "github.com/docker/docker/daemon/logger/splunk"
_ "github.com/docker/docker/daemon/logger/syslog"
)
256 changes: 256 additions & 0 deletions daemon/logger/splunk/splunk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// 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/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"
)

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"`
ContainerID string `json:"containerId"`
Source string `json:"source"`
}

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 skiping 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]

logger := &splunkLogger{
client: client,
transport: transport,
url: splunkURL.String(),
auth: "Splunk " + splunkToken,
nullMessage: nullMessage,
}

err = verifySplunkConnection(logger)
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm completely off-topic but if the connection to splunk is unavailable when starting the plugin, will the plugin start ? (like it does with fluentd, ref. to fix it is there #17182)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually placed check here to fail start command so user will know that logging is not available. This call does not do anything other than that.
I can just remove it from here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vdemeester is there are maybe an easy way to showing a WARNING instead of returning err?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh messed my comment, I meant "will the container start ?" The idea would be to do more or less what's done there : https://github.com/docker/docker/pull/17182/files#diff-4e76640044eca60a4a45cd8631049282R66 I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually understood your first comment as "will the container start", but kind of did not answer :)

My intention was to actually not start container when logger driver is misconfigured, I guess this is wrong. Two options: a) I can just remove this line and user will see the error when for the first time this logger will try to send logs b) I can do something similar to fluentd where we will send some warnings when we cannot connect to splunk on container start.

I do not see a lot of benefits in (b). We do not need to do any initial connections, so I will probably just remove this check. Any objections?

/cc @glennblock @itay @LK4D4

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting container without properly configured logdriver wrong indeed and that PR is wrong too.

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 = splunkMessageEvent{
Line: string(msg.Line),
ContainerID: msg.ContainerID,
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
}
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 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:
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 schema://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
}
2 changes: 1 addition & 1 deletion docs/reference/api/docker_remote_api_v1.22.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ Json Parameters:
systems, such as SELinux.
- **LogConfig** - Log configuration for the container, specified as a JSON object in the form
`{ "Type": "<driver_name>", "Config": {"key1": "val1"}}`.
Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `none`.
Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `splunk`, `none`.
`json-file` logging driver.
- **CgroupParent** - Path to `cgroups` under which the container's `cgroup` is created. If the path is not absolute, the path is considered to be relative to the `cgroups` path of the init process. Cgroups are created if they do not already exist.
- **VolumeDriver** - Driver that this container users to mount volumes.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/logging/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ weight=8
* [Fluentd logging driver](fluentd.md)
* [Journald logging driver](journald.md)
* [Amazon CloudWatch Logs logging driver](awslogs.md)
* [Splunk logging driver](splunk.md)
11 changes: 11 additions & 0 deletions docs/reference/logging/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ container's logging driver. The following options are supported:
| `gelf` | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. |
| `fluentd` | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input). |
| `awslogs` | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs. |
| `splunk` | Splunk logging driver for Docker. Writes log messages to `splunk` using HTTP Event Collector. |

The `docker logs`command is available only for the `json-file` logging driver.

Expand Down Expand Up @@ -172,3 +173,13 @@ The Amazon CloudWatch Logs logging driver supports the following options:


For detailed information on working with this logging driver, see [the awslogs logging driver](awslogs.md) reference documentation.

## Splunk options

The Splunk logging driver requires the following options:

--log-opt splunk-token=<splunk_http_event_collector_token>
--log-opt splunk-url=https://your_splunk_instance:8088

For detailed information about working with this logging driver, see the [Splunk logging driver](splunk.md)
reference documentation.
Loading