github.com/Axway/agent-sdk@v1.1.101/pkg/transaction/metric/reportcache.go (about)

     1  package metric
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"sort"
    12  	"strconv"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/Axway/agent-sdk/pkg/agent"
    17  	"github.com/Axway/agent-sdk/pkg/cache"
    18  	"github.com/Axway/agent-sdk/pkg/jobs"
    19  	"github.com/Axway/agent-sdk/pkg/traceability"
    20  	"github.com/Axway/agent-sdk/pkg/util/log"
    21  	"github.com/gorhill/cronexpr"
    22  )
    23  
    24  const (
    25  	eventsKey                 = "lighthouse_events"
    26  	lastPublishTimestampKey   = "timestamp"
    27  	offlineCacheFileName      = "agent-report-working.json"
    28  	offlineReportSuffix       = "usage_report.json"
    29  	offlineReportDateFormat   = "2006_01_02"
    30  	qaOfflineReportDateFormat = "2006_01_02_15_04"
    31  )
    32  
    33  type currentTimeFunc func() time.Time
    34  
    35  type usageReportCache struct {
    36  	jobs.Job
    37  	logger                  log.FieldLogger
    38  	cacheFilePath           string
    39  	reportCache             cache.Cache
    40  	reportCacheLock         sync.Mutex
    41  	isInitialized           bool
    42  	offlineReportDateFormat string
    43  	currTimeFunc            currentTimeFunc
    44  }
    45  
    46  func newReportCache() *usageReportCache {
    47  	reportManager := &usageReportCache{
    48  		logger:                  log.NewFieldLogger().WithPackage("metric").WithComponent("usageReportCache"),
    49  		cacheFilePath:           traceability.GetCacheDirPath() + "/" + offlineCacheFileName,
    50  		reportCacheLock:         sync.Mutex{},
    51  		reportCache:             cache.New(),
    52  		isInitialized:           false,
    53  		offlineReportDateFormat: offlineReportDateFormat,
    54  		currTimeFunc:            time.Now,
    55  	}
    56  	if agent.GetCentralConfig().GetUsageReportingConfig().UsingQAVars() {
    57  		reportManager.offlineReportDateFormat = qaOfflineReportDateFormat
    58  	}
    59  
    60  	reportManager.initialize()
    61  	return reportManager
    62  }
    63  
    64  func (c *usageReportCache) initialize() {
    65  	reportCache := cache.Load(c.cacheFilePath)
    66  	c.reportCache = reportCache
    67  	c.isInitialized = true
    68  }
    69  
    70  // getEvents - gets the events from the cache, lock before calling this
    71  func (c *usageReportCache) getEvents() UsageEvent {
    72  	var savedLighthouseEvents UsageEvent
    73  
    74  	savedEventString, err := c.reportCache.Get(eventsKey)
    75  	if err != nil {
    76  		return UsageEvent{Report: map[string]UsageReport{}}
    77  	}
    78  
    79  	err = json.Unmarshal([]byte(savedEventString.(string)), &savedLighthouseEvents)
    80  	if err != nil {
    81  		return UsageEvent{Report: map[string]UsageReport{}}
    82  	}
    83  	return savedLighthouseEvents
    84  }
    85  
    86  // loadEvents - locks the cache before getting the events
    87  func (c *usageReportCache) loadEvents() UsageEvent {
    88  	if !agent.GetCentralConfig().GetUsageReportingConfig().CanPublish() {
    89  		return UsageEvent{Report: map[string]UsageReport{}}
    90  	}
    91  	c.reportCacheLock.Lock()
    92  	defer c.reportCacheLock.Unlock()
    93  
    94  	return c.getEvents()
    95  }
    96  
    97  // setEvents - sets the events in the cache and saves the cache to the disk, lock the cache before calling this
    98  func (c *usageReportCache) setEvents(lighthouseEvent UsageEvent) {
    99  	eventBytes, err := json.Marshal(lighthouseEvent)
   100  	if err != nil {
   101  		return
   102  	}
   103  	c.reportCache.Set(eventsKey, string(eventBytes))
   104  	c.reportCache.Save(c.cacheFilePath)
   105  }
   106  
   107  // updateEvents - locks the cache before setting the new light house events in the cache
   108  func (c *usageReportCache) updateEvents(lighthouseEvent UsageEvent) {
   109  	if !c.isInitialized || !agent.GetCentralConfig().GetUsageReportingConfig().CanPublish() {
   110  		return
   111  	}
   112  
   113  	c.reportCacheLock.Lock()
   114  	defer c.reportCacheLock.Unlock()
   115  
   116  	c.setEvents(lighthouseEvent)
   117  }
   118  
   119  func (c *usageReportCache) setLastPublishTimestamp(lastPublishTimestamp time.Time) {
   120  	c.reportCache.Set(lastPublishTimestampKey, lastPublishTimestamp)
   121  	c.reportCache.Save(c.cacheFilePath)
   122  }
   123  
   124  func (c *usageReportCache) getLastPublishTimestamp() time.Time {
   125  	c.reportCacheLock.Lock()
   126  	defer c.reportCacheLock.Unlock()
   127  
   128  	lastPublishTime, err := parseTimeFromCache(c.reportCache, lastPublishTimestampKey)
   129  	if err != nil {
   130  		return time.Time{}
   131  	}
   132  
   133  	return lastPublishTime
   134  }
   135  
   136  func (c *usageReportCache) generateReportPath(timestamp ISO8601Time, index int) string {
   137  	format := "%s_%s"
   138  	if index != 0 {
   139  		format = "%s_" + strconv.Itoa(index) + "_%s"
   140  	}
   141  	return path.Join(traceability.GetReportsDirPath(), fmt.Sprintf(format, time.Time(timestamp).Format(c.offlineReportDateFormat), offlineReportSuffix))
   142  }
   143  
   144  // validateReport - copies usage events setting all usages to 0 for any missing time interval
   145  func (c *usageReportCache) validateReport(savedEvents UsageEvent) UsageEvent {
   146  	reportDuration := time.Duration(savedEvents.Granularity * int(time.Millisecond))
   147  
   148  	// order all the keys, this will be used to find any missing times
   149  	orderedKeys := make([]string, 0, len(savedEvents.Report))
   150  	for k := range savedEvents.Report {
   151  		orderedKeys = append(orderedKeys, k)
   152  	}
   153  	sort.Strings(orderedKeys)
   154  
   155  	// create an empty report to insert when necessary
   156  	emptyReport := UsageReport{
   157  		Product: savedEvents.Report[orderedKeys[0]].Product,
   158  		Usage:   make(map[string]int64),
   159  		Meta:    savedEvents.Report[orderedKeys[0]].Meta,
   160  	}
   161  	for usage := range savedEvents.Report[orderedKeys[0]].Usage {
   162  		emptyReport.Usage[usage] = 0
   163  	}
   164  
   165  	curDate, _ := time.Parse(ISO8601, orderedKeys[0])
   166  	lastDate, _ := time.Parse(ISO8601, orderedKeys[len(orderedKeys)-1])
   167  	for curDate.Before(lastDate) {
   168  		curDateString := curDate.Format(ISO8601)
   169  		if _, exists := savedEvents.Report[curDateString]; !exists {
   170  			savedEvents.Report[curDateString] = emptyReport
   171  		}
   172  		curDate = curDate.Add(reportDuration)
   173  	}
   174  	return savedEvents
   175  }
   176  
   177  // addReport - adds a new report to the cache
   178  func (c *usageReportCache) addReport(event UsageEvent) error {
   179  	// Open and load the existing usage file
   180  	savedEvents := c.loadEvents()
   181  
   182  	for key, report := range event.Report {
   183  		savedEvents.Report[key] = report
   184  	}
   185  	// Put all reports into the new event
   186  	event.Report = savedEvents.Report
   187  
   188  	// Update the cache
   189  	c.updateEvents(event)
   190  
   191  	return nil
   192  }
   193  
   194  // saveReport - creates a new file with the latest cached events then clears all reports from the cache, lock outside of this
   195  func (c *usageReportCache) saveReport() error {
   196  	c.reportCacheLock.Lock()
   197  	defer c.reportCacheLock.Unlock()
   198  	savedEvents := c.getEvents()
   199  
   200  	// no reports yet, skip creating the event
   201  	if len(savedEvents.Report) == 0 {
   202  		return nil
   203  	}
   204  	savedEvents = c.validateReport(savedEvents)
   205  
   206  	// create the path to save the file
   207  	outputFilePath := ""
   208  	i := 0
   209  	fileExists := true
   210  	for fileExists {
   211  		outputFilePath = c.generateReportPath(savedEvents.Timestamp, i)
   212  		_, err := os.Stat(outputFilePath)
   213  		i++
   214  		fileExists = !os.IsNotExist(err)
   215  	}
   216  
   217  	// create the new file to save the events
   218  	file, err := os.Create(filepath.Clean(outputFilePath))
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	// marshal the event into json bytes
   224  	cacheBytes, err := json.Marshal(savedEvents)
   225  	if err != nil {
   226  		file.Close()
   227  		return err
   228  	}
   229  
   230  	// save the bytes and close the file
   231  	_, err = io.Copy(file, bytes.NewReader(cacheBytes))
   232  	file.Close()
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	// clear out all reports
   238  	savedEvents.Report = make(map[string]UsageReport)
   239  	c.setEvents(savedEvents)
   240  	return nil
   241  }
   242  
   243  // sendReport - creates a new report with the latest cached events then clears all reports from the cache, lock outside of this
   244  func (c *usageReportCache) sendReport(publishFunc func(event UsageEvent) error) error {
   245  	c.reportCacheLock.Lock()
   246  	defer c.reportCacheLock.Unlock()
   247  	savedEvents := c.getEvents()
   248  
   249  	// no reports yet, skip creating the event
   250  	if len(savedEvents.Report) == 0 {
   251  		return nil
   252  	}
   253  	savedEvents = c.validateReport(savedEvents)
   254  	if err := publishFunc(savedEvents); err != nil {
   255  		c.logger.Error("could not publish usage, will send at next scheduled publishing")
   256  		return nil
   257  	}
   258  
   259  	// update the publish time
   260  	lastPublishTime := time.Now()
   261  	c.setLastPublishTimestamp(lastPublishTime)
   262  
   263  	savedEvents.Report = make(map[string]UsageReport)
   264  	c.setEvents(savedEvents)
   265  	return nil
   266  }
   267  
   268  func (c *usageReportCache) shouldPublish(schedule string) bool {
   269  	currentTime := c.currTimeFunc()
   270  	lastPublishTimestamp := c.getLastPublishTimestamp()
   271  
   272  	// if the last publish was made more than a day ago, publish
   273  	elapsedTimeSinceLastPublish := currentTime.Sub(lastPublishTimestamp)
   274  	if lastPublishTimestamp.IsZero() || elapsedTimeSinceLastPublish >= 24*time.Hour {
   275  		return true
   276  	}
   277  
   278  	cronSchedule, err := cronexpr.Parse(schedule)
   279  	if err != nil {
   280  		return false
   281  	}
   282  	// publish if last scheduled time is past
   283  	nextPublishTime := cronSchedule.Next(lastPublishTimestamp)
   284  	if nextPublishTime.Before(currentTime) {
   285  		return true
   286  	}
   287  
   288  	return false
   289  }