Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.00% covered (warning)
85.00%
17 / 20
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Telemetry
85.00% covered (warning)
85.00%
17 / 20
87.50% covered (warning)
87.50%
7 / 8
13.57
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getInstance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getRequestId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 overrideRequestId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 regenerateRequestId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTracestate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getTraceparent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getRequestHeaders
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20namespace MediaWiki\Http;
21
22use Wikimedia\Http\TelemetryHeadersInterface;
23
24/**
25 * Service for handling telemetry data
26 * @unstable
27 * @since 1.41
28 */
29class Telemetry implements TelemetryHeadersInterface {
30
31    /**
32     * @var Telemetry|null
33     */
34    private static ?Telemetry $instance = null;
35
36    /**
37     *
38     * @var string|null Request id
39     */
40    private ?string $reqId = null;
41
42    /**
43     * Server and execution environment information.
44     *
45     * @see https://www.php.net/manual/en/reserved.variables.server.php
46     * @var array
47     */
48    private array $server;
49
50    private ?bool $allowExternalReqID;
51
52    /**
53     * @param array $server Server and execution environment information, most likely the $_SERVER variable
54     */
55    public function __construct( array $server, bool $allowExternalReqID = null ) {
56        $this->server = $server;
57        $this->allowExternalReqID = $allowExternalReqID;
58    }
59
60    public static function getInstance(): Telemetry {
61        if ( !isset( self::$instance ) ) {
62            global $wgAllowExternalReqID;
63            self::$instance = new self( $_SERVER, $wgAllowExternalReqID );
64        }
65
66        return self::$instance;
67    }
68
69    /**
70     * Get the current request ID.
71     *
72     * This is usually based on the `X-Request-Id` header, or the `UNIQUE_ID`
73     * environment variable, falling back to (process cached) randomly-generated string.
74     *
75     * @return string
76     */
77    public function getRequestId(): string {
78        // This method is called from various error handlers and MUST be kept simple and stateless.
79        if ( !isset( $this->reqId ) ) {
80            if ( $this->allowExternalReqID ) {
81                $id = ( $this->server['HTTP_X_REQUEST_ID'] ?? $this->server['UNIQUE_ID'] ?? wfRandomString( 24 ) );
82            } else {
83                $id = ( $this->server['UNIQUE_ID'] ?? wfRandomString( 24 ) );
84            }
85
86            $this->reqId = $id;
87        }
88
89        return $this->reqId;
90    }
91
92    /**
93     * Override the unique request ID. This is for sub-requests, such as jobs,
94     * that wish to use the same id but are not part of the same execution context.
95     *
96     * @param string $newId
97     */
98    public function overrideRequestId( string $newId ): void {
99        $this->reqId = $newId;
100    }
101
102    /**
103     * Regenerate the request id by setting it to null, next call to `getRequestId`
104     * will refetch the request id from header/UNIQUE_ID or regenerate it.
105     * @return void
106     */
107    public function regenerateRequestId() {
108        $this->reqId = null;
109    }
110
111    /**
112     * Get the OpenTelemetry tracestate info
113     * Returns null when not present or AllowExternalReqID is set to false
114     *
115     * @return string|null
116     */
117    public function getTracestate(): ?string {
118        return $this->allowExternalReqID ? $this->server['HTTP_TRACESTATE'] ?? null : null;
119    }
120
121    /**
122     * Get the OpenTelemetry traceparent info,
123     * Returns null when not present or AllowExternalReqID is set to false
124     *
125     * @return string|null
126     */
127    public function getTraceparent(): ?string {
128        return $this->allowExternalReqID ? $this->server['HTTP_TRACEPARENT'] ?? null : null;
129    }
130
131    /**
132     * Return Telemetry data in form of request headers
133     * @return array
134     */
135    public function getRequestHeaders(): array {
136        return array_filter( [
137            'tracestate' => $this->getTracestate(),
138            'traceparent' => $this->getTraceparent(),
139            'X-Request-Id' => $this->getRequestId()
140        ] );
141    }
142}