github.com/instana/go-sensor@v1.62.2-0.20240520081010-4919868049e1/azure_agent.go (about)

     1  // (c) Copyright IBM Corp. 2022
     2  
     3  package instana
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"os"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/instana/go-sensor/acceptor"
    19  	"github.com/instana/go-sensor/autoprofile"
    20  )
    21  
    22  const (
    23  	flushPeriodInSec = 2
    24  
    25  	azureCustomRuntime string = "custom"
    26  )
    27  
    28  type azureAgent struct {
    29  	Endpoint string
    30  	Key      string
    31  	PID      int
    32  
    33  	snapshot serverlessSnapshot
    34  
    35  	mu        sync.Mutex
    36  	spanQueue []Span
    37  
    38  	client *http.Client
    39  	logger LeveledLogger
    40  }
    41  
    42  func newAzureAgent(acceptorEndpoint, agentKey string, client *http.Client, logger LeveledLogger) *azureAgent {
    43  	if logger == nil {
    44  		logger = defaultLogger
    45  	}
    46  
    47  	if client == nil {
    48  		client = http.DefaultClient
    49  		client.Timeout = 500 * time.Millisecond
    50  	}
    51  
    52  	logger.Debug("initializing azure agent")
    53  
    54  	agent := &azureAgent{
    55  		Endpoint: acceptorEndpoint,
    56  		Key:      agentKey,
    57  		PID:      os.Getpid(),
    58  		client:   client,
    59  		logger:   logger,
    60  	}
    61  
    62  	go func() {
    63  		t := time.NewTicker(flushPeriodInSec * time.Second)
    64  		defer t.Stop()
    65  
    66  		for range t.C {
    67  			if err := agent.Flush(context.Background()); err != nil {
    68  				agent.logger.Error("failed to post collected data: ", err)
    69  			}
    70  		}
    71  	}()
    72  
    73  	return agent
    74  }
    75  
    76  func (a *azureAgent) Ready() bool { return true }
    77  
    78  func (a *azureAgent) SendMetrics(acceptor.Metrics) error { return nil }
    79  
    80  func (a *azureAgent) SendEvent(*EventData) error { return nil }
    81  
    82  func (a *azureAgent) SendSpans(spans []Span) error {
    83  	a.enqueueSpans(spans)
    84  	return nil
    85  }
    86  
    87  func (a *azureAgent) SendProfiles([]autoprofile.Profile) error { return nil }
    88  
    89  func (a *azureAgent) Flush(ctx context.Context) error {
    90  	a.collectSnapshot()
    91  
    92  	if a.snapshot.EntityID == "" {
    93  		return ErrAgentNotReady
    94  	}
    95  
    96  	from := newServerlessAgentFromS(a.snapshot.EntityID, "azure")
    97  
    98  	payload := struct {
    99  		Metrics metricsPayload `json:"metrics,omitempty"`
   100  		Spans   []Span         `json:"spans,omitempty"`
   101  	}{
   102  		Metrics: metricsPayload{
   103  			Plugins: []acceptor.PluginPayload{
   104  				acceptor.NewAzurePluginPayload(a.snapshot.EntityID),
   105  			},
   106  		},
   107  	}
   108  
   109  	a.mu.Lock()
   110  	payload.Spans = make([]Span, len(a.spanQueue))
   111  	copy(payload.Spans, a.spanQueue)
   112  	a.spanQueue = a.spanQueue[:0]
   113  	a.mu.Unlock()
   114  
   115  	for i := range payload.Spans {
   116  		payload.Spans[i].From = from
   117  	}
   118  
   119  	buf := bytes.NewBuffer(nil)
   120  	if err := json.NewEncoder(buf).Encode(payload); err != nil {
   121  		return fmt.Errorf("failed to marshal traces payload: %s", err)
   122  	}
   123  
   124  	payloadSize := buf.Len()
   125  	if payloadSize > maxContentLength {
   126  		a.logger.Warn(fmt.Sprintf("failed to send the spans. Payload size: %d exceeded max size: %d", payloadSize, maxContentLength))
   127  		return payloadTooLargeErr
   128  	}
   129  
   130  	req, err := http.NewRequest(http.MethodPost, a.Endpoint+"/bundle", buf)
   131  	if err != nil {
   132  		a.enqueueSpans(payload.Spans)
   133  		return fmt.Errorf("failed to prepare send traces request: %s", err)
   134  	}
   135  
   136  	req.Header.Set("Content-Type", "application/json")
   137  
   138  	if err := a.sendRequest(req.WithContext(ctx)); err != nil {
   139  		a.enqueueSpans(payload.Spans)
   140  		return fmt.Errorf("failed to send traces, will retry later: %dsec. Error details: %s",
   141  			flushPeriodInSec, err.Error())
   142  	}
   143  
   144  	return nil
   145  }
   146  
   147  func (a *azureAgent) enqueueSpans(spans []Span) {
   148  	a.mu.Lock()
   149  	defer a.mu.Unlock()
   150  
   151  	a.spanQueue = append(a.spanQueue, spans...)
   152  }
   153  
   154  func (a *azureAgent) sendRequest(req *http.Request) error {
   155  	req.Header.Set("X-Instana-Host", a.snapshot.Host)
   156  	req.Header.Set("X-Instana-Key", a.Key)
   157  
   158  	resp, err := a.client.Do(req)
   159  	if err != nil {
   160  		return fmt.Errorf("failed to send request to the serverless agent: %s", err)
   161  	}
   162  
   163  	defer resp.Body.Close()
   164  
   165  	if resp.StatusCode >= http.StatusBadRequest {
   166  		respBody, err := ioutil.ReadAll(resp.Body)
   167  		if err != nil {
   168  			a.logger.Debug("failed to read serverless agent response: ", err.Error())
   169  			return err
   170  		}
   171  
   172  		a.logger.Info("serverless agent has responded with ", resp.Status, ": ", string(respBody))
   173  		return err
   174  	}
   175  
   176  	io.CopyN(ioutil.Discard, resp.Body, 1<<20)
   177  
   178  	return nil
   179  }
   180  
   181  func (a *azureAgent) collectSnapshot() {
   182  	if a.snapshot.EntityID != "" {
   183  		return
   184  	}
   185  
   186  	var subscriptionID, resourceGrp, functionApp string
   187  	var ok bool
   188  	if websiteOwnerName, ok := os.LookupEnv("WEBSITE_OWNER_NAME"); ok {
   189  		arr := strings.Split(websiteOwnerName, "+")
   190  		if len(arr) > 1 {
   191  			subscriptionID = arr[0]
   192  		} else {
   193  			a.logger.Warn("failed to retrieve the subscription id. This will affect the correlation metrics.")
   194  		}
   195  	} else {
   196  		a.logger.Warn("failed to retrieve the subscription id. This will affect the correlation metrics.")
   197  	}
   198  
   199  	if resourceGrp, ok = os.LookupEnv("WEBSITE_RESOURCE_GROUP"); !ok {
   200  		a.logger.Warn("failed to retrieve the resource group. This will affect the correlation metrics.")
   201  	}
   202  
   203  	if functionApp, ok = os.LookupEnv("APPSETTING_WEBSITE_SITE_NAME"); !ok {
   204  		a.logger.Warn("failed to retrieve the function app. This will affect the correlation metrics.")
   205  	}
   206  
   207  	entityID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/sites/%s",
   208  		subscriptionID, resourceGrp, functionApp)
   209  
   210  	a.snapshot = serverlessSnapshot{
   211  		EntityID: entityID,
   212  		PID:      a.PID,
   213  	}
   214  	a.logger.Debug("collected snapshot")
   215  }