splunk.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. // Package splunk provides the log driver for forwarding server logs to
  2. // Splunk HTTP Event Collector endpoint.
  3. package splunk
  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. "github.com/Sirupsen/logrus"
  16. "github.com/docker/docker/daemon/logger"
  17. "github.com/docker/docker/daemon/logger/loggerutils"
  18. "github.com/docker/docker/pkg/urlutil"
  19. )
  20. const (
  21. driverName = "splunk"
  22. splunkURLKey = "splunk-url"
  23. splunkTokenKey = "splunk-token"
  24. splunkSourceKey = "splunk-source"
  25. splunkSourceTypeKey = "splunk-sourcetype"
  26. splunkIndexKey = "splunk-index"
  27. splunkCAPathKey = "splunk-capath"
  28. splunkCANameKey = "splunk-caname"
  29. splunkInsecureSkipVerifyKey = "splunk-insecureskipverify"
  30. envKey = "env"
  31. labelsKey = "labels"
  32. tagKey = "tag"
  33. )
  34. type splunkLogger struct {
  35. client *http.Client
  36. transport *http.Transport
  37. url string
  38. auth string
  39. nullMessage *splunkMessage
  40. }
  41. type splunkMessage struct {
  42. Event splunkMessageEvent `json:"event"`
  43. Time string `json:"time"`
  44. Host string `json:"host"`
  45. Source string `json:"source,omitempty"`
  46. SourceType string `json:"sourcetype,omitempty"`
  47. Index string `json:"index,omitempty"`
  48. }
  49. type splunkMessageEvent struct {
  50. Line string `json:"line"`
  51. Source string `json:"source"`
  52. Tag string `json:"tag,omitempty"`
  53. Attrs map[string]string `json:"attrs,omitempty"`
  54. }
  55. func init() {
  56. if err := logger.RegisterLogDriver(driverName, New); err != nil {
  57. logrus.Fatal(err)
  58. }
  59. if err := logger.RegisterLogOptValidator(driverName, ValidateLogOpt); err != nil {
  60. logrus.Fatal(err)
  61. }
  62. }
  63. // New creates splunk logger driver using configuration passed in context
  64. func New(ctx logger.Context) (logger.Logger, error) {
  65. hostname, err := ctx.Hostname()
  66. if err != nil {
  67. return nil, fmt.Errorf("%s: cannot access hostname to set source field", driverName)
  68. }
  69. // Parse and validate Splunk URL
  70. splunkURL, err := parseURL(ctx)
  71. if err != nil {
  72. return nil, err
  73. }
  74. // Splunk Token is required parameter
  75. splunkToken, ok := ctx.Config[splunkTokenKey]
  76. if !ok {
  77. return nil, fmt.Errorf("%s: %s is expected", driverName, splunkTokenKey)
  78. }
  79. tlsConfig := &tls.Config{}
  80. // Splunk is using autogenerated certificates by default,
  81. // allow users to trust them with skiping verification
  82. if insecureSkipVerifyStr, ok := ctx.Config[splunkInsecureSkipVerifyKey]; ok {
  83. insecureSkipVerify, err := strconv.ParseBool(insecureSkipVerifyStr)
  84. if err != nil {
  85. return nil, err
  86. }
  87. tlsConfig.InsecureSkipVerify = insecureSkipVerify
  88. }
  89. // If path to the root certificate is provided - load it
  90. if caPath, ok := ctx.Config[splunkCAPathKey]; ok {
  91. caCert, err := ioutil.ReadFile(caPath)
  92. if err != nil {
  93. return nil, err
  94. }
  95. caPool := x509.NewCertPool()
  96. caPool.AppendCertsFromPEM(caCert)
  97. tlsConfig.RootCAs = caPool
  98. }
  99. if caName, ok := ctx.Config[splunkCANameKey]; ok {
  100. tlsConfig.ServerName = caName
  101. }
  102. transport := &http.Transport{
  103. TLSClientConfig: tlsConfig,
  104. }
  105. client := &http.Client{
  106. Transport: transport,
  107. }
  108. var nullMessage = &splunkMessage{
  109. Host: hostname,
  110. }
  111. // Optional parameters for messages
  112. nullMessage.Source = ctx.Config[splunkSourceKey]
  113. nullMessage.SourceType = ctx.Config[splunkSourceTypeKey]
  114. nullMessage.Index = ctx.Config[splunkIndexKey]
  115. tag, err := loggerutils.ParseLogTag(ctx, "{{.ID}}")
  116. if err != nil {
  117. return nil, err
  118. }
  119. nullMessage.Event.Tag = tag
  120. nullMessage.Event.Attrs = ctx.ExtraAttributes(nil)
  121. logger := &splunkLogger{
  122. client: client,
  123. transport: transport,
  124. url: splunkURL.String(),
  125. auth: "Splunk " + splunkToken,
  126. nullMessage: nullMessage,
  127. }
  128. err = verifySplunkConnection(logger)
  129. if err != nil {
  130. return nil, err
  131. }
  132. return logger, nil
  133. }
  134. func (l *splunkLogger) Log(msg *logger.Message) error {
  135. // Construct message as a copy of nullMessage
  136. message := *l.nullMessage
  137. message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/1000000000)
  138. message.Event.Line = string(msg.Line)
  139. message.Event.Source = msg.Source
  140. jsonEvent, err := json.Marshal(&message)
  141. if err != nil {
  142. return err
  143. }
  144. req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(jsonEvent))
  145. if err != nil {
  146. return err
  147. }
  148. req.Header.Set("Authorization", l.auth)
  149. res, err := l.client.Do(req)
  150. if err != nil {
  151. return err
  152. }
  153. if res.Body != nil {
  154. defer res.Body.Close()
  155. }
  156. if res.StatusCode != http.StatusOK {
  157. var body []byte
  158. body, err = ioutil.ReadAll(res.Body)
  159. if err != nil {
  160. return err
  161. }
  162. return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body)
  163. }
  164. io.Copy(ioutil.Discard, res.Body)
  165. return nil
  166. }
  167. func (l *splunkLogger) Close() error {
  168. l.transport.CloseIdleConnections()
  169. return nil
  170. }
  171. func (l *splunkLogger) Name() string {
  172. return driverName
  173. }
  174. // ValidateLogOpt looks for all supported by splunk driver options
  175. func ValidateLogOpt(cfg map[string]string) error {
  176. for key := range cfg {
  177. switch key {
  178. case splunkURLKey:
  179. case splunkTokenKey:
  180. case splunkSourceKey:
  181. case splunkSourceTypeKey:
  182. case splunkIndexKey:
  183. case splunkCAPathKey:
  184. case splunkCANameKey:
  185. case splunkInsecureSkipVerifyKey:
  186. case envKey:
  187. case labelsKey:
  188. case tagKey:
  189. default:
  190. return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName)
  191. }
  192. }
  193. return nil
  194. }
  195. func parseURL(ctx logger.Context) (*url.URL, error) {
  196. splunkURLStr, ok := ctx.Config[splunkURLKey]
  197. if !ok {
  198. return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey)
  199. }
  200. splunkURL, err := url.Parse(splunkURLStr)
  201. if err != nil {
  202. return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey)
  203. }
  204. if !urlutil.IsURL(splunkURLStr) ||
  205. !splunkURL.IsAbs() ||
  206. (splunkURL.Path != "" && splunkURL.Path != "/") ||
  207. splunkURL.RawQuery != "" ||
  208. splunkURL.Fragment != "" {
  209. return nil, fmt.Errorf("%s: expected format schema://dns_name_or_ip:port for %s", driverName, splunkURLKey)
  210. }
  211. splunkURL.Path = "/services/collector/event/1.0"
  212. return splunkURL, nil
  213. }
  214. func verifySplunkConnection(l *splunkLogger) error {
  215. req, err := http.NewRequest("OPTIONS", l.url, nil)
  216. if err != nil {
  217. return err
  218. }
  219. res, err := l.client.Do(req)
  220. if err != nil {
  221. return err
  222. }
  223. if res.Body != nil {
  224. defer res.Body.Close()
  225. }
  226. if res.StatusCode != http.StatusOK {
  227. var body []byte
  228. body, err = ioutil.ReadAll(res.Body)
  229. if err != nil {
  230. return err
  231. }
  232. return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body)
  233. }
  234. return nil
  235. }