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 }