github.com/walkingsparrow/docker@v1.4.2-0.20151218153551-b708a2249bfa/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 if res.Body != nil { 178 defer res.Body.Close() 179 } 180 if res.StatusCode != http.StatusOK { 181 var body []byte 182 body, err = ioutil.ReadAll(res.Body) 183 if err != nil { 184 return err 185 } 186 return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body) 187 } 188 io.Copy(ioutil.Discard, res.Body) 189 return nil 190 } 191 192 func (l *splunkLogger) Close() error { 193 l.transport.CloseIdleConnections() 194 return nil 195 } 196 197 func (l *splunkLogger) Name() string { 198 return driverName 199 } 200 201 // ValidateLogOpt looks for all supported by splunk driver options 202 func ValidateLogOpt(cfg map[string]string) error { 203 for key := range cfg { 204 switch key { 205 case splunkURLKey: 206 case splunkTokenKey: 207 case splunkSourceKey: 208 case splunkSourceTypeKey: 209 case splunkIndexKey: 210 case splunkCAPathKey: 211 case splunkCANameKey: 212 case splunkInsecureSkipVerifyKey: 213 case envKey: 214 case labelsKey: 215 case tagKey: 216 default: 217 return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName) 218 } 219 } 220 return nil 221 } 222 223 func parseURL(ctx logger.Context) (*url.URL, error) { 224 splunkURLStr, ok := ctx.Config[splunkURLKey] 225 if !ok { 226 return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey) 227 } 228 229 splunkURL, err := url.Parse(splunkURLStr) 230 if err != nil { 231 return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey) 232 } 233 234 if !urlutil.IsURL(splunkURLStr) || 235 !splunkURL.IsAbs() || 236 (splunkURL.Path != "" && splunkURL.Path != "/") || 237 splunkURL.RawQuery != "" || 238 splunkURL.Fragment != "" { 239 return nil, fmt.Errorf("%s: expected format schema://dns_name_or_ip:port for %s", driverName, splunkURLKey) 240 } 241 242 splunkURL.Path = "/services/collector/event/1.0" 243 244 return splunkURL, nil 245 } 246 247 func verifySplunkConnection(l *splunkLogger) error { 248 req, err := http.NewRequest("OPTIONS", l.url, nil) 249 if err != nil { 250 return err 251 } 252 res, err := l.client.Do(req) 253 if err != nil { 254 return err 255 } 256 if res.Body != nil { 257 defer res.Body.Close() 258 } 259 if res.StatusCode != http.StatusOK { 260 var body []byte 261 body, err = ioutil.ReadAll(res.Body) 262 if err != nil { 263 return err 264 } 265 return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body) 266 } 267 return nil 268 }