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 }