statistic.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package statistic
  2. import (
  3. "path/filepath"
  4. "strings"
  5. "sync"
  6. "time"
  7. "imuslab.com/zoraxy/mod/database"
  8. )
  9. /*
  10. Statistic Package
  11. This packet is designed to collection information
  12. and store them for future analysis
  13. */
  14. // Faststat, a interval summary for all collected data and avoid
  15. // looping through every data everytime a overview is needed
  16. type DailySummary struct {
  17. TotalRequest int64 //Total request of the day
  18. ErrorRequest int64 //Invalid request of the day, including error or not found
  19. ValidRequest int64 //Valid request of the day
  20. //Type counters
  21. ForwardTypes *sync.Map //Map that hold the forward types
  22. RequestOrigin *sync.Map //Map that hold [country ISO code]: visitor counter
  23. RequestClientIp *sync.Map //Map that hold all unique request IPs
  24. Referer *sync.Map //Map that store where the user was refered from
  25. UserAgent *sync.Map //Map that store the useragent of the request
  26. RequestURL *sync.Map //Request URL of the request object
  27. }
  28. type RequestInfo struct {
  29. IpAddr string
  30. RequestOriginalCountryISOCode string
  31. Succ bool
  32. StatusCode int
  33. ForwardType string
  34. Referer string
  35. UserAgent string
  36. RequestURL string
  37. Target string
  38. }
  39. type CollectorOption struct {
  40. Database *database.Database
  41. }
  42. type Collector struct {
  43. rtdataStopChan chan bool
  44. DailySummary *DailySummary
  45. Option *CollectorOption
  46. }
  47. func NewStatisticCollector(option CollectorOption) (*Collector, error) {
  48. option.Database.NewTable("stats")
  49. //Create the collector object
  50. thisCollector := Collector{
  51. DailySummary: newDailySummary(),
  52. Option: &option,
  53. }
  54. //Load the stat if exists for today
  55. //This will exists if the program was forcefully restarted
  56. year, month, day := time.Now().Date()
  57. summary := thisCollector.LoadSummaryOfDay(year, month, day)
  58. if summary != nil {
  59. thisCollector.DailySummary = summary
  60. }
  61. //Schedule the realtime statistic clearing at midnight everyday
  62. rtstatStopChan := thisCollector.ScheduleResetRealtimeStats()
  63. thisCollector.rtdataStopChan = rtstatStopChan
  64. return &thisCollector, nil
  65. }
  66. // Write the current in-memory summary to database file
  67. func (c *Collector) SaveSummaryOfDay() {
  68. //When it is called in 0:00am, make sure it is stored as yesterday key
  69. t := time.Now().Add(-30 * time.Second)
  70. summaryKey := t.Format("2006_01_02")
  71. saveData := DailySummaryToExport(*c.DailySummary)
  72. c.Option.Database.Write("stats", summaryKey, saveData)
  73. }
  74. // Load the summary of a day given
  75. func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary {
  76. date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
  77. summaryKey := date.Format("2006_01_02")
  78. targetSummaryExport := DailySummaryExport{}
  79. c.Option.Database.Read("stats", summaryKey, &targetSummaryExport)
  80. targetSummary := DailySummaryExportToSummary(targetSummaryExport)
  81. return &targetSummary
  82. }
  83. // This function gives the current slot in the 288- 5 minutes interval of the day
  84. func (c *Collector) GetCurrentRealtimeStatIntervalId() int {
  85. now := time.Now()
  86. startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).Unix()
  87. secondsSinceStartOfDay := now.Unix() - startOfDay
  88. interval := secondsSinceStartOfDay / (5 * 60)
  89. return int(interval)
  90. }
  91. func (c *Collector) Close() {
  92. //Stop the ticker
  93. c.rtdataStopChan <- true
  94. //Write the buffered data into database
  95. c.SaveSummaryOfDay()
  96. }
  97. // Main function to record all the inbound traffics
  98. // Note that this function run in go routine and might have concurrent R/W issue
  99. // Please make sure there is no racing paramters in this function
  100. func (c *Collector) RecordRequest(ri RequestInfo) {
  101. go func() {
  102. c.DailySummary.TotalRequest++
  103. if ri.Succ {
  104. c.DailySummary.ValidRequest++
  105. } else {
  106. c.DailySummary.ErrorRequest++
  107. }
  108. //Store the request info into correct types of maps
  109. ft, ok := c.DailySummary.ForwardTypes.Load(ri.ForwardType)
  110. if !ok {
  111. c.DailySummary.ForwardTypes.Store(ri.ForwardType, 1)
  112. } else {
  113. c.DailySummary.ForwardTypes.Store(ri.ForwardType, ft.(int)+1)
  114. }
  115. originISO := strings.ToLower(ri.RequestOriginalCountryISOCode)
  116. fo, ok := c.DailySummary.RequestOrigin.Load(originISO)
  117. if !ok {
  118. c.DailySummary.RequestOrigin.Store(originISO, 1)
  119. } else {
  120. c.DailySummary.RequestOrigin.Store(originISO, fo.(int)+1)
  121. }
  122. //Filter out CF forwarded requests
  123. if strings.Contains(ri.IpAddr, ",") {
  124. ips := strings.Split(strings.TrimSpace(ri.IpAddr), ",")
  125. if len(ips) >= 1 && IsValidIPAddress(strings.TrimPrefix(ips[0], "[")) {
  126. //Example when forwarded from CF: 158.250.160.114,109.21.249.211
  127. //Or IPv6 [15c4:cbb4:cc98:4291:ffc1:3a46:06a1:51a7],109.21.249.211
  128. ri.IpAddr = ips[0]
  129. }
  130. }
  131. fi, ok := c.DailySummary.RequestClientIp.Load(ri.IpAddr)
  132. if !ok {
  133. c.DailySummary.RequestClientIp.Store(ri.IpAddr, 1)
  134. } else {
  135. c.DailySummary.RequestClientIp.Store(ri.IpAddr, fi.(int)+1)
  136. }
  137. //Record the referer
  138. rf, ok := c.DailySummary.Referer.Load(ri.Referer)
  139. if !ok {
  140. c.DailySummary.Referer.Store(ri.Referer, 1)
  141. } else {
  142. c.DailySummary.Referer.Store(ri.Referer, rf.(int)+1)
  143. }
  144. //Record the UserAgent
  145. ua, ok := c.DailySummary.UserAgent.Load(ri.UserAgent)
  146. if !ok {
  147. c.DailySummary.UserAgent.Store(ri.UserAgent, 1)
  148. } else {
  149. c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1)
  150. }
  151. //ADD MORE HERE IF NEEDED
  152. //Record request URL, if it is a page
  153. ext := filepath.Ext(ri.RequestURL)
  154. if ext != "" && !isWebPageExtension(ext) {
  155. return
  156. }
  157. ru, ok := c.DailySummary.RequestURL.Load(ri.RequestURL)
  158. if !ok {
  159. c.DailySummary.RequestURL.Store(ri.RequestURL, 1)
  160. } else {
  161. c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1)
  162. }
  163. }()
  164. }
  165. // nightly task
  166. func (c *Collector) ScheduleResetRealtimeStats() chan bool {
  167. doneCh := make(chan bool)
  168. go func() {
  169. defer close(doneCh)
  170. for {
  171. // calculate duration until next midnight
  172. now := time.Now()
  173. // Get midnight of the next day in the local time zone
  174. midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
  175. // Calculate the duration until midnight
  176. duration := midnight.Sub(now)
  177. select {
  178. case <-time.After(duration):
  179. // store daily summary to database and reset summary
  180. c.SaveSummaryOfDay()
  181. c.DailySummary = newDailySummary()
  182. case <-doneCh:
  183. // stop the routine
  184. return
  185. }
  186. }
  187. }()
  188. return doneCh
  189. }
  190. func newDailySummary() *DailySummary {
  191. return &DailySummary{
  192. TotalRequest: 0,
  193. ErrorRequest: 0,
  194. ValidRequest: 0,
  195. ForwardTypes: &sync.Map{},
  196. RequestOrigin: &sync.Map{},
  197. RequestClientIp: &sync.Map{},
  198. Referer: &sync.Map{},
  199. UserAgent: &sync.Map{},
  200. RequestURL: &sync.Map{},
  201. }
  202. }