github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/transferstats/collector.go (about) 1 /* 2 * Copyright (c) 2015, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package transferstats 21 22 import ( 23 "sync" 24 ) 25 26 // TODO: Stats for a server are only removed when they are sent in a status 27 // update to that server. So if there's an unexpected disconnect from serverA 28 // and then a reconnect to serverB, the stats for serverA will never get sent 29 // (unless there's later a reconnect to serverA). That means the stats for 30 // serverA will never get deleted and the memory won't get freed. This is only 31 // a small amount of memory (< 1KB, probably), but we should still probably add 32 // some kind of stale-stats cleanup. 33 34 // Per-host/domain stats. 35 // Note that the bytes we're counting are the ones going into the tunnel, so do 36 // not include transport overhead. 37 type hostStats struct { 38 numBytesSent int64 39 numBytesReceived int64 40 } 41 42 // AccumulatedStats holds the Psiphon Server API status request data for a 43 // given server. To accommodate status requests that may fail, and be retried, 44 // the TakeOutStatsForServer/PutBackStatsForServer procedure allows the requester 45 // to check out stats for reporting and merge back stats for a later retry. 46 type AccumulatedStats struct { 47 hostnameToStats map[string]*hostStats 48 } 49 50 // GetStatsForStatusRequest summarizes AccumulatedStats data as 51 // required for the Psiphon Server API status request. 52 func (stats AccumulatedStats) GetStatsForStatusRequest() map[string]int64 { 53 54 hostBytes := make(map[string]int64) 55 56 for hostname, hostStats := range stats.hostnameToStats { 57 totalBytes := hostStats.numBytesReceived + hostStats.numBytesSent 58 hostBytes[hostname] = totalBytes 59 } 60 61 return hostBytes 62 } 63 64 // serverStats holds per-server stats. 65 // accumulatedStats data is payload for the Psiphon status request 66 // which is accessed via TakeOut/PutBack. 67 // recentBytes data is for tunnel monitoring which is accessed via 68 // ReportRecentBytesTransferredForServer. 69 type serverStats struct { 70 accumulatedStats *AccumulatedStats 71 recentBytesSent int64 72 recentBytesReceived int64 73 } 74 75 // allStats is the root object that holds stats for all servers and all hosts, 76 // as well as the mutex to access them. 77 var allStats = struct { 78 statsMutex sync.RWMutex 79 serverIDtoStats map[string]*serverStats 80 }{serverIDtoStats: make(map[string]*serverStats)} 81 82 // statsUpdate contains new stats counts to be aggregated. 83 type statsUpdate struct { 84 serverID string 85 hostname string 86 numBytesSent int64 87 numBytesReceived int64 88 } 89 90 // recordStats makes sure the given stats update is added to the global 91 // collection. recentBytes are not adjusted when isPutBack is true, 92 // as recentBytes aren't subject to TakeOut/PutBack. 93 func recordStat(stat *statsUpdate, isRecordingHostBytes, isPutBack bool) { 94 allStats.statsMutex.Lock() 95 defer allStats.statsMutex.Unlock() 96 97 storedServerStats := allStats.serverIDtoStats[stat.serverID] 98 if storedServerStats == nil { 99 storedServerStats = &serverStats{ 100 accumulatedStats: &AccumulatedStats{ 101 hostnameToStats: make(map[string]*hostStats)}} 102 allStats.serverIDtoStats[stat.serverID] = storedServerStats 103 } 104 105 if isRecordingHostBytes { 106 107 if stat.hostname == "" { 108 stat.hostname = "(OTHER)" 109 } 110 111 storedHostStats := storedServerStats.accumulatedStats.hostnameToStats[stat.hostname] 112 if storedHostStats == nil { 113 storedHostStats = &hostStats{} 114 storedServerStats.accumulatedStats.hostnameToStats[stat.hostname] = storedHostStats 115 } 116 117 storedHostStats.numBytesSent += stat.numBytesSent 118 storedHostStats.numBytesReceived += stat.numBytesReceived 119 } 120 121 if !isPutBack { 122 storedServerStats.recentBytesSent += stat.numBytesSent 123 storedServerStats.recentBytesReceived += stat.numBytesReceived 124 } 125 } 126 127 // ReportRecentBytesTransferredForServer returns bytes sent and received since 128 // the last call to ReportRecentBytesTransferredForServer. The accumulated sent 129 // and received are reset to 0 by this call. 130 func ReportRecentBytesTransferredForServer(serverID string) (sent, received int64) { 131 allStats.statsMutex.Lock() 132 defer allStats.statsMutex.Unlock() 133 134 stats := allStats.serverIDtoStats[serverID] 135 136 if stats == nil { 137 return 138 } 139 140 sent = stats.recentBytesSent 141 received = stats.recentBytesReceived 142 143 stats.recentBytesSent = 0 144 stats.recentBytesReceived = 0 145 146 return 147 } 148 149 // TakeOutStatsForServer borrows the AccumulatedStats for the specified 150 // server. When we fail to report these stats, resubmit them with 151 // PutBackStatsForServer. Stats will continue to be accumulated between 152 // TakeOut and PutBack calls. The recentBytes values are unaffected by 153 // TakeOut/PutBack. Returns empty stats if the serverID is not found. 154 func TakeOutStatsForServer(serverID string) (accumulatedStats *AccumulatedStats) { 155 allStats.statsMutex.Lock() 156 defer allStats.statsMutex.Unlock() 157 158 newAccumulatedStats := &AccumulatedStats{ 159 hostnameToStats: make(map[string]*hostStats)} 160 161 // Note: for an existing serverStats, only the accumulatedStats is 162 // affected; the recentBytes fields are not changed. 163 serverStats := allStats.serverIDtoStats[serverID] 164 if serverStats != nil { 165 accumulatedStats = serverStats.accumulatedStats 166 serverStats.accumulatedStats = newAccumulatedStats 167 } else { 168 accumulatedStats = newAccumulatedStats 169 } 170 return 171 } 172 173 // PutBackStatsForServer re-adds a set of server stats to the collection. 174 func PutBackStatsForServer(serverID string, accumulatedStats *AccumulatedStats) { 175 for hostname, hoststats := range accumulatedStats.hostnameToStats { 176 recordStat( 177 &statsUpdate{ 178 serverID: serverID, 179 hostname: hostname, 180 numBytesSent: hoststats.numBytesSent, 181 numBytesReceived: hoststats.numBytesReceived, 182 }, 183 // We can set isRecordingHostBytes to true, regardless of whether there 184 // are any regexes, since there will be no host bytes to put back if they 185 // are not being recorded. 186 true, 187 true) 188 } 189 }