chiark / gitweb /
Merge branch 'metadata-checks'
[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     return
214
215     # Calculate and write stats for licenses...
216     licenses = {}
217     for app in metaapps:
218         license = app['License']
219         if license in licenses:
220             licenses[license] += 1;
221         else:
222             licenses[license] = 1
223     f = open('stats/licenses.txt', 'w')
224     for license, count in licenses.iteritems():
225         f.write(license + ' ' + str(count) + '\n')
226     f.close()
227
228     # Write list of latest apps added to the repo...
229     latest = knownapks.getlatest(10)
230     f = open('stats/latestapps.txt', 'w')
231     for app in latest:
232         f.write(app + '\n')
233     f.close()
234
235     if len(unknownapks) > 0:
236         print '\nUnknown apks:'
237         for apk in unknownapks:
238             print apk
239
240     print "Finished."
241
242 if __name__ == "__main__":
243     main()
244