mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-06 23:46:29 +02:00
195 lines
4.8 KiB
Go
195 lines
4.8 KiB
Go
package trace
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.opencensus.io/trace"
|
|
)
|
|
|
|
// Error variables for factory validation.
|
|
var (
|
|
ErrLoggerNotProvided = errors.New("logger not provided")
|
|
ErrHostNotProvided = errors.New("host not provided")
|
|
)
|
|
|
|
// Log provides support for logging inside this package.
|
|
// Unfortunately, the opentrace API calls into the ExportSpan
|
|
// function directly with no means to pass user defined arguments.
|
|
type Log func(format string, v ...interface{})
|
|
|
|
// Exporter provides support to batch spans and send them
|
|
// to the sidecar for processing.
|
|
type Exporter struct {
|
|
log Log // Handler function for logging.
|
|
host string // IP:port of the sidecare consuming the trace data.
|
|
batchSize int // Size of the batch of spans before sending.
|
|
sendInterval time.Duration // Time to send a batch if batch size is not met.
|
|
sendTimeout time.Duration // Time to wait for the sidecar to respond on send.
|
|
client http.Client // Provides APIs for performing the http send.
|
|
batch []*trace.SpanData // Maintains the batch of span data to be sent.
|
|
mu sync.Mutex // Provide synchronization to access the batch safely.
|
|
timer *time.Timer // Signals when the sendInterval is met.
|
|
}
|
|
|
|
// NewExporter creates an exporter for use.
|
|
func NewExporter(log Log, host string, batchSize int, sendInterval, sendTimeout time.Duration) (*Exporter, error) {
|
|
if log == nil {
|
|
return nil, ErrLoggerNotProvided
|
|
}
|
|
if host == "" {
|
|
return nil, ErrHostNotProvided
|
|
}
|
|
|
|
tr := http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
DualStack: true,
|
|
}).DialContext,
|
|
MaxIdleConns: 2,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
|
|
e := Exporter{
|
|
log: log,
|
|
host: host,
|
|
batchSize: batchSize,
|
|
sendInterval: sendInterval,
|
|
sendTimeout: sendTimeout,
|
|
client: http.Client{
|
|
Transport: &tr,
|
|
},
|
|
batch: make([]*trace.SpanData, 0, batchSize),
|
|
timer: time.NewTimer(sendInterval),
|
|
}
|
|
|
|
return &e, nil
|
|
}
|
|
|
|
// Close sends the remaining spans that have not been sent yet.
|
|
func (e *Exporter) Close() (int, error) {
|
|
var sendBatch []*trace.SpanData
|
|
e.mu.Lock()
|
|
{
|
|
sendBatch = e.batch
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
err := e.send(sendBatch)
|
|
if err != nil {
|
|
return len(sendBatch), err
|
|
}
|
|
|
|
return len(sendBatch), nil
|
|
}
|
|
|
|
// ExportSpan is called by opentracing when spans are created. It implements
|
|
// the Exporter interface.
|
|
func (e *Exporter) ExportSpan(span *trace.SpanData) {
|
|
sendBatch := e.saveBatch(span)
|
|
if sendBatch != nil {
|
|
go func() {
|
|
e.log("trace : Exporter : ExportSpan : Sending Batch[%d]", len(sendBatch))
|
|
if err := e.send(sendBatch); err != nil {
|
|
e.log("trace : Exporter : ExportSpan : ERROR : %v", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Saves the span data to the batch. If the batch should be sent,
|
|
// returns a batch to send.
|
|
func (e *Exporter) saveBatch(span *trace.SpanData) []*trace.SpanData {
|
|
var sendBatch []*trace.SpanData
|
|
|
|
e.mu.Lock()
|
|
{
|
|
// We want to append this new span to the collection.
|
|
e.batch = append(e.batch, span)
|
|
|
|
// Do we need to send the current batch?
|
|
switch {
|
|
case len(e.batch) == e.batchSize:
|
|
|
|
// We hit the batch size. Now save the current
|
|
// batch for sending and start a new batch.
|
|
sendBatch = e.batch
|
|
e.batch = make([]*trace.SpanData, 0, e.batchSize)
|
|
e.timer.Reset(e.sendInterval)
|
|
|
|
default:
|
|
|
|
// We did not hit the batch size but maybe send what
|
|
// we have based on time.
|
|
select {
|
|
case <-e.timer.C:
|
|
|
|
// The time has expired so save the current
|
|
// batch for sending and start a new batch.
|
|
sendBatch = e.batch
|
|
e.batch = make([]*trace.SpanData, 0, e.batchSize)
|
|
e.timer.Reset(e.sendInterval)
|
|
|
|
// It's not time yet, just move on.
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
return sendBatch
|
|
}
|
|
|
|
// send uses HTTP to send the data to the tracing sidecare for processing.
|
|
func (e *Exporter) send(sendBatch []*trace.SpanData) error {
|
|
data, err := json.Marshal(sendBatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", e.host, bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(req.Context(), e.sendTimeout)
|
|
defer cancel()
|
|
req = req.WithContext(ctx)
|
|
|
|
ch := make(chan error)
|
|
go func() {
|
|
resp, err := e.client.Do(req)
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
data, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
ch <- fmt.Errorf("error on call : status[%s]", resp.Status)
|
|
return
|
|
}
|
|
ch <- fmt.Errorf("error on call : status[%s] : %s", resp.Status, string(data))
|
|
return
|
|
}
|
|
|
|
ch <- nil
|
|
}()
|
|
|
|
return <-ch
|
|
}
|