mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
log,internal/debug: Add Splunk logging capability
Adds the ability to log directly to Splunk using the HTTP Event Collector (HEC). Some of the code is taken after github.com/ZachtimusPrime/Go-Splunk-HTTP, but has been patched and edited to support this use case best.
This commit is contained in:
parent
ca22d0761b
commit
d8d001e896
5 changed files with 415 additions and 1 deletions
|
|
@ -86,13 +86,40 @@ var (
|
||||||
Name: "trace",
|
Name: "trace",
|
||||||
Usage: "Write execution trace to the given file",
|
Usage: "Write execution trace to the given file",
|
||||||
}
|
}
|
||||||
|
splunkurlFlag = cli.StringFlag{
|
||||||
|
Name: "splunkurl",
|
||||||
|
Usage: "URL to Splunk HTTP Event Collector",
|
||||||
|
}
|
||||||
|
splunktokenFlag = cli.StringFlag{
|
||||||
|
Name: "splunktoken",
|
||||||
|
Usage: "Splunk HTTP Event Collector token",
|
||||||
|
}
|
||||||
|
splunksourceFlag = cli.StringFlag{
|
||||||
|
Name: "splunksource",
|
||||||
|
Usage: "Splunk source field value, description of the source of the event",
|
||||||
|
Value: "geth",
|
||||||
|
}
|
||||||
|
splunkindexFlag = cli.StringFlag{
|
||||||
|
Name: "splunkindex",
|
||||||
|
Usage: "Splunk Index, optional name of the Splunk index to store the event in",
|
||||||
|
}
|
||||||
|
splunksourcetypeFlag = cli.StringFlag{
|
||||||
|
Name: "splunksourcetype",
|
||||||
|
Usage: "Splunk source type, optional name of a sourcetype field value",
|
||||||
|
}
|
||||||
|
splunkskiptlsverifyFlag = cli.BoolFlag{
|
||||||
|
Name: "splunkskiptlsverify",
|
||||||
|
Usage: "Skip verifying the certificate of the HTTP Event Collector",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flags holds all command-line flags required for debugging.
|
// Flags holds all command-line flags required for debugging.
|
||||||
var Flags = []cli.Flag{
|
var Flags = []cli.Flag{
|
||||||
verbosityFlag, vmoduleFlag, backtraceAtFlag, debugFlag,
|
verbosityFlag, vmoduleFlag, backtraceAtFlag, debugFlag,
|
||||||
pprofFlag, pprofAddrFlag, pprofPortFlag,
|
pprofFlag, pprofAddrFlag, pprofPortFlag,
|
||||||
memprofilerateFlag, blockprofilerateFlag, cpuprofileFlag, traceFlag,
|
memprofilerateFlag, blockprofilerateFlag, cpuprofileFlag,
|
||||||
|
traceFlag, splunkurlFlag, splunktokenFlag, splunksourceFlag,
|
||||||
|
splunkindexFlag, splunksourcetypeFlag, splunkskiptlsverifyFlag,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -114,6 +141,24 @@ func init() {
|
||||||
// It should be called as early as possible in the program.
|
// It should be called as early as possible in the program.
|
||||||
func Setup(ctx *cli.Context) error {
|
func Setup(ctx *cli.Context) error {
|
||||||
// logging
|
// logging
|
||||||
|
splunkURL := ctx.GlobalString(splunkurlFlag.Name)
|
||||||
|
if splunkURL != "" {
|
||||||
|
splunkToken := ctx.GlobalString(splunktokenFlag.Name)
|
||||||
|
splunkSource := ctx.GlobalString(splunksourceFlag.Name)
|
||||||
|
splunkSourceType := ctx.GlobalString(splunksourcetypeFlag.Name)
|
||||||
|
splunkIndex := ctx.GlobalString(splunkindexFlag.Name)
|
||||||
|
splunkSkipTLSVerify := ctx.GlobalBool(splunkskiptlsverifyFlag.Name)
|
||||||
|
originalHandler := log.NewGlogHandler(ostream)
|
||||||
|
originalHandler.Verbosity(log.Lvl(ctx.GlobalInt(verbosityFlag.Name)))
|
||||||
|
originalHandler.Vmodule(ctx.GlobalString(vmoduleFlag.Name))
|
||||||
|
originalHandler.BacktraceAt(ctx.GlobalString(backtraceAtFlag.Name))
|
||||||
|
splunkstream, err := log.SplunkHandler(splunkURL, splunkToken, splunkSource, splunkSourceType, splunkIndex,
|
||||||
|
splunkSkipTLSVerify, log.TerminalFormat(false), originalHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
glogger.SetHandler(splunkstream)
|
||||||
|
}
|
||||||
log.PrintOrigins(ctx.GlobalBool(debugFlag.Name))
|
log.PrintOrigins(ctx.GlobalBool(debugFlag.Name))
|
||||||
glogger.Verbosity(log.Lvl(ctx.GlobalInt(verbosityFlag.Name)))
|
glogger.Verbosity(log.Lvl(ctx.GlobalInt(verbosityFlag.Name)))
|
||||||
glogger.Vmodule(ctx.GlobalString(vmoduleFlag.Name))
|
glogger.Vmodule(ctx.GlobalString(vmoduleFlag.Name))
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log/splunk"
|
||||||
"github.com/go-stack/stack"
|
"github.com/go-stack/stack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -81,6 +85,34 @@ func NetHandler(network, addr string, fmtr Format) (Handler, error) {
|
||||||
return closingHandler{conn, StreamHandler(conn, fmtr)}, nil
|
return closingHandler{conn, StreamHandler(conn, fmtr)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SplunkHandler posts data to Splunk HTTP Event Collector.
|
||||||
|
func SplunkHandler(url, token, source, sourcetype, index string, skipTLSVerify bool, fmtr Format,
|
||||||
|
originalHandler Handler) (Handler, error) {
|
||||||
|
originalLogger := logger{[]interface{}{}, new(swapHandler)}
|
||||||
|
originalLogger.SetHandler(originalHandler)
|
||||||
|
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLSVerify}}
|
||||||
|
httpClient := &http.Client{Timeout: time.Second * 20, Transport: tr}
|
||||||
|
splunkClient := splunk.NewClient(
|
||||||
|
httpClient,
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
source,
|
||||||
|
sourcetype,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
writer := &splunk.Writer{
|
||||||
|
Client: splunkClient,
|
||||||
|
MaxRetries: 2,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
err := <-writer.Errors()
|
||||||
|
originalLogger.Warn("Error sending info to Splunk", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return StreamHandler(writer, fmtr), nil
|
||||||
|
}
|
||||||
|
|
||||||
// XXX: closingHandler is essentially unused at the moment
|
// XXX: closingHandler is essentially unused at the moment
|
||||||
// it's meant for a future time when the Handler interface supports
|
// it's meant for a future time when the Handler interface supports
|
||||||
// a possible Close() operation
|
// a possible Close() operation
|
||||||
|
|
|
||||||
141
log/splunk/splunk.go
Normal file
141
log/splunk/splunk.go
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
package splunk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Time int64 `json:"time"` // epoch time in seconds
|
||||||
|
Host string `json:"host"` // hostname
|
||||||
|
Source string `json:"source,omitempty"` // optional description of the source of the event; typically the app's name
|
||||||
|
SourceType string `json:"sourcetype,omitempty"` // optional name of a Splunk parsing configuration; this is usually inferred by Splunk
|
||||||
|
Index string `json:"index,omitempty"` // optional name of the Splunk index to store the event in; not required if the token has a default index set in Splunk
|
||||||
|
Event interface{} `json:"event"` // throw any useful key/val pairs here
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
HTTPClient *http.Client
|
||||||
|
URL string
|
||||||
|
Hostname string
|
||||||
|
Token string
|
||||||
|
Source string
|
||||||
|
SourceType string
|
||||||
|
Index string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(httpClient *http.Client, URL string, Token string, Source string, SourceType string, Index string) *Client {
|
||||||
|
if httpClient == nil {
|
||||||
|
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}}
|
||||||
|
httpClient = &http.Client{Timeout: time.Second * 20, Transport: tr}
|
||||||
|
}
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
c := &Client{
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
URL: URL,
|
||||||
|
Hostname: hostname,
|
||||||
|
Token: Token,
|
||||||
|
Source: Source,
|
||||||
|
SourceType: SourceType,
|
||||||
|
Index: Index,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) NewEvent(event interface{}, source string, sourcetype string, index string) *Event {
|
||||||
|
e := &Event{
|
||||||
|
Time: time.Now().Unix(),
|
||||||
|
Host: c.Hostname,
|
||||||
|
Source: source,
|
||||||
|
SourceType: sourcetype,
|
||||||
|
Index: index,
|
||||||
|
Event: event,
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) NewEventWithTime(t int64, event interface{}, source string, sourcetype string, index string) *Event {
|
||||||
|
e := &Event{
|
||||||
|
Time: t,
|
||||||
|
Host: c.Hostname,
|
||||||
|
Source: source,
|
||||||
|
SourceType: sourcetype,
|
||||||
|
Index: index,
|
||||||
|
Event: event,
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Log(event interface{}) error {
|
||||||
|
log := c.NewEvent(event, c.Source, c.SourceType, c.Index)
|
||||||
|
return c.LogEvent(log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) LogWithTime(t int64, event interface{}) error {
|
||||||
|
log := c.NewEventWithTime(t, event, c.Source, c.SourceType, c.Index)
|
||||||
|
return c.LogEvent(log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) LogEvent(e *Event) error {
|
||||||
|
b, err := json.Marshal(e)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.doRequest(bytes.NewBuffer(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) LogEvents(events []*Event) error {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
for _, e := range events {
|
||||||
|
b, err := json.Marshal(e)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
buf.WriteString("\r\n\r\n")
|
||||||
|
}
|
||||||
|
return c.doRequest(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Writer() io.Writer {
|
||||||
|
return &Writer{
|
||||||
|
Client: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doRequest(b *bytes.Buffer) error {
|
||||||
|
url := c.URL
|
||||||
|
req, err := http.NewRequest("POST", url, b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Authorization", "Splunk "+c.Token)
|
||||||
|
|
||||||
|
res, err := c.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
switch res.StatusCode {
|
||||||
|
case 200:
|
||||||
|
io.Copy(ioutil.Discard, res.Body)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(res.Body)
|
||||||
|
responseBody := buf.String()
|
||||||
|
err = errors.New(responseBody)
|
||||||
|
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
105
log/splunk/writer.go
Normal file
105
log/splunk/writer.go
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package splunk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bufferSize = 10000
|
||||||
|
defaultInterval = 5 * time.Second
|
||||||
|
defaultThreshold = 100
|
||||||
|
defaultRetries = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Writer struct {
|
||||||
|
Client *Client
|
||||||
|
// How often the write buffer should be flushed to splunk
|
||||||
|
FlushInterval time.Duration
|
||||||
|
// How many Write()'s before buffer should be flushed to splunk
|
||||||
|
FlushThreshold int
|
||||||
|
// Max number of retries we should do when we flush the buffer
|
||||||
|
MaxRetries int
|
||||||
|
dataChan chan *message
|
||||||
|
errors chan error
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
data string
|
||||||
|
writtenAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) initialize() {
|
||||||
|
w.once.Do(func() {
|
||||||
|
w.dataChan = make(chan *message, bufferSize)
|
||||||
|
w.errors = make(chan error, bufferSize)
|
||||||
|
go w.listen()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Write(b []byte) (int, error) {
|
||||||
|
w.initialize()
|
||||||
|
w.dataChan <- &message{
|
||||||
|
data: string(b),
|
||||||
|
writtenAt: time.Now(),
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Errors() <-chan error {
|
||||||
|
w.initialize()
|
||||||
|
return w.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) listen() {
|
||||||
|
if w.FlushInterval <= 0 {
|
||||||
|
w.FlushInterval = defaultInterval
|
||||||
|
}
|
||||||
|
if w.FlushThreshold == 0 {
|
||||||
|
w.FlushThreshold = defaultThreshold
|
||||||
|
}
|
||||||
|
if w.MaxRetries == 0 {
|
||||||
|
w.MaxRetries = defaultRetries
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(w.FlushInterval)
|
||||||
|
buffer := make([]*message, 0)
|
||||||
|
|
||||||
|
flush := func() {
|
||||||
|
go w.send(buffer, w.MaxRetries)
|
||||||
|
buffer = make([]*message, 0)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if len(buffer) > 0 {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
case d := <-w.dataChan:
|
||||||
|
buffer = append(buffer, d)
|
||||||
|
if len(buffer) > w.FlushThreshold {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) send(messages []*message, retries int) {
|
||||||
|
events := make([]*Event, len(messages))
|
||||||
|
for i, m := range messages {
|
||||||
|
events[i] = w.Client.NewEventWithTime(m.writtenAt.Unix(), m.data, w.Client.Source, w.Client.SourceType, w.Client.Index)
|
||||||
|
}
|
||||||
|
err := w.Client.LogEvents(events)
|
||||||
|
if err != nil {
|
||||||
|
for i := 0; i < retries; i++ {
|
||||||
|
err = w.Client.LogEvents(events)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case w.errors <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
log/splunk/writer_test.go
Normal file
91
log/splunk/writer_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package splunk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriter_Write(t *testing.T) {
|
||||||
|
numWrites := 1000
|
||||||
|
numMessages := 0
|
||||||
|
lock := sync.Mutex{}
|
||||||
|
notify := make(chan bool, numWrites)
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := ioutil.ReadAll(r.Body)
|
||||||
|
split := strings.Split(string(b), "\n")
|
||||||
|
num := 0
|
||||||
|
// Since we batch our logs up before we send them:
|
||||||
|
// Increment our messages counter by one for each JSON object we got in this response
|
||||||
|
// We don't know how many responses we'll get, we only care about the number of messages
|
||||||
|
for _, line := range split {
|
||||||
|
if strings.HasPrefix(line, "{") {
|
||||||
|
num++
|
||||||
|
notify <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lock.Lock()
|
||||||
|
numMessages = numMessages + num
|
||||||
|
lock.Unlock()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create a writer that's flushing constantly. We want this test to run
|
||||||
|
// quickly
|
||||||
|
writer := Writer{
|
||||||
|
Client: NewClient(server.Client(), server.URL, "", "", "", ""),
|
||||||
|
FlushInterval: 1 * time.Millisecond,
|
||||||
|
}
|
||||||
|
// Send a bunch of messages in separate goroutines to make sure we're properly
|
||||||
|
// testing Writer's concurrency promise
|
||||||
|
for i := 0; i < numWrites; i++ {
|
||||||
|
go writer.Write([]byte(fmt.Sprintf("%d", i)))
|
||||||
|
}
|
||||||
|
// To notify our test we've collected everything we need.
|
||||||
|
doneChan := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < numWrites; i++ {
|
||||||
|
// Do nothing, just loop through to the next one
|
||||||
|
<-notify
|
||||||
|
}
|
||||||
|
doneChan <- true
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-doneChan:
|
||||||
|
// Do nothing, we're good
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
t.Errorf("Timed out waiting for messages")
|
||||||
|
}
|
||||||
|
// We may have received more than numWrites amount of messages, check that case
|
||||||
|
if numMessages != numWrites {
|
||||||
|
t.Errorf("Didn't get the right number of messages, expected %d, got %d", numWrites, numMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriter_Errors(t *testing.T) {
|
||||||
|
numMessages := 1000
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintln(w, "bad request")
|
||||||
|
}))
|
||||||
|
writer := Writer{
|
||||||
|
Client: NewClient(server.Client(), server.URL, "", "", "", ""),
|
||||||
|
// Will flush after the last message is sent
|
||||||
|
FlushThreshold: numMessages - 1,
|
||||||
|
// Don't let the flush interval cause raciness
|
||||||
|
FlushInterval: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
for i := 0; i < numMessages; i++ {
|
||||||
|
_, _ = writer.Write([]byte("some data"))
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-writer.Errors():
|
||||||
|
// good to go, got our error
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
t.Errorf("Timed out waiting for error, should have gotten 1 error")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue