Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
38 / 38 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
AuthManagerStatsdHandler | |
100.00% |
38 / 38 |
|
100.00% |
3 / 3 |
17 | |
100.00% |
1 / 1 |
handle | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
12 | |||
getEntryPoint | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getField | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * Custom logger for counting certain events. |
4 | * |
5 | * (c) Wikimedia Foundation 2015, GPL |
6 | * |
7 | * This program is free software; you can redistribute it and/or modify |
8 | * it under the terms of the GNU General Public License as published by |
9 | * the Free Software Foundation; either version 2 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 General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU General Public License along |
18 | * with this program; if not, write to the Free Software Foundation, Inc., |
19 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
20 | * http://www.gnu.org/copyleft/gpl.html |
21 | * |
22 | * @file |
23 | */ |
24 | |
25 | namespace WikimediaEvents; |
26 | |
27 | use MediaWiki\MediaWikiServices; |
28 | use Monolog\Handler\AbstractHandler; |
29 | |
30 | /** |
31 | * Counts authentication-related log events (those sent to the 'authevents' |
32 | * channel). |
33 | * |
34 | * Events can include the following data in their context: |
35 | * - 'event': (string, required) the type of the event (e.g. 'login'). |
36 | * - 'eventType': (string) a subtype for more complex events. |
37 | * - 'successful': (bool) whether the attempt was successful. |
38 | * - 'status': (string) attempt status (such as an error message key). |
39 | * Will be ignored unless 'successful' is false. |
40 | * |
41 | * Will result in a ping to a statsd counter that looks like |
42 | * <MediaWiki root>.authmanager.<event>.<type>.<entrypoint>.[success|failure].<status> |
43 | * Some segments will be omitted when the appropriate data is not present. |
44 | * <entrypoint> is 'web' or 'api' and filled automatically. |
45 | * |
46 | * Generic stats counters will also be incremented, depending on the event: |
47 | * - 'authmanager_success_total' |
48 | * - 'authmanager_error_total' |
49 | * With the following labels: |
50 | * - 'entrypoint': as described above, 'web' or 'api' |
51 | * - 'event': the type of the event |
52 | * - 'subtype': (can be 'n/a' if no subtype is found) |
53 | * - 'reason': failure reason, set only for errors |
54 | * |
55 | * Used to alert on sudden, unexplained changes in e.g. the number of login |
56 | * errors. |
57 | */ |
58 | class AuthManagerStatsdHandler extends AbstractHandler { |
59 | |
60 | /** |
61 | * @inheritDoc |
62 | */ |
63 | public function handle( array $record ): bool { |
64 | $event = $this->getField( 'event', $record['context'] ); |
65 | $type = $this->getField( [ 'eventType', 'type' ], $record['context'] ); |
66 | $entrypoint = $this->getEntryPoint(); |
67 | $status = $this->getField( 'status', $record['context'] ); |
68 | $successful = $this->getField( 'successful', $record['context'] ); |
69 | $error = null; |
70 | if ( $successful === false ) { |
71 | $error = strval( $status ); |
72 | } |
73 | |
74 | // Sense-check in case this was invoked from some non-metrics-related |
75 | // code by accident |
76 | if ( |
77 | ( $record['channel'] !== 'authevents' && $record['channel'] !== 'captcha' ) |
78 | || !$event || !is_string( $event ) |
79 | || ( $type && !is_string( $type ) ) |
80 | ) { |
81 | return false; |
82 | } |
83 | |
84 | // some key parts can be null and will be removed by array_filter |
85 | $keyParts = [ 'authmanager', $event, $type, $entrypoint ]; |
86 | if ( $successful === true ) { |
87 | $keyParts[] = 'success'; |
88 | $counterName = 'authmanager_success_total'; |
89 | } elseif ( $successful === false ) { |
90 | $counterName = 'authmanager_error_total'; |
91 | $keyParts[] = 'failure'; |
92 | $keyParts[] = $error; |
93 | } else { |
94 | $counterName = 'authmanager_event_total'; |
95 | } |
96 | $statsdKey = implode( '.', array_filter( $keyParts ) ); |
97 | |
98 | // use of this class is set up in operations/mediawiki-config so no nice dependency injection |
99 | $counter = MediaWikiServices::getInstance()->getStatsFactory() |
100 | ->withComponent( 'WikimediaEvents' ) |
101 | ->getCounter( $counterName ) |
102 | ->setLabel( 'entrypoint', $entrypoint ) |
103 | ->setLabel( 'event', $event ) |
104 | ->setLabel( 'subtype', $type ?? 'n/a' ); |
105 | if ( $successful === false ) { |
106 | $counter->setLabel( 'reason', $error ?: 'n/a' ); |
107 | } |
108 | $counter->copyToStatsdAt( $statsdKey ) |
109 | ->increment(); |
110 | |
111 | // pass to next handler |
112 | return false; |
113 | } |
114 | |
115 | /** |
116 | * @return string |
117 | */ |
118 | protected function getEntryPoint() { |
119 | return defined( 'MW_API' ) ? 'api' : 'web'; |
120 | } |
121 | |
122 | /** |
123 | * Get a field from an array without triggering errors if it does not exist |
124 | * @param string|array $field Field name or list of field name + fallbacks |
125 | * @param array $data |
126 | * @return mixed Field value, or null if field was missing |
127 | */ |
128 | protected function getField( $field, array $data ) { |
129 | foreach ( (array)$field as $key ) { |
130 | if ( isset( $data[$key] ) ) { |
131 | return $data[$key]; |
132 | } |
133 | } |
134 | return null; |
135 | } |
136 | } |