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