Browse code

Splunk Logging Driver: formats and verifyconnection

`--log-opt splunk-format=inline|json|raw` allows to change how logging
driver sends data to Splunk, where

`inline` - default value, format used before, message is injected as a
line in JSON payload
`json` - driver will try to parse each line as a JSON object and embed it
inside of the JSON payload
`raw` - driver will send Raw payload instead of JSON, tag and attributes
will be prefixed before the message

`--log-opt splunk-verify-connection=true|false` - allows to skip
verification for Splunk Url

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

Denis Gladkikh authored on 2016/06/10 13:06:44
Showing 4 changed files
... ...
@@ -520,7 +520,7 @@ __docker_complete_log_options() {
520 520
 	local journald_options="env labels tag"
521 521
 	local json_file_options="env labels max-file max-size"
522 522
 	local syslog_options="env labels syslog-address syslog-facility syslog-format syslog-tls-ca-cert syslog-tls-cert syslog-tls-key syslog-tls-skip-verify tag"
523
-	local splunk_options="env labels splunk-caname splunk-capath splunk-index splunk-insecureskipverify splunk-source splunk-sourcetype splunk-token splunk-url tag"
523
+	local splunk_options="env labels splunk-caname splunk-capath splunk-format splunk-index splunk-insecureskipverify splunk-source splunk-sourcetype splunk-token splunk-url splunk-verify-connection tag"
524 524
 
525 525
 	local all_options="$fluentd_options $gcplogs_options $gelf_options $journald_options $json_file_options $syslog_options $splunk_options"
526 526
 
... ...
@@ -629,10 +629,14 @@ __docker_complete_log_driver_options() {
629 629
 			__ltrim_colon_completions "${cur}"
630 630
 			return
631 631
 			;;
632
-		splunk-insecureskipverify)
632
+		splunk-insecureskipverify|splunk-verify-connection)
633 633
 			COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) )
634 634
 			return
635 635
 			;;
636
+		splunk-format)
637
+			COMPREPLY=( $( compgen -W "inline json raw" -- "${cur##*=}" ) )
638
+			return
639
+			;;
636 640
 	esac
637 641
 	return 1
638 642
 }
... ...
@@ -228,7 +228,7 @@ __docker_get_log_options() {
228 228
     journald_options=("env" "labels" "tag")
229 229
     json_file_options=("env" "labels" "max-file" "max-size")
230 230
     syslog_options=("env" "labels" "syslog-address" "syslog-facility" "syslog-format" "syslog-tls-ca-cert" "syslog-tls-cert" "syslog-tls-key" "syslog-tls-skip-verify" "tag")
231
-    splunk_options=("env" "labels" "splunk-caname" "splunk-capath" "splunk-index" "splunk-insecureskipverify" "splunk-source" "splunk-sourcetype" "splunk-token" "splunk-url" "tag")
231
+    splunk_options=("env" "labels" "splunk-caname" "splunk-capath" "splunk-format" "splunk-index" "splunk-insecureskipverify" "splunk-source" "splunk-sourcetype" "splunk-token" "splunk-url" "splunk-verify-connection" "tag")
232 232
 
233 233
     [[ $log_driver = (awslogs|all) ]] && _describe -t awslogs-options "awslogs options" awslogs_options "$@" && ret=0
234 234
     [[ $log_driver = (fluentd|all) ]] && _describe -t fluentd-options "fluentd options" fluentd_options "$@" && ret=0
... ...
@@ -30,6 +30,8 @@ const (
30 30
 	splunkCAPathKey             = "splunk-capath"
31 31
 	splunkCANameKey             = "splunk-caname"
32 32
 	splunkInsecureSkipVerifyKey = "splunk-insecureskipverify"
33
+	splunkFormatKey             = "splunk-format"
34
+	splunkVerifyConnectionKey   = "splunk-verify-connection"
33 35
 	envKey                      = "env"
34 36
 	labelsKey                   = "labels"
35 37
 	tagKey                      = "tag"
... ...
@@ -44,22 +46,44 @@ type splunkLogger struct {
44 44
 	nullMessage *splunkMessage
45 45
 }
46 46
 
47
+type splunkLoggerInline struct {
48
+	splunkLogger
49
+
50
+	nullEvent *splunkMessageEvent
51
+}
52
+
53
+type splunkLoggerJSON struct {
54
+	splunkLoggerInline
55
+}
56
+
57
+type splunkLoggerRaw struct {
58
+	splunkLogger
59
+
60
+	prefix []byte
61
+}
62
+
47 63
 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"`
64
+	Event      interface{} `json:"event"`
65
+	Time       string      `json:"time"`
66
+	Host       string      `json:"host"`
67
+	Source     string      `json:"source,omitempty"`
68
+	SourceType string      `json:"sourcetype,omitempty"`
69
+	Index      string      `json:"index,omitempty"`
54 70
 }
55 71
 
56 72
 type splunkMessageEvent struct {
57
-	Line   string            `json:"line"`
73
+	Line   interface{}       `json:"line"`
58 74
 	Source string            `json:"source"`
59 75
 	Tag    string            `json:"tag,omitempty"`
60 76
 	Attrs  map[string]string `json:"attrs,omitempty"`
61 77
 }
62 78
 
79
+const (
80
+	splunkFormatRaw    = "raw"
81
+	splunkFormatJSON   = "json"
82
+	splunkFormatInline = "inline"
83
+)
84
+
63 85
 func init() {
64 86
 	if err := logger.RegisterLogDriver(driverName, New); err != nil {
65 87
 		logrus.Fatal(err)
... ...
@@ -122,21 +146,23 @@ func New(ctx logger.Context) (logger.Logger, error) {
122 122
 		Transport: transport,
123 123
 	}
124 124
 
125
+	source := ctx.Config[splunkSourceKey]
126
+	sourceType := ctx.Config[splunkSourceTypeKey]
127
+	index := ctx.Config[splunkIndexKey]
128
+
125 129
 	var nullMessage = &splunkMessage{
126
-		Host: hostname,
130
+		Host:       hostname,
131
+		Source:     source,
132
+		SourceType: sourceType,
133
+		Index:      index,
127 134
 	}
128 135
 
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 136
 	tag, err := loggerutils.ParseLogTag(ctx, loggerutils.DefaultTemplate)
135 137
 	if err != nil {
136 138
 		return nil, err
137 139
 	}
138
-	nullMessage.Event.Tag = tag
139
-	nullMessage.Event.Attrs = ctx.ExtraAttributes(nil)
140
+
141
+	attrs := ctx.ExtraAttributes(nil)
140 142
 
141 143
 	logger := &splunkLogger{
142 144
 		client:      client,
... ...
@@ -146,21 +172,107 @@ func New(ctx logger.Context) (logger.Logger, error) {
146 146
 		nullMessage: nullMessage,
147 147
 	}
148 148
 
149
-	err = verifySplunkConnection(logger)
150
-	if err != nil {
151
-		return nil, err
149
+	// By default we verify connection, but we allow use to skip that
150
+	verifyConnection := true
151
+	if verifyConnectionStr, ok := ctx.Config[splunkVerifyConnectionKey]; ok {
152
+		var err error
153
+		verifyConnection, err = strconv.ParseBool(verifyConnectionStr)
154
+		if err != nil {
155
+			return nil, err
156
+		}
157
+	}
158
+	if verifyConnection {
159
+		err = verifySplunkConnection(logger)
160
+		if err != nil {
161
+			return nil, err
162
+		}
152 163
 	}
153 164
 
154
-	return logger, nil
165
+	var splunkFormat string
166
+	if splunkFormatParsed, ok := ctx.Config[splunkFormatKey]; ok {
167
+		switch splunkFormatParsed {
168
+		case splunkFormatInline:
169
+		case splunkFormatJSON:
170
+		case splunkFormatRaw:
171
+		default:
172
+			return nil, fmt.Errorf("Unknown format specified %s, supported formats are inline, json and raw", splunkFormat)
173
+		}
174
+		splunkFormat = splunkFormatParsed
175
+	} else {
176
+		splunkFormat = splunkFormatInline
177
+	}
178
+
179
+	switch splunkFormat {
180
+	case splunkFormatInline:
181
+		nullEvent := &splunkMessageEvent{
182
+			Tag:   tag,
183
+			Attrs: attrs,
184
+		}
185
+
186
+		return &splunkLoggerInline{*logger, nullEvent}, nil
187
+	case splunkFormatJSON:
188
+		nullEvent := &splunkMessageEvent{
189
+			Tag:   tag,
190
+			Attrs: attrs,
191
+		}
192
+
193
+		return &splunkLoggerJSON{splunkLoggerInline{*logger, nullEvent}}, nil
194
+	case splunkFormatRaw:
195
+		var prefix bytes.Buffer
196
+		prefix.WriteString(tag)
197
+		prefix.WriteString(" ")
198
+		for key, value := range attrs {
199
+			prefix.WriteString(key)
200
+			prefix.WriteString("=")
201
+			prefix.WriteString(value)
202
+			prefix.WriteString(" ")
203
+		}
204
+
205
+		return &splunkLoggerRaw{*logger, prefix.Bytes()}, nil
206
+	default:
207
+		return nil, fmt.Errorf("Unexpected format %s", splunkFormat)
208
+	}
155 209
 }
156 210
 
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
211
+func (l *splunkLoggerInline) Log(msg *logger.Message) error {
212
+	message := l.createSplunkMessage(msg)
213
+
214
+	event := *l.nullEvent
215
+	event.Line = string(msg.Line)
216
+	event.Source = msg.Source
163 217
 
218
+	message.Event = &event
219
+
220
+	return l.postMessage(&message)
221
+}
222
+
223
+func (l *splunkLoggerJSON) Log(msg *logger.Message) error {
224
+	message := l.createSplunkMessage(msg)
225
+	event := *l.nullEvent
226
+
227
+	var rawJSONMessage json.RawMessage
228
+	if err := json.Unmarshal(msg.Line, &rawJSONMessage); err == nil {
229
+		event.Line = &rawJSONMessage
230
+	} else {
231
+		event.Line = string(msg.Line)
232
+	}
233
+
234
+	event.Source = msg.Source
235
+
236
+	message.Event = &event
237
+
238
+	return l.postMessage(&message)
239
+}
240
+
241
+func (l *splunkLoggerRaw) Log(msg *logger.Message) error {
242
+	message := l.createSplunkMessage(msg)
243
+
244
+	message.Event = string(append(l.prefix, msg.Line...))
245
+
246
+	return l.postMessage(&message)
247
+}
248
+
249
+func (l *splunkLogger) postMessage(message *splunkMessage) error {
164 250
 	jsonEvent, err := json.Marshal(&message)
165 251
 	if err != nil {
166 252
 		return err
... ...
@@ -196,6 +308,12 @@ func (l *splunkLogger) Name() string {
196 196
 	return driverName
197 197
 }
198 198
 
199
+func (l *splunkLogger) createSplunkMessage(msg *logger.Message) splunkMessage {
200
+	message := *l.nullMessage
201
+	message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/1000000000)
202
+	return message
203
+}
204
+
199 205
 // ValidateLogOpt looks for all supported by splunk driver options
200 206
 func ValidateLogOpt(cfg map[string]string) error {
201 207
 	for key := range cfg {
... ...
@@ -208,6 +326,8 @@ func ValidateLogOpt(cfg map[string]string) error {
208 208
 		case splunkCAPathKey:
209 209
 		case splunkCANameKey:
210 210
 		case splunkInsecureSkipVerifyKey:
211
+		case splunkFormatKey:
212
+		case splunkVerifyConnectionKey:
211 213
 		case envKey:
212 214
 		case labelsKey:
213 215
 		case tagKey:
... ...
@@ -42,6 +42,8 @@ logging driver options:
42 42
 | `splunk-capath`             | optional | Path to root certificate.                                                                                                                                                                                          |
43 43
 | `splunk-caname`             | optional | Name to use for validating server certificate; by default the hostname of the `splunk-url` will be used.                                                                                                           |
44 44
 | `splunk-insecureskipverify` | optional | Ignore server certificate validation.                                                                                                                                                                              |
45
+| `splunk-format`             | optional | Message format. Can be `inline`, `json` or `raw`. Defaults to `inline`.                                                                                                                                            |
46
+| `splunk-verify-connection`   | optional | Verify on start, that docker can connect to Splunk server. Defaults to true.                                                                                                                                       |
45 47
 | `tag`                       | optional | Specify tag for message, which interpret some markup. Default value is `{{.ID}}` (12 characters of the container ID). Refer to the [log tag option documentation](log_tags.md) for customizing the log tag format. |
46 48
 | `labels`                    | optional | Comma-separated list of keys of labels, which should be included in message, if these labels are specified for container.                                                                                          |
47 49
 | `env`                       | optional | Comma-separated list of keys of environment variables, which should be included in message, if these variables are specified for container.                                                                        |
... ...
@@ -66,3 +68,67 @@ The `SplunkServerDefaultCert` is automatically generated by Splunk certificates.
66 66
         --env "TEST=false"
67 67
         --label location=west
68 68
         your/application
69
+
70
+### Message formats
71
+
72
+By default Logging Driver sends messages as `inline` format, where each message
73
+will be embedded as a string, for example
74
+
75
+```
76
+{
77
+    "attrs": {
78
+        "env1": "val1",
79
+        "label1": "label1"
80
+    },
81
+    "tag": "MyImage/MyContainer",
82
+    "source":  "stdout",
83
+    "line": "my message"
84
+}
85
+{
86
+    "attrs": {
87
+        "env1": "val1",
88
+        "label1": "label1"
89
+    },
90
+    "tag": "MyImage/MyContainer",
91
+    "source":  "stdout",
92
+    "line": "{\"foo\": \"bar\"}"
93
+}
94
+```
95
+
96
+In case if your messages are JSON objects you may want to embed them in the
97
+message we send to Splunk. By specifying `--log-opt splunk-format=json` driver
98
+will try to parse every line as a JSON object and send it as embedded object. In
99
+case if it cannot parse it - message will be send as `inline`. For example
100
+
101
+
102
+```
103
+{
104
+    "attrs": {
105
+        "env1": "val1",
106
+        "label1": "label1"
107
+    },
108
+    "tag": "MyImage/MyContainer",
109
+    "source":  "stdout",
110
+    "line": "my message"
111
+}
112
+{
113
+    "attrs": {
114
+        "env1": "val1",
115
+        "label1": "label1"
116
+    },
117
+    "tag": "MyImage/MyContainer",
118
+    "source":  "stdout",
119
+    "line": {
120
+        "foo": "bar"
121
+    }
122
+}
123
+```
124
+
125
+Third format is a `raw` message. You can specify it by using
126
+`--log-opt splunk-format=raw`. Attributes (environment variables and labels) and
127
+tag will be prefixed to the message. For example
128
+
129
+```
130
+MyImage/MyContainer env1=val1 label1=label1 my message
131
+MyImage/MyContainer env1=val1 label1=label1 {"foo": "bar"}
132
+```