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  }