Browse code

Add Splunk logging driver #16207

Allow to send Splunk logs using Http Event Collector

Signed-off-by: Denis Gladkikh <denis@gladkikh.email>

Denis Gladkikh authored on 2015/08/28 08:03:46
Showing 10 changed files
... ...
@@ -321,6 +321,7 @@ __docker_log_drivers() {
321 321
 		journald
322 322
 		json-file
323 323
 		none
324
+		splunk
324 325
 		syslog
325 326
 	" -- "$cur" ) )
326 327
 }
... ...
@@ -333,8 +334,9 @@ __docker_log_driver_options() {
333 333
 	local journald_options="env labels"
334 334
 	local json_file_options="env labels max-file max-size"
335 335
 	local syslog_options="syslog-address syslog-facility tag"
336
+	local splunk_options="splunk-caname splunk-capath splunk-index splunk-insecureskipverify splunk-source splunk-sourcetype splunk-token splunk-url"
336 337
 
337
-	local all_options="$fluentd_options $gelf_options $journald_options $json_file_options $syslog_options"
338
+	local all_options="$fluentd_options $gelf_options $journald_options $json_file_options $syslog_options $splunk_options"
338 339
 
339 340
 	case $(__docker_value_of_option --log-driver) in
340 341
 		'')
... ...
@@ -358,6 +360,9 @@ __docker_log_driver_options() {
358 358
 		syslog)
359 359
 			COMPREPLY=( $( compgen -W "$syslog_options" -S = -- "$cur" ) )
360 360
 			;;
361
+		splunk)
362
+			COMPREPLY=( $( compgen -W "$splunk_options" -S = -- "$cur" ) )
363
+			;;
361 364
 		*)
362 365
 			return
363 366
 			;;
... ...
@@ -405,6 +410,17 @@ __docker_complete_log_driver_options() {
405 405
 			" -- "${cur#=}" ) )
406 406
 			return
407 407
 			;;
408
+		*splunk-url=*)
409
+			COMPREPLY=( $( compgen -W "http:// https://" -- "${cur#=}" ) )
410
+			compopt -o nospace
411
+			__ltrim_colon_completions "${cur}"
412
+			return
413
+			;;
414
+		*splunk-insecureskipverify=*)
415
+			COMPREPLY=( $( compgen -W "true false" -- "${cur#=}" ) )
416
+			compopt -o nospace
417
+			return
418
+			;;
408 419
 	esac
409 420
 	return 1
410 421
 }
... ...
@@ -8,5 +8,6 @@ import (
8 8
 	_ "github.com/docker/docker/daemon/logger/gelf"
9 9
 	_ "github.com/docker/docker/daemon/logger/journald"
10 10
 	_ "github.com/docker/docker/daemon/logger/jsonfilelog"
11
+	_ "github.com/docker/docker/daemon/logger/splunk"
11 12
 	_ "github.com/docker/docker/daemon/logger/syslog"
12 13
 )
13 14
new file mode 100644
... ...
@@ -0,0 +1,256 @@
0
+// Package splunk provides the log driver for forwarding server logs to
1
+// Splunk HTTP Event Collector endpoint.
2
+package splunk
3
+
4
+import (
5
+	"bytes"
6
+	"crypto/tls"
7
+	"crypto/x509"
8
+	"encoding/json"
9
+	"fmt"
10
+	"io"
11
+	"io/ioutil"
12
+	"net/http"
13
+	"net/url"
14
+	"strconv"
15
+
16
+	"github.com/Sirupsen/logrus"
17
+	"github.com/docker/docker/daemon/logger"
18
+	"github.com/docker/docker/pkg/urlutil"
19
+)
20
+
21
+const (
22
+	driverName                  = "splunk"
23
+	splunkURLKey                = "splunk-url"
24
+	splunkTokenKey              = "splunk-token"
25
+	splunkSourceKey             = "splunk-source"
26
+	splunkSourceTypeKey         = "splunk-sourcetype"
27
+	splunkIndexKey              = "splunk-index"
28
+	splunkCAPathKey             = "splunk-capath"
29
+	splunkCANameKey             = "splunk-caname"
30
+	splunkInsecureSkipVerifyKey = "splunk-insecureskipverify"
31
+)
32
+
33
+type splunkLogger struct {
34
+	client    *http.Client
35
+	transport *http.Transport
36
+
37
+	url         string
38
+	auth        string
39
+	nullMessage *splunkMessage
40
+}
41
+
42
+type splunkMessage struct {
43
+	Event      splunkMessageEvent `json:"event"`
44
+	Time       string             `json:"time"`
45
+	Host       string             `json:"host"`
46
+	Source     string             `json:"source,omitempty"`
47
+	SourceType string             `json:"sourcetype,omitempty"`
48
+	Index      string             `json:"index,omitempty"`
49
+}
50
+
51
+type splunkMessageEvent struct {
52
+	Line        string `json:"line"`
53
+	ContainerID string `json:"containerId"`
54
+	Source      string `json:"source"`
55
+}
56
+
57
+func init() {
58
+	if err := logger.RegisterLogDriver(driverName, New); err != nil {
59
+		logrus.Fatal(err)
60
+	}
61
+	if err := logger.RegisterLogOptValidator(driverName, ValidateLogOpt); err != nil {
62
+		logrus.Fatal(err)
63
+	}
64
+}
65
+
66
+// New creates splunk logger driver using configuration passed in context
67
+func New(ctx logger.Context) (logger.Logger, error) {
68
+	hostname, err := ctx.Hostname()
69
+	if err != nil {
70
+		return nil, fmt.Errorf("%s: cannot access hostname to set source field", driverName)
71
+	}
72
+
73
+	// Parse and validate Splunk URL
74
+	splunkURL, err := parseURL(ctx)
75
+	if err != nil {
76
+		return nil, err
77
+	}
78
+
79
+	// Splunk Token is required parameter
80
+	splunkToken, ok := ctx.Config[splunkTokenKey]
81
+	if !ok {
82
+		return nil, fmt.Errorf("%s: %s is expected", driverName, splunkTokenKey)
83
+	}
84
+
85
+	tlsConfig := &tls.Config{}
86
+
87
+	// Splunk is using autogenerated certificates by default,
88
+	// allow users to trust them with skiping verification
89
+	if insecureSkipVerifyStr, ok := ctx.Config[splunkInsecureSkipVerifyKey]; ok {
90
+		insecureSkipVerify, err := strconv.ParseBool(insecureSkipVerifyStr)
91
+		if err != nil {
92
+			return nil, err
93
+		}
94
+		tlsConfig.InsecureSkipVerify = insecureSkipVerify
95
+	}
96
+
97
+	// If path to the root certificate is provided - load it
98
+	if caPath, ok := ctx.Config[splunkCAPathKey]; ok {
99
+		caCert, err := ioutil.ReadFile(caPath)
100
+		if err != nil {
101
+			return nil, err
102
+		}
103
+		caPool := x509.NewCertPool()
104
+		caPool.AppendCertsFromPEM(caCert)
105
+		tlsConfig.RootCAs = caPool
106
+	}
107
+
108
+	if caName, ok := ctx.Config[splunkCANameKey]; ok {
109
+		tlsConfig.ServerName = caName
110
+	}
111
+
112
+	transport := &http.Transport{
113
+		TLSClientConfig: tlsConfig,
114
+	}
115
+	client := &http.Client{
116
+		Transport: transport,
117
+	}
118
+
119
+	var nullMessage = &splunkMessage{
120
+		Host: hostname,
121
+	}
122
+
123
+	// Optional parameters for messages
124
+	nullMessage.Source = ctx.Config[splunkSourceKey]
125
+	nullMessage.SourceType = ctx.Config[splunkSourceTypeKey]
126
+	nullMessage.Index = ctx.Config[splunkIndexKey]
127
+
128
+	logger := &splunkLogger{
129
+		client:      client,
130
+		transport:   transport,
131
+		url:         splunkURL.String(),
132
+		auth:        "Splunk " + splunkToken,
133
+		nullMessage: nullMessage,
134
+	}
135
+
136
+	err = verifySplunkConnection(logger)
137
+	if err != nil {
138
+		return nil, err
139
+	}
140
+
141
+	return logger, nil
142
+}
143
+
144
+func (l *splunkLogger) Log(msg *logger.Message) error {
145
+	// Construct message as a copy of nullMessage
146
+	message := *l.nullMessage
147
+	message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/1000000000)
148
+	message.Event = splunkMessageEvent{
149
+		Line:        string(msg.Line),
150
+		ContainerID: msg.ContainerID,
151
+		Source:      msg.Source,
152
+	}
153
+
154
+	jsonEvent, err := json.Marshal(&message)
155
+	if err != nil {
156
+		return err
157
+	}
158
+	req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(jsonEvent))
159
+	if err != nil {
160
+		return err
161
+	}
162
+	req.Header.Set("Authorization", l.auth)
163
+	res, err := l.client.Do(req)
164
+	if err != nil {
165
+		return err
166
+	}
167
+	if res.Body != nil {
168
+		defer res.Body.Close()
169
+	}
170
+	if res.StatusCode != http.StatusOK {
171
+		var body []byte
172
+		body, err = ioutil.ReadAll(res.Body)
173
+		if err != nil {
174
+			return err
175
+		}
176
+		return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body)
177
+	}
178
+	io.Copy(ioutil.Discard, res.Body)
179
+	return nil
180
+}
181
+
182
+func (l *splunkLogger) Close() error {
183
+	l.transport.CloseIdleConnections()
184
+	return nil
185
+}
186
+
187
+func (l *splunkLogger) Name() string {
188
+	return driverName
189
+}
190
+
191
+// ValidateLogOpt looks for all supported by splunk driver options
192
+func ValidateLogOpt(cfg map[string]string) error {
193
+	for key := range cfg {
194
+		switch key {
195
+		case splunkURLKey:
196
+		case splunkTokenKey:
197
+		case splunkSourceKey:
198
+		case splunkSourceTypeKey:
199
+		case splunkIndexKey:
200
+		case splunkCAPathKey:
201
+		case splunkCANameKey:
202
+		case splunkInsecureSkipVerifyKey:
203
+		default:
204
+			return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName)
205
+		}
206
+	}
207
+	return nil
208
+}
209
+
210
+func parseURL(ctx logger.Context) (*url.URL, error) {
211
+	splunkURLStr, ok := ctx.Config[splunkURLKey]
212
+	if !ok {
213
+		return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey)
214
+	}
215
+
216
+	splunkURL, err := url.Parse(splunkURLStr)
217
+	if err != nil {
218
+		return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey)
219
+	}
220
+
221
+	if !urlutil.IsURL(splunkURLStr) ||
222
+		!splunkURL.IsAbs() ||
223
+		(splunkURL.Path != "" && splunkURL.Path != "/") ||
224
+		splunkURL.RawQuery != "" ||
225
+		splunkURL.Fragment != "" {
226
+		return nil, fmt.Errorf("%s: expected format schema://dns_name_or_ip:port for %s", driverName, splunkURLKey)
227
+	}
228
+
229
+	splunkURL.Path = "/services/collector/event/1.0"
230
+
231
+	return splunkURL, nil
232
+}
233
+
234
+func verifySplunkConnection(l *splunkLogger) error {
235
+	req, err := http.NewRequest("OPTIONS", l.url, nil)
236
+	if err != nil {
237
+		return err
238
+	}
239
+	res, err := l.client.Do(req)
240
+	if err != nil {
241
+		return err
242
+	}
243
+	if res.Body != nil {
244
+		defer res.Body.Close()
245
+	}
246
+	if res.StatusCode != http.StatusOK {
247
+		var body []byte
248
+		body, err = ioutil.ReadAll(res.Body)
249
+		if err != nil {
250
+			return err
251
+		}
252
+		return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body)
253
+	}
254
+	return nil
255
+}
... ...
@@ -309,7 +309,7 @@ Json Parameters:
309 309
         systems, such as SELinux.
310 310
     -   **LogConfig** - Log configuration for the container, specified as a JSON object in the form
311 311
           `{ "Type": "<driver_name>", "Config": {"key1": "val1"}}`.
312
-          Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `none`.
312
+          Available types: `json-file`, `syslog`, `journald`, `gelf`, `awslogs`, `splunk`, `none`.
313 313
           `json-file` logging driver.
314 314
     -   **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.
315 315
     -   **VolumeDriver** - Driver that this container users to mount volumes.
... ...
@@ -18,3 +18,4 @@ weight=8
18 18
 * [Fluentd logging driver](fluentd.md)
19 19
 * [Journald logging driver](journald.md)
20 20
 * [Amazon CloudWatch Logs logging driver](awslogs.md)
21
+* [Splunk logging driver](splunk.md)
... ...
@@ -24,6 +24,7 @@ container's logging driver. The following options are supported:
24 24
 | `gelf`      | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. |
25 25
 | `fluentd`   | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input).                                          |
26 26
 | `awslogs`   | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs.                              |
27
+| `splunk`    | Splunk logging driver for Docker. Writes log messages to `splunk` using HTTP Event Collector.                                 |
27 28
 
28 29
 The `docker logs`command is available only for the `json-file` logging driver.
29 30
 
... ...
@@ -172,3 +173,13 @@ The Amazon CloudWatch Logs logging driver supports the following options:
172 172
 
173 173
 
174 174
 For detailed information on working with this logging driver, see [the awslogs logging driver](awslogs.md) reference documentation.
175
+
176
+## Splunk options
177
+
178
+The Splunk logging driver requires the following options:
179
+
180
+    --log-opt splunk-token=<splunk_http_event_collector_token>
181
+    --log-opt splunk-url=https://your_splunk_instance:8088
182
+
183
+For detailed information about working with this logging driver, see the [Splunk logging driver](splunk.md)
184
+reference documentation.
175 185
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+<!--[metadata]>
1
+title = "Splunk logging driver"
2
+description = "Describes how to use the Splunk logging driver."
3
+keywords = ["splunk, docker, logging, driver"]
4
+[menu.main]
5
+parent = "smn_logging"
6
+weight = 2
7
+<![end-metadata]-->
8
+
9
+# Splunk logging driver
10
+
11
+The `splunk` logging driver sends container logs to
12
+[HTTP Event Collector](http://dev.splunk.com/view/event-collector/SP-CAAAE6M)
13
+in Splunk Enterprise and Splunk Cloud.
14
+
15
+## Usage
16
+
17
+You can configure the default logging driver by passing the `--log-driver`
18
+option to the Docker daemon:
19
+
20
+    docker --log-driver=splunk
21
+
22
+You can set the logging driver for a specific container by using the
23
+`--log-driver` option to `docker run`:
24
+
25
+    docker run --log-driver=splunk ...
26
+
27
+## Splunk options
28
+
29
+You can use the `--log-opt NAME=VALUE` flag to specify these additional Splunk
30
+logging driver options:
31
+
32
+  - `splunk-token` required, Splunk HTTP Event Collector token
33
+  - `splunk-url` required, path to your Splunk Enterprise or Splunk Cloud instance
34
+      (including port and schema used by HTTP Event Collector) `https://your_splunk_instance:8088`
35
+  - `splunk-source` optional, event source
36
+  - `splunk-sourcetype` optional, event source type
37
+  - `splunk-index` optional, event index
38
+  - `splunk-capath` optional, path to root certificate
39
+  - `splunk-caname` optional, name to use for validating server
40
+      certificate; by default the hostname of the `splunk-url` will be used
41
+  - `splunk-insecureskipverify` optional, ignore server certificate validation
42
+
43
+Below is an example of the logging option specified for the Splunk Enterprise
44
+instance. The instance is installed locally on the same machine on which the
45
+Docker daemon is running. The path to the root certificate and Common Name is
46
+specified using an HTTPS schema. This is used for verification.
47
+The `SplunkServerDefaultCert` is automatically generated by Splunk certificates.
48
+
49
+    docker run --log-driver=splunk \
50
+        --log-opt splunk-token=176FCEBF-4CF5-4EDF-91BC-703796522D20 \
51
+        --log-opt splunk-url=https://localhost:8088 \
52
+        --log-opt splunk-capath=/opt/splunk/etc/auth/cacert.pem \
53
+        --log-opt splunk-caname=SplunkServerDefaultCert
... ...
@@ -1071,6 +1071,7 @@ container's logging driver. The following options are supported:
1071 1071
 | `gelf`      | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. |
1072 1072
 | `fluentd`   | Fluentd logging driver for Docker. Writes log messages to `fluentd` (forward input).                                          |
1073 1073
 | `awslogs`   | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs                               |
1074
+| `splunk`    | Splunk logging driver for Docker. Writes log messages to `splunk` using Event Http Collector.                                 |
1074 1075
 
1075 1076
 The `docker logs` command is available only for the `json-file` and `journald`
1076 1077
 logging drivers.  For detailed information on working with logging drivers, see
... ...
@@ -174,7 +174,7 @@ millions of trillions.
174 174
    Add link to another container in the form of <name or id>:alias or just
175 175
    <name or id> in which case the alias will match the name.
176 176
 
177
-**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
177
+**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*none*"
178 178
   Logging driver for container. Default is defined by daemon `--log-driver` flag.
179 179
   **Warning**: the `docker logs` command works only for the `json-file` and
180 180
   `journald` logging drivers.
... ...
@@ -277,7 +277,7 @@ which interface and port to use.
277 277
 **--lxc-conf**=[]
278 278
    (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"
279 279
 
280
-**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*none*"
280
+**--log-driver**="|*json-file*|*syslog*|*journald*|*gelf*|*fluentd*|*awslogs*|*splunk*|*none*"
281 281
   Logging driver for container. Default is defined by daemon `--log-driver` flag.
282 282
   **Warning**: the `docker logs` command works only for the `json-file` and
283 283
   `journald` logging drivers.