splunk.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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 skipping 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, loggerutils.DefaultTemplate)
  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. defer res.Body.Close()
  154. if res.StatusCode != http.StatusOK {
  155. var body []byte
  156. body, err = ioutil.ReadAll(res.Body)
  157. if err != nil {
  158. return err
  159. }
  160. return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body)
  161. }
  162. io.Copy(ioutil.Discard, res.Body)
  163. return nil
  164. }
  165. func (l *splunkLogger) Close() error {
  166. l.transport.CloseIdleConnections()
  167. return nil
  168. }
  169. func (l *splunkLogger) Name() string {
  170. return driverName
  171. }
  172. // ValidateLogOpt looks for all supported by splunk driver options
  173. func ValidateLogOpt(cfg map[string]string) error {
  174. for key := range cfg {
  175. switch key {
  176. case splunkURLKey:
  177. case splunkTokenKey:
  178. case splunkSourceKey:
  179. case splunkSourceTypeKey:
  180. case splunkIndexKey:
  181. case splunkCAPathKey:
  182. case splunkCANameKey:
  183. case splunkInsecureSkipVerifyKey:
  184. case envKey:
  185. case labelsKey:
  186. case tagKey:
  187. default:
  188. return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName)
  189. }
  190. }
  191. return nil
  192. }
  193. func parseURL(ctx logger.Context) (*url.URL, error) {
  194. splunkURLStr, ok := ctx.Config[splunkURLKey]
  195. if !ok {
  196. return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey)
  197. }
  198. splunkURL, err := url.Parse(splunkURLStr)
  199. if err != nil {
  200. return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey)
  201. }
  202. if !urlutil.IsURL(splunkURLStr) ||
  203. !splunkURL.IsAbs() ||
  204. (splunkURL.Path != "" && splunkURL.Path != "/") ||
  205. splunkURL.RawQuery != "" ||
  206. splunkURL.Fragment != "" {
  207. return nil, fmt.Errorf("%s: expected format scheme://dns_name_or_ip:port for %s", driverName, splunkURLKey)
  208. }
  209. splunkURL.Path = "/services/collector/event/1.0"
  210. return splunkURL, nil
  211. }
  212. func verifySplunkConnection(l *splunkLogger) error {
  213. req, err := http.NewRequest("OPTIONS", l.url, nil)
  214. if err != nil {
  215. return err
  216. }
  217. res, err := l.client.Do(req)
  218. if err != nil {
  219. return err
  220. }
  221. if res.Body != nil {
  222. defer res.Body.Close()
  223. }
  224. if res.StatusCode != http.StatusOK {
  225. var body []byte
  226. body, err = ioutil.ReadAll(res.Body)
  227. if err != nil {
  228. return err
  229. }
  230. return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body)
  231. }
  232. return nil
  233. }