chiark / gitweb /
Improved some logging levels for stats
[fdroidserver.git] / fdroidserver / stats.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # stats.py - part of the FDroid server tools
5 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Affero General Public License for more details.
16 #
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import sys
21 import os
22 import re
23 import time
24 import traceback
25 import glob
26 from optparse import OptionParser
27 import paramiko
28 import socket
29 import logging
30 import common, metadata
31 import subprocess
32 from collections import Counter
33
34 def carbon_send(key, value):
35     s = socket.socket()
36     s.connect((config['carbon_host'], config['carbon_port']))
37     msg = '%s %d %d\n' % (key, value, int(time.time()))
38     s.sendall(msg)
39     s.close()
40
41 options = None
42 config = None
43
44 def main():
45
46     global options, config
47
48     # Parse command line...
49     parser = OptionParser()
50     parser.add_option("-v", "--verbose", action="store_true", default=False,
51                       help="Spew out even more information than normal")
52     parser.add_option("-q", "--quiet", action="store_true", default=False,
53                       help="Restrict output to warnings and errors")
54     parser.add_option("-d", "--download", action="store_true", default=False,
55                       help="Download logs we don't have")
56     parser.add_option("--nologs", action="store_true", default=False,
57                       help="Don't do anything logs-related")
58     (options, args) = parser.parse_args()
59
60     config = common.read_config(options)
61
62     if not config['update_stats']:
63         logging.info("Stats are disabled - check your configuration")
64         sys.exit(1)
65
66     # Get all metadata-defined apps...
67     metaapps = metadata.read_metadata(options.verbose)
68
69     statsdir = 'stats'
70     logsdir = os.path.join(statsdir, 'logs')
71     datadir = os.path.join(statsdir, 'data')
72     if not os.path.exists(statsdir):
73         os.mkdir(statsdir)
74     if not os.path.exists(logsdir):
75         os.mkdir(logsdir)
76     if not os.path.exists(datadir):
77         os.mkdir(datadir)
78
79     if options.download:
80         # Get any access logs we don't have...
81         ssh = None
82         ftp = None
83         try:
84             logging.info('Retrieving logs')
85             ssh = paramiko.SSHClient()
86             ssh.load_system_host_keys()
87             ssh.connect('f-droid.org', username='fdroid', timeout=10,
88                     key_filename=config['webserver_keyfile'])
89             ftp = ssh.open_sftp()
90             ftp.get_channel().settimeout(60)
91             logging.info("...connected")
92
93             ftp.chdir('logs')
94             files = ftp.listdir()
95             for f in files:
96                 if f.startswith('access-') and f.endswith('.log.gz'):
97
98                     destpath = os.path.join(logsdir, f)
99                     destsize = ftp.stat(f).st_size
100                     if (not os.path.exists(destpath) or
101                             os.path.getsize(destpath) != destsize):
102                         logging.debug("...retrieving " + f)
103                         ftp.get(f, destpath)
104         except Exception:
105             traceback.print_exc()
106             sys.exit(1)
107         finally:
108             #Disconnect
109             if ftp is not None:
110                 ftp.close()
111             if ssh is not None:
112                 ssh.close()
113
114     knownapks = common.KnownApks()
115     unknownapks = []
116
117     if not options.nologs:
118         # Process logs
119         logging.info('Processing logs...')
120         appscount = Counter()
121         appsvercount = Counter()
122         logexpr = '(?P<ip>[.:0-9a-fA-F]+) - - \[(?P<time>.*?)\] "GET (?P<uri>.*?) HTTP/1.\d" (?P<statuscode>\d+) \d+ "(?P<referral>.*?)" "(?P<useragent>.*?)"'
123         logsearch = re.compile(logexpr).search
124         for logfile in glob.glob(os.path.join(logsdir,'access-*.log.gz')):
125             logging.debug('...' + logfile)
126             if options.verbose:
127                 print '...' + logfile
128             p = subprocess.Popen(["zcat", logfile], stdout = subprocess.PIPE)
129             matches = (logsearch(line) for line in p.stdout)
130             for match in matches:
131                 if match and match.group('statuscode') == '200':
132                     uri = match.group('uri')
133                     if uri.endswith('.apk'):
134                         _, apkname = os.path.split(uri)
135                         app = knownapks.getapp(apkname)
136                         if app:
137                             appid, _ = app
138                             appscount[appid] += 1
139                             # Strip the '.apk' from apkname
140                             appver = apkname[:-4]
141                             appsvercount[appver] += 1
142                         else:
143                             if not apkname in unknownapks:
144                                 unknownapks.append(apkname)
145
146         # Calculate and write stats for total downloads...
147         lst = []
148         alldownloads = 0
149         for appid in appscount:
150             count = appscount[appid]
151             lst.append(app + " " + str(count))
152             if config['stats_to_carbon']:
153                 carbon_send('fdroid.download.' + app.replace('.', '_'), count)
154             alldownloads += count
155         lst.append("ALL " + str(alldownloads))
156         f = open('stats/total_downloads_app.txt', 'w')
157         f.write('# Total downloads by application, since October 2011\n')
158         for line in sorted(lst):
159             f.write(line + '\n')
160         f.close()
161
162         f = open('stats/total_downloads_app_version.txt', 'w')
163         f.write('# Total downloads by application and version, since October 2011\n')
164         lst = []
165         for appver in appsvercount:
166             count = appsvercount[appver]
167             lst.append(appver + " " + str(count))
168         for line in sorted(lst):
169             f.write(line + "\n")
170         f.close()
171
172     # Calculate and write stats for repo types...
173     logging.info("Processing repo types...")
174     repotypes = Counter()
175     for app in metaapps:
176         if len(app['Repo Type']) == 0:
177             rtype = 'none'
178         else:
179             if app['Repo Type'] == 'srclib':
180                 rtype = common.getsrclibvcs(app['Repo'])
181             else:
182                 rtype = app['Repo Type']
183         repotypes[rtype] += 1
184     f = open('stats/repotypes.txt', 'w')
185     for rtype in repotypes:
186         count = repotypes[rtype]
187         f.write(rtype + ' ' + str(count) + '\n')
188     f.close()
189
190     # Calculate and write stats for update check modes...
191     logging.info("Processing update check modes...")
192     ucms = Counter()
193     for app in metaapps:
194         checkmode = app['Update Check Mode']
195         if checkmode.startswith('RepoManifest/'):
196             checkmode = checkmode[:12]
197         if checkmode.startswith('Tags '):
198             checkmode = checkmode[:4]
199         ucms[checkmode] += 1;
200     f = open('stats/update_check_modes.txt', 'w')
201     for checkmode in ucms:
202         count = ucms[checkmode]
203         f.write(checkmode + ' ' + str(count) + '\n')
204     f.close()
205
206     logging.info("Processing categories...")
207     ctgs = Counter()
208     for app in metaapps:
209         for category in app['Categories']:
210             ctgs[category] += 1;
211     f = open('stats/categories.txt', 'w')
212     for category in ctgs:
213         count = ctgs[category]
214         f.write(category + ' ' + str(count) + '\n')
215     f.close()
216
217     logging.info("Processing antifeatures...")
218     afs = Counter()
219     for app in metaapps:
220         if app['AntiFeatures'] is None:
221             continue
222         antifeatures = [a.strip() for a in app['AntiFeatures'].split(',')]
223         for antifeature in antifeatures:
224             afs[antifeature] += 1;
225     f = open('stats/antifeatures.txt', 'w')
226     for antifeature in afs:
227         count = afs[antifeature]
228         f.write(antifeature + ' ' + str(count) + '\n')
229     f.close()
230
231     # Calculate and write stats for licenses...
232     logging.info("Processing licenses...")
233     licenses = Counter()
234     for app in metaapps:
235         license = app['License']
236         licenses[license] += 1;
237     f = open('stats/licenses.txt', 'w')
238     for license in licenses:
239         count = licenses[license]
240         f.write(license + ' ' + str(count) + '\n')
241     f.close()
242
243     # Write list of latest apps added to the repo...
244     logging.info("Processing latest apps...")
245     latest = knownapks.getlatest(10)
246     f = open('stats/latestapps.txt', 'w')
247     for app in latest:
248         f.write(app + '\n')
249     f.close()
250
251     if unknownapks:
252         logging.info('\nUnknown apks:')
253         for apk in unknownapks:
254             logging.info(apk)
255
256     logging.info("Finished.")
257
258 if __name__ == "__main__":
259     main()
260