Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
29.55% covered (danger)
29.55%
13 / 44
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOMSH_Log
30.23% covered (danger)
30.23%
13 / 43
44.44% covered (danger)
44.44%
4 / 9
141.59
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 unsafe_direct_log
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 unsafe_direct_log_logstash
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add_hooks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 log
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 log_to_logstash
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 ensure_shutdown_hook
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 send_to_api
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * WPCOMSH Log file.
4 *
5 * @package wpcomsh
6 */
7
8/**
9 * Class WPCOMSH_Log
10 *
11 * This is an interface for logging arbitrary data to wpcom logstash cluster.
12 * This auto-initializes and provides a hook to log data:
13 * ```
14 * do_action( 'wpcomsh_log', "test" );
15 * ```
16 *
17 * You can see logs in Kibana, log2logstash index, under `feature:automated_transfer`
18 * by default. Records sent via `unsafe_direct_log_logstash()` land under the
19 * caller-supplied `feature:` bucket instead.
20 *
21 * Note that logging must be enabled for the site for the logs to be sent,
22 * which involves enabling the `at_options_logging_on` site option on the
23 * Jetpack site.
24 */
25class WPCOMSH_Log {
26    /**
27     * Logging Endpoint URL.
28     *
29     * @var string
30     */
31    protected static $log_endpoint = 'https://public-api.wordpress.com/rest/v1.1/automated-transfers/log';
32
33    /**
34     * Logstash endpoint URL — drained by `unsafe_direct_log_logstash()`,
35     * which lets each record land under its own Kibana `feature` bucket
36     * instead of the default `feature:automated_transfer` stream.
37     *
38     * @var string
39     */
40    protected static $logstash_endpoint = 'https://public-api.wordpress.com/rest/v1.1/logstash';
41
42    /**
43     * Class instance.
44     *
45     * @var WPCOMSH_Log
46     */
47    private static $instance;
48
49    /**
50     * Queue of log messages bound for /automated-transfers/log.
51     *
52     * @var array
53     */
54    private $log_queue = array();
55
56    /**
57     * Queue of log messages bound for /logstash. Each entry carries its own
58     * feature/severity so the receiver can bucket records distinctly.
59     *
60     * @var array
61     */
62    private $logstash_queue = array();
63
64    /**
65     * Whether it has a shutdown hook.
66     *
67     * @var bool
68     */
69    private $has_shutdown_hook = false;
70
71    /**
72     * Site URL.
73     *
74     * @var string
75     */
76    private $siteurl;
77
78    /**
79     * This instantiates the logging system. Because constructor is private, it can be only set up with `init` or `unsafe_direct_log`.
80     * `init` respects `at_options_logging_on` option. This essentially turns logging on/off so that we don't flood
81     * endpoint with too many requests.
82     * This is to be hooked into wp `init` hook.
83     */
84    public static function init() {
85        if ( ! get_option( 'at_options_logging_on' ) ) {
86            return;
87        }
88
89        if ( self::$instance ) {
90            return;
91        }
92
93        self::$instance = new self();
94        self::$instance->add_hooks();
95    }
96
97    /**
98     * This method bypasses `at_options_logging_on` check.
99     * It is intended to be used when we are sure we want to send logs to logstash and
100     * we are sure that we don't fire it off frequently. Good example of when we want to use this
101     * is during the site setup process
102     *
103     * @param string $message Log message.
104     * @param array  $extra   Optional. Additional log data. Defaults to empty array.
105     */
106    public static function unsafe_direct_log( $message, $extra = array() ) {
107        if ( ! self::$instance ) {
108            self::$instance = new self();
109        }
110        self::$instance->log( $message, $extra );
111    }
112
113    /**
114     * Direct counterpart to `unsafe_direct_log()` for records that should
115     * land on the public `/logstash` endpoint under their own
116     * `feature` bucket instead of the default `feature:automated_transfer`
117     * stream that `/automated-transfers/log` writes into.
118     *
119     * Use this when the record needs to be alertable / dashboarded
120     * independently from the automated-transfer telemetry firehose.
121     * Like `unsafe_direct_log()`, this bypasses the
122     * `at_options_logging_on` site option, so callers must ensure they
123     * don't fire it off frequently.
124     *
125     * @param string $feature Logstash `feature` bucket.
126     * @param string $message Log message.
127     * @param array  $options {
128     *     Optional. Per-record options.
129     *
130     *     @type array  $properties Structured key-value data, indexed under `properties.*` in Kibana for filtering / sorting / aggregation.
131     *     @type string $severity   Severity tag (e.g. 'critical', 'error', 'warning', 'info').
132     *     @type array  $extra      Unstructured context — preserved on the record but not intended for term-aggregation.
133     * }
134     */
135    public static function unsafe_direct_log_logstash( $feature, $message, $options = array() ) {
136        if ( ! self::$instance ) {
137            self::$instance = new self();
138        }
139        self::$instance->log_to_logstash( $feature, $message, $options );
140    }
141
142    /**
143     * Constructor.
144     */
145    private function __construct() {
146        $this->siteurl = get_site_url();
147    }
148
149    /**
150     * Adds the log action.
151     */
152    private function add_hooks() {
153        add_action( 'wpcomsh_log', array( $this, 'log' ), 1 );
154    }
155
156    /**
157     * Logs a log message.
158     *
159     * @param string $message Log message.
160     * @param array  $extra   Optional. Additional log data. Defaults to empty array.
161     */
162    public function log( $message, $extra = array() ) {
163        $this->log_queue[] = array(
164            'message' => $message,
165            'extra'   => $extra,
166        );
167        $this->ensure_shutdown_hook();
168    }
169
170    /**
171     * Queue a record for the `/logstash` endpoint. Drained on shutdown by
172     * `send_to_api()` alongside the `/automated-transfers/log` queue.
173     *
174     * @param string $feature Logstash `feature` bucket.
175     * @param string $message Log message.
176     * @param array  $options Optional per-record options: `properties`, `severity`, `extra`.
177     *                        See `unsafe_direct_log_logstash()` for details.
178     */
179    public function log_to_logstash( $feature, $message, $options = array() ) {
180        $entry = array(
181            'feature' => $feature,
182            'message' => $message,
183        );
184        if ( ! empty( $options['properties'] ) ) {
185            $entry['properties'] = (array) $options['properties'];
186        }
187        if ( ! empty( $options['severity'] ) ) {
188            $entry['severity'] = (string) $options['severity'];
189        }
190        if ( ! empty( $options['extra'] ) ) {
191            $entry['extra'] = (array) $options['extra'];
192        }
193        $this->logstash_queue[] = $entry;
194        $this->ensure_shutdown_hook();
195    }
196
197    /**
198     * Register the shared shutdown drain on first enqueue.
199     */
200    private function ensure_shutdown_hook() {
201        if ( $this->has_shutdown_hook ) {
202            return;
203        }
204        register_shutdown_function( array( $this, 'send_to_api' ) );
205        $this->has_shutdown_hook = true;
206    }
207
208    /**
209     * Sends log messages to the API endpoint.
210     */
211    public function send_to_api() {
212        if ( count( $this->log_queue ) > 0 ) {
213            $payload = array(
214                'siteurl'  => $this->siteurl,
215                'messages' => $this->log_queue,
216            );
217
218            wp_remote_post( self::$log_endpoint, array( 'body' => array( 'error' => wp_json_encode( $payload, JSON_UNESCAPED_SLASHES ) ) ) );
219        }
220
221        foreach ( $this->logstash_queue as $entry ) {
222            wp_remote_post( self::$logstash_endpoint, array( 'body' => array( 'params' => wp_json_encode( $entry, JSON_UNESCAPED_SLASHES ) ) ) );
223        }
224    }
225}
226add_action( 'init', array( 'WPCOMSH_Log', 'init' ) );