chiark / gitweb /
Fix latestapps stats, broken in 59cde15ad1
[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 common, metadata
29 import socket
30 import subprocess
31
32 def carbon_send(key, value):
33     s = socket.socket()
34     s.connect((config['carbon_host'], config['carbon_port']))
35     msg = '%s %d %d\n' % (key, value, int(time.time()))
36     s.sendall(msg)
37     s.close()
38
39 options = None
40 config = None
41
42 def main():
43
44     global options, config
45
46     # Parse command line...
47     parser = OptionParser()
48     parser.add_option("-v", "--verbose", action="store_true", default=False,
49                       help="Spew out even more information than normal")
50     parser.add_option("-d", "--download", action="store_true", default=False,
51                       help="Download logs we don't have")
52     (options, args) = parser.parse_args()
53
54     config = common.read_config(options)
55
56     if not config['update_stats']:
57         print "Stats are disabled - check your configuration"
58         sys.exit(1)
59
60     # Get all metadata-defined apps...
61     metaapps = metadata.read_metadata(options.verbose)
62
63     statsdir = 'stats'
64     logsdir = os.path.join(statsdir, 'logs')
65     datadir = os.path.join(statsdir, 'data')
66     if not os.path.exists(statsdir):
67         os.mkdir(statsdir)
68     if not os.path.exists(logsdir):
69         os.mkdir(logsdir)
70     if not os.path.exists(datadir):
71         os.mkdir(datadir)
72
73     if options.download:
74         # Get any access logs we don't have...
75         ssh = None
76         ftp = None
77         try:
78             print 'Retrieving logs'
79             ssh = paramiko.SSHClient()
80             ssh.load_system_host_keys()
81             ssh.connect('f-droid.org', username='fdroid', timeout=10,
82                     key_filename=config['webserver_keyfile'])
83             ftp = ssh.open_sftp()
84             ftp.get_channel().settimeout(60)
85             print "...connected"
86
87             ftp.chdir('logs')
88             files = ftp.listdir()
89             for f in files:
90                 if f.startswith('access-') and f.endswith('.log.gz'):
91
92                     destpath = os.path.join(logsdir, f)
93                     destsize = ftp.stat(f).st_size
94                     if (not os.path.exists(destpath) or
95                             os.path.getsize(destpath) != destsize):
96                         print "...retrieving " + f
97                         ftp.get(f, destpath)
98         except Exception:
99             traceback.print_exc()
100             sys.exit(1)
101         finally:
102             #Disconnect
103             if ftp is not None:
104                 ftp.close()
105             if ssh is not None:
106                 ssh.close()
107
108     # Process logs
109     if options.verbose:
110         print 'Processing logs...'
111     logexpr = '(?P<ip>[.:0-9a-fA-F]+) - - \[(?P<time>.*?)\] "GET (?P<uri>.*?) HTTP/1.\d" (?P<statuscode>\d+) \d+ "(?P<referral>.*?)" "(?P<useragent>.*?)"'
112     logsearch = re.compile(logexpr).search
113     apps = {}
114     unknownapks = []
115     knownapks = common.KnownApks()
116     for logfile in glob.glob(os.path.join(logsdir,'access-*.log.gz')):
117         if options.verbose:
118             print '...' + logfile
119         p = subprocess.Popen(["zcat", logfile], stdout = subprocess.PIPE)
120         matches = (logsearch(line) for line in p.stdout)
121         for match in matches:
122             if match and match.group('statuscode') == '200':
123                 uri = match.group('uri')
124                 if uri.endswith('.apk'):
125                     _, apkname = os.path.split(uri)
126                     app = knownapks.getapp(apkname)
127                     if app:
128                         appid, _ = app
129                         if appid in apps:
130                             apps[appid] += 1
131                         else:
132                             apps[appid] = 1
133                     else:
134                         if not apkname in unknownapks:
135                             unknownapks.append(apkname)
136
137     # Calculate and write stats for total downloads...
138     lst = []
139     alldownloads = 0
140     for app, count in apps.iteritems():
141         lst.append(app + " " + str(count))
142         if config['stats_to_carbon']:
143             carbon_send('fdroid.download.' + app.replace('.', '_'), count)
144         alldownloads += count
145     lst.append("ALL " + str(alldownloads))
146     f = open('stats/total_downloads_app.txt', 'w')
147     f.write('# Total downloads by application, since October 2011\n')
148     for line in sorted(lst):
149         f.write(line + '\n')
150     f.close()
151
152     # Calculate and write stats for repo types...
153     repotypes = {}
154     for app in metaapps:
155         if len(app['Repo Type']) == 0:
156             rtype = 'none'
157         else:
158             if app['Repo Type'] == 'srclib':
159                 rtype = common.getsrclibvcs(app['Repo'])
160             else:
161                 rtype = app['Repo Type']
162         if rtype in repotypes:
163             repotypes[rtype] += 1;
164         else:
165             repotypes[rtype] = 1
166     f = open('stats/repotypes.txt', 'w')
167     for rtype, count in repotypes.iteritems():
168         f.write(rtype + ' ' + str(count) + '\n')
169     f.close()
170
171     # Calculate and write stats for update check modes...
172     ucms = {}
173     for app in metaapps:
174         checkmode = app['Update Check Mode'].split('/')[0]
175         if checkmode in ucms:
176             ucms[checkmode] += 1;
177         else:
178             ucms[checkmode] = 1
179     f = open('stats/update_check_modes.txt', 'w')
180     for checkmode, count in ucms.iteritems():
181         f.write(checkmode + ' ' + str(count) + '\n')
182     f.close()
183
184     ctgs = {}
185     for app in metaapps:
186         if app['Categories'] is None:
187             continue
188         categories = [c.strip() for c in app['Categories'].split(',')]
189         for category in categories:
190             if category in ctgs:
191                 ctgs[category] += 1;
192             else:
193                 ctgs[category] = 1
194     f = open('stats/categories.txt', 'w')
195     for category, count in ctgs.iteritems():
196         f.write(category + ' ' + str(count) + '\n')
197     f.close()
198
199     afs = {}
200     for app in metaapps:
201         if app['AntiFeatures'] is None:
202             continue
203         antifeatures = [a.strip() for a in app['AntiFeatures'].split(',')]
204         for antifeature in antifeatures:
205             if antifeature in afs:
206                 afs[antifeature] += 1;
207             else:
208                 afs[antifeature] = 1
209     f = open('stats/antifeatures.txt', 'w')
210     for antifeature, count in afs.iteritems():
211         f.write(antifeature + ' ' + str(count) + '\n')
212     f.close()
213
214     # Calculate and write stats for licenses...
215     licenses = {}
216     for app in metaapps:
217         license = app['License']
218         if license in licenses:
219             licenses[license] += 1;
220         else:
221             licenses[license] = 1
222     f = open('stats/licenses.txt', 'w')
223     for license, count in licenses.iteritems():
224         f.write(license + ' ' + str(count) + '\n')
225     f.close()
226
227     # Write list of latest apps added to the repo...
228     latest = knownapks.getlatest(10)
229     f = open('stats/latestapps.txt', 'w')
230     for app in latest:
231         f.write(app + '\n')
232     f.close()
233
234     if len(unknownapks) > 0:
235         print '\nUnknown apks:'
236         for apk in unknownapks:
237             print apk
238
239     print "Finished."
240
241 if __name__ == "__main__":
242     main()
243