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 }