github.com/Axway/agent-sdk@v1.1.101/pkg/transaction/metric/usagepublisher.go (about) 1 package metric 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "mime/multipart" 9 "net/textproto" 10 "sort" 11 "strings" 12 "time" 13 14 "github.com/google/uuid" 15 "github.com/sirupsen/logrus" 16 17 "github.com/Axway/agent-sdk/pkg/agent" 18 "github.com/Axway/agent-sdk/pkg/api" 19 "github.com/Axway/agent-sdk/pkg/jobs" 20 "github.com/Axway/agent-sdk/pkg/util" 21 "github.com/Axway/agent-sdk/pkg/util/log" 22 ) 23 24 type usagePublisher struct { 25 apiClient api.Client 26 storage storageCache 27 report *usageReportCache 28 jobID string 29 schedule string 30 ready bool 31 offline bool 32 logger log.FieldLogger 33 usageLogger log.FieldLogger 34 } 35 36 func (c *usagePublisher) publishEvent(event interface{}) error { 37 if usageEvent, ok := event.(UsageEvent); ok { 38 return c.publishToCache(usageEvent) 39 } 40 c.logger.Error("event was not a usage event") 41 return nil 42 } 43 44 func (c *usagePublisher) publishToCache(event UsageEvent) error { 45 return c.report.addReport(event) 46 } 47 48 func (c *usagePublisher) publishToPlatformUsage(event UsageEvent) error { 49 token, err := agent.GetCentralAuthToken() 50 if err != nil { 51 return err 52 } 53 54 event, startTime := aggregateReports(event) 55 b, contentType, err := c.createMultipartFormData(event) 56 if err != nil { 57 return err 58 } 59 60 headers := map[string]string{ 61 "Content-Type": contentType, 62 "Authorization": "Bearer " + token, 63 } 64 65 request := api.Request{ 66 Method: api.POST, 67 URL: agent.GetCentralConfig().GetUsageReportingConfig().GetURL() + "/api/v1/usage", 68 Headers: headers, 69 Body: b.Bytes(), 70 } 71 c.logger.Debugf("Payload for Usage event : %s\n", b.String()) 72 response, err := c.apiClient.Send(request) 73 if err != nil { 74 c.logger.WithError(err).Error("publishing usage") 75 return err 76 } 77 78 fields := logrus.Fields{ 79 "date": startTime, 80 } 81 for usageKey, usageVal := range event.Report[startTime].Usage { 82 fields[usageKey] = usageVal 83 } 84 if response.Code == 202 { 85 c.logger.WithField("statusCode", 202).Debugf("successful request with payload: %s", b.String()) 86 c.usageLogger.WithFields(fields).Info("successfully published") 87 return nil 88 } else if response.Code >= 500 { 89 err := fmt.Errorf("server error") 90 c.logger.WithField("statusCode", response.Code).WithError(err).Error(string(response.Body)) 91 return err 92 } 93 94 usageResp := UsageResponse{} 95 err = json.Unmarshal(response.Body, &usageResp) 96 if err != nil { 97 c.logger.WithField("responseBody", string(response.Body)).WithField("statusCode", response.Code). 98 WithError(err).Error("Could not unmarshal response body") 99 return err 100 } 101 if strings.HasPrefix(usageResp.Description, "The file exceeds the maximum upload size of ") || usageResp.Description == "Environment ID not found" { 102 err := fmt.Errorf("request failed with unexpected status code. Not scheduling for retry in the next batch") 103 c.logger.WithField("statusCode", response.Code).WithError(err).Error(usageResp.Description) 104 return nil 105 } 106 err = fmt.Errorf("request failed with unexpected status code") 107 c.logger.WithField("statusCode", response.Code).WithError(err).Error(usageResp.Description) 108 return err 109 } 110 111 func (c *usagePublisher) createMultipartFormData(event UsageEvent) (b bytes.Buffer, contentType string, err error) { 112 buffer, _ := json.Marshal(event) 113 w := multipart.NewWriter(&b) 114 defer w.Close() 115 w.WriteField("organizationId", event.OrgGUID) 116 117 var fw io.Writer 118 if fw, err = c.createFilePart(w, uuid.New().String()+".json"); err != nil { 119 return 120 } 121 if _, err = io.Copy(fw, bytes.NewReader(buffer)); err != nil { 122 return 123 } 124 contentType = w.FormDataContentType() 125 126 return 127 } 128 129 func aggregateReports(event UsageEvent) (UsageEvent, string) { 130 131 // order all the keys, this will be used to find first and last timestamp 132 orderedKeys := make([]string, 0, len(event.Report)) 133 for k := range event.Report { 134 orderedKeys = append(orderedKeys, k) 135 } 136 sort.Strings(orderedKeys) 137 138 // create a single report which has all eventReports appended 139 finalReport := map[string]UsageReport{ 140 orderedKeys[0]: { 141 Product: event.Report[orderedKeys[0]].Product, 142 Usage: make(map[string]int64), 143 Meta: event.Report[orderedKeys[0]].Meta, 144 }, 145 } 146 147 for _, report := range event.Report { 148 for usageKey, usageVal := range report.Usage { 149 finalReport[orderedKeys[0]].Usage[usageKey] += usageVal 150 } 151 } 152 event.Report = finalReport 153 154 startTime, _ := time.Parse(ISO8601, orderedKeys[0]) 155 endTime := now() 156 event.Granularity = int(endTime.Sub(startTime).Milliseconds()) 157 event.Timestamp = ISO8601Time(endTime) 158 return event, orderedKeys[0] 159 } 160 161 // createFilePart - adds the file part to the request 162 func (c *usagePublisher) createFilePart(w *multipart.Writer, filename string) (io.Writer, error) { 163 h := make(textproto.MIMEHeader) 164 h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filename)) 165 h.Set("Content-Type", "application/json") 166 return w.CreatePart(h) 167 } 168 169 // newUsagePublisher - Creates publisher job 170 func newUsagePublisher(storage storageCache, report *usageReportCache) *usagePublisher { 171 centralCfg := agent.GetCentralConfig() 172 publisher := &usagePublisher{ 173 apiClient: api.NewClient(centralCfg.GetTLSConfig(), centralCfg.GetProxyURL(), 174 api.WithTimeout(centralCfg.GetClientTimeout()), 175 api.WithSingleURL()), 176 storage: storage, 177 report: report, 178 offline: agent.GetCentralConfig().GetUsageReportingConfig().IsOfflineMode(), 179 logger: log.NewFieldLogger().WithComponent("usagePublisher").WithPackage("metric"), 180 usageLogger: log.NewUsageFieldLogger(), 181 } 182 183 publisher.usageLogger.Info("usage logger started") 184 publisher.registerReportJob() 185 return publisher 186 } 187 188 func (c *usagePublisher) isReady() bool { 189 return c.ready 190 } 191 192 func (c *usagePublisher) registerReportJob() { 193 if !util.IsNotTest() { 194 return // skip setting up the job in test 195 } 196 197 schedule := agent.GetCentralConfig().GetUsageReportingConfig().GetSchedule() 198 if agent.GetCentralConfig().GetUsageReportingConfig().IsOfflineMode() { 199 schedule = agent.GetCentralConfig().GetUsageReportingConfig().GetReportSchedule() 200 } 201 c.schedule = schedule 202 203 // start the job according to the cron schedule 204 var err error 205 c.jobID, err = jobs.RegisterScheduledJobWithName(c, c.schedule, "Usage Reporting") 206 if err != nil { 207 c.logger.WithError(err).Error("could not register usage report creation job") 208 } 209 } 210 211 // Status - returns an error if the status of the offline report job is in error 212 func (c *usagePublisher) Status() error { 213 return nil 214 } 215 216 // Ready - indicates that the offline report job is ready to process 217 // 218 // additionally runs the initial report gen if the last trigger would 219 // have ran but the agent was down 220 func (c *usagePublisher) Ready() bool { 221 if agent.GetCentralConfig().GetEnvironmentID() == "" { 222 return false 223 } 224 225 defer func() { 226 c.ready = true 227 }() // once any existing reports are saved off this isReady 228 229 err := c.Execute() 230 if err != nil { 231 c.logger.WithError(err).Errorf("error hit generating report, report still in cache") 232 } 233 return true 234 } 235 236 // Execute - process the offline report generation 237 func (c *usagePublisher) Execute() error { 238 if c.report.shouldPublish(c.schedule) { 239 if c.offline { 240 return c.report.saveReport() 241 } 242 return c.report.sendReport(c.publishToPlatformUsage) 243 } 244 return nil 245 }