1
0
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

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
}