github.com/rpdict/ponzu@v0.10.1-0.20190226054626-477f29d6bf5e/system/api/analytics/init.go (about)

     1  // Package analytics provides the methods to run an analytics reporting system
     2  // for API requests which may be useful to users for measuring access and
     3  // possibly identifying bad actors abusing requests.
     4  package analytics
     5  
     6  import (
     7  	"encoding/json"
     8  	"log"
     9  	"net/http"
    10  	"runtime"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/boltdb/bolt"
    15  )
    16  
    17  type apiRequest struct {
    18  	URL        string `json:"url"`
    19  	Method     string `json:"http_method"`
    20  	Origin     string `json:"origin"`
    21  	Proto      string `json:"http_protocol"`
    22  	RemoteAddr string `json:"ip_address"`
    23  	Timestamp  int64  `json:"timestamp"`
    24  	External   bool   `json:"external_content"`
    25  }
    26  
    27  type apiMetric struct {
    28  	Date   string `json:"date"`
    29  	Total  int    `json:"total"`
    30  	Unique int    `json:"unique"`
    31  }
    32  
    33  var (
    34  	store       *bolt.DB
    35  	requestChan chan apiRequest
    36  )
    37  
    38  // RANGE determines the number of days ponzu request analytics and metrics are
    39  // stored and displayed within the system
    40  const RANGE = 14
    41  
    42  // Record queues an apiRequest for metrics
    43  func Record(req *http.Request) {
    44  	external := strings.Contains(req.URL.Path, "/external/")
    45  
    46  	ts := int64(time.Nanosecond) * time.Now().UnixNano() / int64(time.Millisecond)
    47  
    48  	r := apiRequest{
    49  		URL:        req.URL.String(),
    50  		Method:     req.Method,
    51  		Origin:     req.Header.Get("Origin"),
    52  		Proto:      req.Proto,
    53  		RemoteAddr: req.RemoteAddr,
    54  		Timestamp:  ts,
    55  		External:   external,
    56  	}
    57  
    58  	// put r on buffered requestChan to take advantage of batch insertion in DB
    59  	requestChan <- r
    60  }
    61  
    62  // Close exports the abillity to close our db file. Should be called with defer
    63  // after call to Init() from the same place.
    64  func Close() {
    65  	err := store.Close()
    66  	if err != nil {
    67  		log.Println(err)
    68  	}
    69  }
    70  
    71  // Init creates a db connection, initializes the db with schema and data and
    72  // sets up the queue/batching channel
    73  func Init() {
    74  	var err error
    75  	store, err = bolt.Open("analytics.db", 0666, nil)
    76  	if err != nil {
    77  		log.Fatalln(err)
    78  	}
    79  
    80  	err = store.Update(func(tx *bolt.Tx) error {
    81  		_, err := tx.CreateBucketIfNotExists([]byte("__requests"))
    82  		if err != nil {
    83  			return err
    84  		}
    85  
    86  		_, err = tx.CreateBucketIfNotExists([]byte("__metrics"))
    87  		if err != nil {
    88  			return err
    89  		}
    90  
    91  		return nil
    92  	})
    93  	if err != nil {
    94  		log.Fatalln("Error idempotently creating requests bucket in analytics.db:", err)
    95  	}
    96  
    97  	requestChan = make(chan apiRequest, 1024*64*runtime.NumCPU())
    98  
    99  	go serve()
   100  
   101  	if err != nil {
   102  		log.Fatalln(err)
   103  	}
   104  }
   105  
   106  func serve() {
   107  	// make timer to notify select to batch request insert from requestChan
   108  	// interval: 30 seconds
   109  	apiRequestTimer := time.NewTicker(time.Second * 30)
   110  
   111  	// make timer to notify select to remove analytics older than RANGE days
   112  	// interval: RANGE/2 days
   113  	// TODO: enable analytics backup service to cloud
   114  	pruneThreshold := time.Hour * 24 * RANGE
   115  	pruneDBTimer := time.NewTicker(pruneThreshold / 2)
   116  
   117  	for {
   118  		select {
   119  		case <-apiRequestTimer.C:
   120  			err := batchInsert(requestChan)
   121  			if err != nil {
   122  				log.Println(err)
   123  			}
   124  
   125  		case <-pruneDBTimer.C:
   126  			err := batchPrune(pruneThreshold)
   127  			if err != nil {
   128  				log.Println(err)
   129  			}
   130  
   131  		case <-time.After(time.Second * 30):
   132  			continue
   133  		}
   134  	}
   135  }
   136  
   137  // ChartData returns the map containing decoded javascript needed to chart RANGE
   138  // days of data by day
   139  func ChartData() (map[string]interface{}, error) {
   140  	// set thresholds for today and the RANGE-1 days preceding
   141  	times := [RANGE]time.Time{}
   142  	dates := [RANGE]string{}
   143  	now := time.Now()
   144  	today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
   145  
   146  	ips := [RANGE]map[string]struct{}{}
   147  	for i := range ips {
   148  		ips[i] = make(map[string]struct{})
   149  	}
   150  
   151  	total := [RANGE]int{}
   152  	unique := [RANGE]int{}
   153  
   154  	for i := range times {
   155  		// subtract 24 * i hours to make days prior
   156  		dur := time.Duration(24 * i * -1)
   157  		day := today.Add(time.Hour * dur)
   158  
   159  		// day threshold is [...n-1-i, n-1, n]
   160  		times[len(times)-1-i] = day
   161  		dates[len(times)-1-i] = day.Format("01/02")
   162  	}
   163  
   164  	// get api request analytics and metrics from db
   165  	var requests = []apiRequest{}
   166  	currentMetrics := make(map[string]apiMetric)
   167  
   168  	err := store.Update(func(tx *bolt.Tx) error {
   169  		m := tx.Bucket([]byte("__metrics"))
   170  		b := tx.Bucket([]byte("__requests"))
   171  
   172  		err := m.ForEach(func(k, v []byte) error {
   173  			var metric apiMetric
   174  			err := json.Unmarshal(v, &metric)
   175  			if err != nil {
   176  				log.Println("Error decoding api metric json from analytics db:", err)
   177  				return nil
   178  			}
   179  
   180  			// add metric to currentMetrics map
   181  			currentMetrics[metric.Date] = metric
   182  
   183  			return nil
   184  		})
   185  		if err != nil {
   186  			return err
   187  		}
   188  
   189  		err = b.ForEach(func(k, v []byte) error {
   190  			var r apiRequest
   191  			err := json.Unmarshal(v, &r)
   192  			if err != nil {
   193  				log.Println("Error decoding api request json from analytics db:", err)
   194  				return nil
   195  			}
   196  
   197  			// append request to requests for analysis if its timestamp is today
   198  			// or if its day is not already in cache, otherwise delete it
   199  			d := time.Unix(r.Timestamp/1000, 0)
   200  			_, inCache := currentMetrics[d.Format("01/02")]
   201  			if !d.Before(today) || !inCache {
   202  				requests = append(requests, r)
   203  			} else {
   204  				err := b.Delete(k)
   205  				if err != nil {
   206  					return err
   207  				}
   208  			}
   209  
   210  			return nil
   211  		})
   212  		if err != nil {
   213  			return err
   214  		}
   215  
   216  		return nil
   217  	})
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  CHECK_REQUEST:
   223  	for i := range requests {
   224  		ts := time.Unix(requests[i].Timestamp/1000, 0)
   225  
   226  		for j := range times {
   227  			// if on today, there will be no next iteration to set values for
   228  			// day prior so all valid requests belong to today
   229  			if j == len(times)-1 {
   230  				if ts.After(times[j]) || ts.Equal(times[j]) {
   231  					// do all record keeping
   232  					total[j]++
   233  
   234  					if _, ok := ips[j][requests[i].RemoteAddr]; !ok {
   235  						unique[j]++
   236  						ips[j][requests[i].RemoteAddr] = struct{}{}
   237  					}
   238  
   239  					continue CHECK_REQUEST
   240  				}
   241  			}
   242  
   243  			if ts.Equal(times[j]) {
   244  				// increment total count for current time threshold (day)
   245  				total[j]++
   246  
   247  				// if no IP found for current threshold, increment unique and record IP
   248  				if _, ok := ips[j][requests[i].RemoteAddr]; !ok {
   249  					unique[j]++
   250  					ips[j][requests[i].RemoteAddr] = struct{}{}
   251  				}
   252  
   253  				continue CHECK_REQUEST
   254  			}
   255  
   256  			if ts.Before(times[j]) {
   257  				// check if older than earliest threshold
   258  				if j == 0 {
   259  					continue CHECK_REQUEST
   260  				}
   261  
   262  				// increment total count for previous time threshold (day)
   263  				total[j-1]++
   264  
   265  				// if no IP found for day prior, increment unique and record IP
   266  				if _, ok := ips[j-1][requests[i].RemoteAddr]; !ok {
   267  					unique[j-1]++
   268  					ips[j-1][requests[i].RemoteAddr] = struct{}{}
   269  				}
   270  			}
   271  		}
   272  	}
   273  
   274  	// add data to currentMetrics from total and unique
   275  	for i := range dates {
   276  		_, ok := currentMetrics[dates[i]]
   277  		if !ok {
   278  			m := apiMetric{
   279  				Date:   dates[i],
   280  				Total:  total[i],
   281  				Unique: unique[i],
   282  			}
   283  
   284  			currentMetrics[dates[i]] = m
   285  		}
   286  	}
   287  
   288  	// loop through total and unique to see which dates are accounted for and
   289  	// insert data from metrics array where dates are not
   290  	err = store.Update(func(tx *bolt.Tx) error {
   291  		b := tx.Bucket([]byte("__metrics"))
   292  
   293  		for i := range dates {
   294  			// populate total and unique with cached data if needed
   295  			if total[i] == 0 {
   296  				total[i] = currentMetrics[dates[i]].Total
   297  			}
   298  
   299  			if unique[i] == 0 {
   300  				unique[i] = currentMetrics[dates[i]].Unique
   301  			}
   302  
   303  			// check if we need to insert old data into cache - as long as it
   304  			// is not today's data
   305  			if dates[i] != today.Format("01/02") {
   306  				k := []byte(dates[i])
   307  				if b.Get(k) == nil {
   308  					// keep zero counts out of cache in case data is added from
   309  					// other sources
   310  					if currentMetrics[dates[i]].Total != 0 {
   311  						v, err := json.Marshal(currentMetrics[dates[i]])
   312  						if err != nil {
   313  							return err
   314  						}
   315  
   316  						err = b.Put(k, v)
   317  						if err != nil {
   318  							return err
   319  						}
   320  					}
   321  				}
   322  			}
   323  		}
   324  
   325  		return nil
   326  	})
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	// marshal array counts to js arrays for output to chart
   332  	jsUnique, err := json.Marshal(unique)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	jsTotal, err := json.Marshal(total)
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	return map[string]interface{}{
   343  		"dates":  dates,
   344  		"unique": string(jsUnique),
   345  		"total":  string(jsTotal),
   346  		"from":   dates[0],
   347  		"to":     dates[len(dates)-1],
   348  	}, nil
   349  }