Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
84.06% |
58 / 69 |
|
58.33% |
7 / 12 |
CRAP | |
0.00% |
0 / 1 |
EventLogging | |
85.29% |
58 / 68 |
|
58.33% |
7 / 12 |
24.68 | |
0.00% |
0 / 1 |
getLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getEventSubmitter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMetricsPlatformClient | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
resetMetricsPlatformClient | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
submit | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
sendBeacon | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
logEvent | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
3.00 | |||
serializeEvent | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
schemaValidate | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
sessionInSample | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
encapsulate | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
getLegacyStreamName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * PHP API for logging events. |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @ingroup EventLogging |
8 | * |
9 | * @author Ori Livneh <ori@wikimedia.org> |
10 | */ |
11 | |
12 | namespace MediaWiki\Extension\EventLogging; |
13 | |
14 | use FormatJson; |
15 | use MediaWiki\Deferred\DeferredUpdates; |
16 | use MediaWiki\Extension\EventLogging\EventSubmitter\EventSubmitter; |
17 | use MediaWiki\Extension\EventLogging\Libs\JsonSchemaValidation\JsonSchemaException; |
18 | use MediaWiki\Extension\EventLogging\Libs\JsonSchemaValidation\JsonTreeRef; |
19 | use MediaWiki\Extension\EventLogging\MetricsPlatform\MetricsClientFactory; |
20 | use MediaWiki\Logger\LoggerFactory; |
21 | use MediaWiki\MediaWikiServices; |
22 | use Psr\Log\LoggerInterface; |
23 | use RequestContext; |
24 | use RuntimeException; |
25 | use Wikimedia\MetricsPlatform\MetricsClient; |
26 | |
27 | class EventLogging { |
28 | |
29 | /** |
30 | * @var MetricsClient|null |
31 | */ |
32 | private static $metricsPlatformClient; |
33 | |
34 | /** |
35 | * Default logger. |
36 | * |
37 | * @internal |
38 | */ |
39 | public static function getLogger(): LoggerInterface { |
40 | return LoggerFactory::getInstance( 'EventLogging' ); |
41 | } |
42 | |
43 | private static function getEventSubmitter(): EventSubmitter { |
44 | return MediaWikiServices::getInstance()->get( 'EventLogging.EventSubmitter' ); |
45 | } |
46 | |
47 | /** |
48 | * Gets the singleton instance of the Metrics Platform Client (MPC). |
49 | * |
50 | * @see https://wikitech.wikimedia.org/wiki/Metrics_Platform |
51 | */ |
52 | public static function getMetricsPlatformClient(): MetricsClient { |
53 | if ( !self::$metricsPlatformClient ) { |
54 | /** @var MetricsClientFactory $metricsClientFactory */ |
55 | $metricsClientFactory = |
56 | MediaWikiServices::getInstance()->getService( 'EventLogging.MetricsClientFactory' ); |
57 | |
58 | self::$metricsPlatformClient = $metricsClientFactory->newMetricsClient( RequestContext::getMain() ); |
59 | } |
60 | |
61 | return self::$metricsPlatformClient; |
62 | } |
63 | |
64 | /** |
65 | * Resets the Metrics Platform Client for testing purposes. See also the warning and note |
66 | * against {@link MediaWikiServices::resetServiceForTesting()}. |
67 | * |
68 | * @internal |
69 | * |
70 | * @throws RuntimeException If called outside a PHPUnit test |
71 | */ |
72 | public static function resetMetricsPlatformClient(): void { |
73 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
74 | throw new RuntimeException( __METHOD__ . ' may only be called during unit tests.' ); |
75 | } |
76 | |
77 | self::$metricsPlatformClient = null; |
78 | } |
79 | |
80 | /** |
81 | * Submit an event according to the given stream's configuration. |
82 | * |
83 | * @param string $streamName |
84 | * @param array $event |
85 | * @param LoggerInterface|null $logger @deprecated since 1.40. All messages will be logged |
86 | * via the `EventLogging.Logger` service |
87 | */ |
88 | public static function submit( |
89 | string $streamName, |
90 | array $event, |
91 | ?LoggerInterface $logger = null |
92 | ): void { |
93 | if ( $logger ) { |
94 | wfDeprecatedMsg( __METHOD__ . ': $logger parameter is deprecated', '1.40' ); |
95 | } |
96 | |
97 | self::getEventSubmitter()->submit( $streamName, $event ); |
98 | } |
99 | |
100 | /** |
101 | * Transfer small data asynchronously using an HTTP POST. |
102 | * This is meant to match the Navigator.sendBeacon() API. |
103 | * |
104 | * @see https://w3c.github.io/beacon/#sec-sendBeacon-method |
105 | * @param string $url |
106 | * @param array $data |
107 | * @return bool |
108 | * @deprecated use submit with new Event Platform based schemas. |
109 | */ |
110 | public static function sendBeacon( $url, array $data = [] ) { |
111 | $fname = __METHOD__; |
112 | $url = wfExpandUrl( $url, PROTO_INTERNAL ); |
113 | DeferredUpdates::addCallableUpdate( static function () use ( $url, $data, $fname ) { |
114 | $options = $data ? [ 'postData' => $data ] : []; |
115 | return MediaWikiServices::getInstance()->getHttpRequestFactory() |
116 | ->post( $url, $options, $fname ); |
117 | } ); |
118 | |
119 | return true; |
120 | } |
121 | |
122 | /** |
123 | * Legacy EventLogging entrypoint. |
124 | * |
125 | * NOTE: For forwards compatibility with Event Platform schemas, |
126 | * we hijack the wgEventLoggingSchemas revision to encode the |
127 | * $schema URI. If the value for a schema defined in |
128 | * EventLoggingSchemas is a string, it is assumed |
129 | * to be an Event Platform $schema URI, not a MW revision id. |
130 | * In this case, the event will be POSTed to EventGate. |
131 | * |
132 | * @param string $schemaName Schema name. |
133 | * @param int $revId |
134 | * revision ID of schema. $schemasInfo[$schemaName] will override this. |
135 | * @param array $eventData |
136 | * Map of event keys/vals. |
137 | * This is the 'event' field, as provided by the caller, |
138 | * not an encapsulated real event. |
139 | * @param int $options This parameter is deprecated and no longer used. |
140 | * @return bool Whether the event was logged. |
141 | * @deprecated use EventLogging::submit() with new Event Platform based schemas. |
142 | * @see https://wikitech.wikimedia.org/wiki/Event_Platform/Instrumentation_How_To#In_PHP |
143 | */ |
144 | public static function logEvent( $schemaName, $revId, $eventData, $options = 0 ) { |
145 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
146 | |
147 | $eventLoggingConfig = Hooks::getEventLoggingConfig( $config ); |
148 | $schemasInfo = $eventLoggingConfig['schemasInfo']; |
149 | $eventLoggingBaseUri = $eventLoggingConfig['baseUrl']; |
150 | |
151 | // Get the configured revision id or $schema URI |
152 | // to use with events of a particular (legacy metawiki) EventLogging schema. |
153 | // $schemasInfo[$schemaName] overrides passed in $revId. |
154 | $revisionOrSchemaUri = $schemasInfo[$schemaName] ?? $revId ?? -1; |
155 | |
156 | // Encapsulate and other event meta data to eventData. |
157 | $event = self::encapsulate( |
158 | $schemaName, |
159 | $revisionOrSchemaUri, |
160 | $eventData |
161 | ); |
162 | |
163 | if ( isset( $event['$schema'] ) ) { |
164 | // Assume that if $schema was set by self::encapsulate(), this |
165 | // event should be POSTed to EventGate via EventServiceClient submit() |
166 | self::submit( self::getLegacyStreamName( $schemaName ), $event ); |
167 | return true; |
168 | } else { |
169 | // Else this will be sent to the legacy eventlogging backend |
170 | // via 'sendBeacon' by url encoding the json data into a query parameter. |
171 | if ( !$eventLoggingBaseUri ) { |
172 | return false; |
173 | } |
174 | |
175 | $json = self::serializeEvent( $event ); |
176 | $url = $eventLoggingBaseUri . '?' . rawurlencode( $json ) . ';'; |
177 | |
178 | return self::sendBeacon( $url ); |
179 | } |
180 | } |
181 | |
182 | /** |
183 | * |
184 | * Converts the encapsulated event from an object to a string. |
185 | * |
186 | * @param array $event Encapsulated event |
187 | * @return string $json |
188 | */ |
189 | public static function serializeEvent( $event ) { |
190 | $eventData = $event['event']; |
191 | |
192 | if ( count( $eventData ) === 0 ) { |
193 | // Ensure empty events are serialized as '{}' and not '[]'. |
194 | $eventData = (object)$eventData; |
195 | } |
196 | $event['event'] = $eventData; |
197 | |
198 | // To make the resultant JSON easily extracted from a row of |
199 | // space-separated values, we replace literal spaces with unicode |
200 | // escapes. This is permitted by the JSON specs. |
201 | return str_replace( ' ', '\u0020', FormatJson::encode( $event ) ); |
202 | } |
203 | |
204 | /** |
205 | * Validates object against JSON Schema. |
206 | * |
207 | * @throws JsonSchemaException If the object fails to validate. |
208 | * @param array $object Object to be validated. |
209 | * @param array|null $schema Schema to validate against (default: JSON Schema). |
210 | * @return bool True. |
211 | */ |
212 | public static function schemaValidate( $object, $schema = null ) { |
213 | if ( $schema === null ) { |
214 | // Default to JSON Schema |
215 | $json = file_get_contents( dirname( __DIR__ ) . '/schemas/schemaschema.json' ); |
216 | $schema = FormatJson::decode( $json, true ); |
217 | } |
218 | |
219 | // We depart from the JSON Schema specification in disallowing by default |
220 | // additional event fields not mentioned in the schema. |
221 | // See <https://bugzilla.wikimedia.org/show_bug.cgi?id=44454> and |
222 | // <https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.4>. |
223 | if ( !array_key_exists( 'additionalProperties', $schema ) ) { |
224 | $schema[ 'additionalProperties' ] = false; |
225 | } |
226 | |
227 | $root = new JsonTreeRef( $object ); |
228 | $root->attachSchema( $schema ); |
229 | return $root->validate(); |
230 | } |
231 | |
232 | /** |
233 | * Randomise inclusion based on population size and a session ID. |
234 | * @param int $populationSize Return true one in this many times. This is 1/samplingRate. |
235 | * @param string $sessionId Hexadecimal value, only the first 8 characters are used |
236 | * @return bool True if the event should be included (sampled in), false if not (sampled out) |
237 | */ |
238 | public static function sessionInSample( $populationSize, $sessionId ) { |
239 | $decimal = (int)base_convert( substr( $sessionId, 0, 8 ), 16, 10 ); |
240 | return $decimal % $populationSize === 0; |
241 | } |
242 | |
243 | /** |
244 | * This encapsulates the event data in a wrapper object with |
245 | * the default metadata for the current request. |
246 | * |
247 | * NOTE: for forwards compatibility with Event Platform schemas, |
248 | * we hijack the wgEventLoggingSchemas revision to encode the |
249 | * $schema URI. If the value for a schema defined in |
250 | * EventLoggingSchemas is a string, it is assumed |
251 | * to be an Event Platform $schema URI, not a MW revision id. |
252 | * In this case, the event will be prepared to be POSTed to EventGate. |
253 | * |
254 | * @param string $schemaName |
255 | * @param int|string $revisionOrSchemaUri |
256 | * The revision id or a string $schema URI for use with Event Platform. |
257 | * @param array $eventData un-encapsulated event data |
258 | * @return array encapsulated event |
259 | */ |
260 | private static function encapsulate( $schemaName, $revisionOrSchemaUri, $eventData ) { |
261 | global $wgDBname; |
262 | |
263 | $event = [ |
264 | 'event' => $eventData, |
265 | 'schema' => $schemaName, |
266 | 'wiki' => $wgDBname, |
267 | ]; |
268 | |
269 | if ( isset( $_SERVER[ 'HTTP_HOST' ] ) ) { |
270 | $event['webHost'] = $_SERVER['HTTP_HOST']; |
271 | } |
272 | |
273 | if ( is_string( $revisionOrSchemaUri ) ) { |
274 | $event['$schema'] = $revisionOrSchemaUri; |
275 | // NOTE: `client_dt` is 'legacy' event time. `dt` is the preferred event time field |
276 | // and is set in EventServiceClient. |
277 | $event['client_dt'] = wfTimestamp( TS_ISO_8601 ); |
278 | |
279 | // Note: some fields will have defaults set by eventgate-wikimedia. |
280 | // See: |
281 | // - https://gerrit.wikimedia.org/r/plugins/gitiles/eventgate-wikimedia/+/refs/heads/master/eventgate-wikimedia.js#358 |
282 | // - https://wikitech.wikimedia.org/wiki/Event_Platform/Schemas/Guidelines#Automatically_populated_fields |
283 | } else { |
284 | $event['revision'] = $revisionOrSchemaUri; |
285 | $event['userAgent'] = $_SERVER[ 'HTTP_USER_AGENT' ] ?? ''; |
286 | } |
287 | |
288 | return $event; |
289 | } |
290 | |
291 | /** |
292 | * Prepend "eventlogging_" to the schema name to create a stream name for a migrated legacy |
293 | * schema. |
294 | * |
295 | * @param string $schemaName |
296 | * @return string |
297 | */ |
298 | private static function getLegacyStreamName( string $schemaName ): string { |
299 | return "eventlogging_$schemaName"; |
300 | } |
301 | |
302 | } |
303 | |
304 | class_alias( EventLogging::class, 'EventLogging' ); |