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  }