github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/pusher/v1/pusher.go (about)

     1  package v1
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/prometheus/client_golang/prometheus"
    12  	"github.com/prometheus/prometheus/prompb"
    13  	"github.com/rs/zerolog"
    14  
    15  	logproto "github.com/grafana/loki/pkg/push"
    16  	"github.com/grafana/synthetic-monitoring-agent/internal/model"
    17  	"github.com/grafana/synthetic-monitoring-agent/internal/pkg/loki"
    18  	"github.com/grafana/synthetic-monitoring-agent/internal/pkg/prom"
    19  	"github.com/grafana/synthetic-monitoring-agent/internal/pusher"
    20  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    21  )
    22  
    23  const (
    24  	defaultBufferCapacity = 10 * 1024
    25  	Name                  = "v1"
    26  )
    27  
    28  var bufPool = sync.Pool{
    29  	New: func() any {
    30  		buf := make([]byte, 0, defaultBufferCapacity)
    31  		return &buf
    32  	},
    33  }
    34  
    35  type remoteTarget struct {
    36  	Events  *prom.Client
    37  	Metrics *prom.Client
    38  }
    39  
    40  type publisherImpl struct {
    41  	ctx           context.Context
    42  	tenantManager pusher.TenantProvider
    43  	logger        zerolog.Logger
    44  	clientsMutex  sync.Mutex
    45  	clients       map[model.GlobalID]*remoteTarget
    46  	metrics       pusher.Metrics
    47  }
    48  
    49  var _ pusher.Publisher = &publisherImpl{}
    50  
    51  func NewPublisher(ctx context.Context, tm pusher.TenantProvider, logger zerolog.Logger, promRegisterer prometheus.Registerer) pusher.Publisher {
    52  	return &publisherImpl{
    53  		ctx:           ctx,
    54  		tenantManager: tm,
    55  		clients:       make(map[model.GlobalID]*remoteTarget),
    56  		logger:        logger,
    57  		metrics:       pusher.NewMetrics(promRegisterer),
    58  	}
    59  }
    60  
    61  func (p *publisherImpl) Publish(payload pusher.Payload) {
    62  	go p.publish(p.ctx, payload)
    63  }
    64  
    65  func (p *publisherImpl) publish(ctx context.Context, payload pusher.Payload) {
    66  	var (
    67  		tenantID = payload.Tenant()
    68  
    69  		// The above tenant ID is potentially a global ID. This is valid
    70  		// for using internally but in logs and metrics we want to publish
    71  		// the region and local tenant ID.
    72  		localID, regionID = model.GetLocalAndRegionIDs(tenantID)
    73  		regionStr         = strconv.FormatInt(int64(regionID), 10)
    74  		tenantStr         = strconv.FormatInt(localID, 10)
    75  
    76  		newClient = false
    77  		logger    = p.logger.With().Int("region", regionID).Int64("tenant", localID).Logger()
    78  	)
    79  
    80  	streams := payload.Streams()
    81  	metrics := payload.Metrics()
    82  
    83  	for retry := 2; retry > 0; retry-- {
    84  		client, err := p.getClient(ctx, tenantID, newClient)
    85  		if err != nil {
    86  			logger.Error().Err(err).Msg("get client failed")
    87  
    88  			if len(streams) > 0 {
    89  				p.metrics.FailedCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueLogs, pusher.LabelValueClient).Inc()
    90  			}
    91  
    92  			if len(metrics) > 0 {
    93  				p.metrics.FailedCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueMetrics, pusher.LabelValueClient).Inc()
    94  			}
    95  
    96  			return
    97  		}
    98  
    99  		if len(streams) > 0 {
   100  			if n, err := p.pushEvents(ctx, client.Events, streams); err != nil {
   101  				httpStatusCode, hasStatusCode := prom.GetHttpStatusCode(err)
   102  				logger.Error().Err(err).Int("status", httpStatusCode).Msg("publish events")
   103  				p.metrics.ErrorCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueLogs, strconv.Itoa(httpStatusCode)).Inc()
   104  
   105  				if hasStatusCode && httpStatusCode == http.StatusUnauthorized {
   106  					// Retry to get a new client, credentials might be stale.
   107  					newClient = true
   108  					continue
   109  				}
   110  			} else {
   111  				p.metrics.PushCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueLogs).Inc()
   112  				p.metrics.BytesOut.WithLabelValues(regionStr, tenantStr, pusher.LabelValueLogs).Add(float64(n))
   113  
   114  				streams = nil
   115  			}
   116  		}
   117  
   118  		if len(metrics) > 0 {
   119  			if n, err := p.pushMetrics(ctx, client.Metrics, metrics); err != nil {
   120  				httpStatusCode, hasStatusCode := prom.GetHttpStatusCode(err)
   121  				logger.Error().Err(err).Int("status", httpStatusCode).Msg("publish metrics")
   122  				p.metrics.ErrorCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueMetrics, strconv.Itoa(httpStatusCode)).Inc()
   123  
   124  				if hasStatusCode && httpStatusCode == http.StatusUnauthorized {
   125  					// Retry to get a new client, credentials might be stale.
   126  					newClient = true
   127  					continue
   128  				}
   129  			} else {
   130  				p.metrics.PushCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueMetrics).Inc()
   131  				p.metrics.BytesOut.WithLabelValues(regionStr, tenantStr, pusher.LabelValueMetrics).Add(float64(n))
   132  				metrics = nil
   133  			}
   134  		}
   135  
   136  		if len(streams) == 0 && len(metrics) == 0 {
   137  			// if we make it here we have sent everything we could send, we are done.
   138  			return
   139  		}
   140  	}
   141  
   142  	// if we are here, we retried and failed
   143  	if len(streams) > 0 {
   144  		p.metrics.FailedCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueLogs, pusher.LabelValueRetryExhausted).Inc()
   145  	}
   146  
   147  	if len(metrics) > 0 {
   148  		p.metrics.FailedCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueMetrics, pusher.LabelValueRetryExhausted).Inc()
   149  	}
   150  
   151  	logger.Warn().Msg("failed to push payload")
   152  }
   153  
   154  func (p *publisherImpl) pushEvents(ctx context.Context, client *prom.Client, streams []logproto.Stream) (int, error) {
   155  	buf := bufPool.Get().(*[]byte)
   156  	defer bufPool.Put(buf)
   157  
   158  	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
   159  	defer cancel()
   160  
   161  	if err := loki.SendStreamsWithBackoff(ctx, client, streams, buf); err != nil {
   162  		return 0, fmt.Errorf("sending events: %w", err)
   163  	}
   164  
   165  	return len(*buf), nil
   166  }
   167  
   168  func (p *publisherImpl) pushMetrics(ctx context.Context, client *prom.Client, metrics []prompb.TimeSeries) (int, error) {
   169  	buf := bufPool.Get().(*[]byte)
   170  	defer bufPool.Put(buf)
   171  
   172  	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
   173  	defer cancel()
   174  
   175  	if err := prom.SendSamplesWithBackoff(ctx, client, metrics, buf); err != nil {
   176  		return 0, fmt.Errorf("sending timeseries: %w", err)
   177  	}
   178  
   179  	return len(*buf), nil
   180  }
   181  
   182  func (p *publisherImpl) getClient(ctx context.Context, tenantId model.GlobalID, newClient bool) (*remoteTarget, error) {
   183  	var (
   184  		client *remoteTarget
   185  		found  bool
   186  	)
   187  
   188  	localID, regionID := model.GetLocalAndRegionIDs(tenantId)
   189  
   190  	p.clientsMutex.Lock()
   191  	if newClient {
   192  		p.logger.Info().Int("regionId", regionID).Int64("tenantId", localID).Msg("removing tenant from cache")
   193  		delete(p.clients, tenantId)
   194  	} else {
   195  		client, found = p.clients[tenantId]
   196  	}
   197  	p.clientsMutex.Unlock()
   198  
   199  	if found {
   200  		return client, nil
   201  	}
   202  
   203  	p.logger.Info().Int("regionId", regionID).Int64("tenantId", localID).Msg("fetching tenant credentials")
   204  
   205  	req := sm.TenantInfo{
   206  		Id: int64(tenantId),
   207  	}
   208  
   209  	tenant, err := p.tenantManager.GetTenant(ctx, &req)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	return p.updateClient(tenant)
   215  }
   216  
   217  func (p *publisherImpl) updateClient(tenant *sm.Tenant) (*remoteTarget, error) {
   218  	mClientCfg, err := pusher.ClientFromRemoteInfo(tenant.MetricsRemote)
   219  	if err != nil {
   220  		return nil, fmt.Errorf("creating metrics client configuration: %w", err)
   221  	}
   222  
   223  	localID, regionID := model.GetLocalAndRegionIDs(model.GlobalID(tenant.Id))
   224  
   225  	regionStr := strconv.FormatInt(int64(regionID), 10)
   226  	tenantStr := strconv.FormatInt(localID, 10)
   227  
   228  	mClient, err := prom.NewClient(tenant.MetricsRemote.Name, mClientCfg, func(c float64) {
   229  		p.metrics.RetriesCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueMetrics).Add(c)
   230  	})
   231  	if err != nil {
   232  		return nil, fmt.Errorf("creating metrics client: %w", err)
   233  	}
   234  
   235  	eClientCfg, err := pusher.ClientFromRemoteInfo(tenant.EventsRemote)
   236  	if err != nil {
   237  		return nil, fmt.Errorf("creating events client configuration: %w", err)
   238  	}
   239  
   240  	eClient, err := prom.NewClient(tenant.EventsRemote.Name, eClientCfg, func(c float64) {
   241  		p.metrics.RetriesCounter.WithLabelValues(regionStr, tenantStr, pusher.LabelValueLogs).Add(c)
   242  	})
   243  	if err != nil {
   244  		return nil, fmt.Errorf("creating events client: %w", err)
   245  	}
   246  
   247  	clients := &remoteTarget{
   248  		Metrics: mClient,
   249  		Events:  eClient,
   250  	}
   251  
   252  	p.clientsMutex.Lock()
   253  	p.clients[model.GlobalID(tenant.Id)] = clients
   254  	p.clientsMutex.Unlock()
   255  	p.logger.Debug().Int("regionId", regionID).Int64("tenantId", localID).Int64("stackId", tenant.StackId).Msg("updated client")
   256  
   257  	return clients, nil
   258  }