Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.83% covered (success)
95.83%
46 / 48
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Logger
95.83% covered (success)
95.83%
46 / 48
0.00% covered (danger)
0.00%
0 / 2
6
0.00% covered (danger)
0.00%
0 / 1
 instance
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 __construct
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
3
1<?php
2/**
3 * Instantiate the logger for your plugin.
4 *
5 * `$logger = \BrianHenryIE\WP_Logger\Logger::instance()`
6 * better:
7 * `$logger = \BrianHenryIE\WP_Logger\Logger::instance( $settings )`
8 *
9 * @see \BrianHenryIE\WP_Logger\Logger_Settings_Interface
10 * @see \BrianHenryIE\WP_Logger\Logger_Settings_Trait
11 * @see \BrianHenryIE\WP_Logger\WooCommerce_Logger_Settings_Interface
12 *
13 * @package brianhenryie/bh-wp-logger
14 */
15
16namespace BrianHenryIE\WP_Logger;
17
18use BrianHenryIE\WC_Logger\Log_Context_Handler;
19use BrianHenryIE\WC_Logger\WC_PSR_Logger;
20use BrianHenryIE\WP_Logger\API\BH_WP_PSR_Logger;
21use BrianHenryIE\WP_Logger\WP_Includes\Plugin_Logger_Actions;
22use BrianHenryIE\WP_Private_Uploads\BH_WP_Private_Uploads_Hooks;
23use BrianHenryIE\WP_Private_Uploads\Private_Uploads_Settings_Interface;
24use BrianHenryIE\WP_Private_Uploads\Private_Uploads_Settings_Trait;
25use BrianHenryIE\WP_Private_Uploads\Private_Uploads;
26use Monolog\Formatter\LineFormatter;
27use Monolog\Handler\PsrHandler;
28use Monolog\Handler\StreamHandler;
29use Monolog\Processor\PsrLogMessageProcessor;
30use Psr\Log\LoggerInterface;
31use Psr\Log\NullLogger;
32
33/**
34 * Wraps parent class in a singleton so it only needs to be configured once.
35 */
36class Logger extends BH_WP_PSR_Logger implements API_Interface, LoggerInterface {
37
38    /**
39     * Singleton.
40     *
41     * @var Logger
42     */
43    protected static Logger $instance;
44
45    /**
46     * Initialize the logger and store the instance in the singleton variable.
47     * Settings are used when provided, inferred when null.
48     * Ideally settings should be provided the first time the logger is instantiated, then they do not need
49     * to be provided when accessing the singleton later on.
50     *
51     * @see Logger_Settings
52     * @see Plugins
53     *
54     * @param ?Logger_Settings_Interface $settings The loglevel, plugin name, slug, and basename.
55     *
56     * @return LoggerInterface Ideally a {@see \BrianHenryIE\WP_Logger\Logger} but `NullLogger` sometimes.
57     */
58    public static function instance( ?Logger_Settings_Interface $settings = null ): LoggerInterface {
59
60        if ( ! isset( self::$instance ) ) {
61
62            // Zero-config.
63            $settings ??= new class() implements Logger_Settings_Interface {
64                use Logger_Settings_Trait;
65            };
66
67            // TODO: This is wrong, the directory must be assumed to contain files and be kept private.
68            if ( 'none' === $settings->get_log_level() ) {
69                return new NullLogger();
70            }
71
72            $logger = new self( $settings );
73
74            self::$instance = $logger;
75
76            // Add the hooks.
77            new Plugin_Logger_Actions( self::$instance, $settings, self::$instance );
78        }
79
80        return self::$instance;
81    }
82
83    /**
84     * If log level is 'none', use NullLogger.
85     * If Settings is WooCommerce_Logger_Settings_Interface use WC_Logger, otherwise use KLogger.
86     *
87     * @param Logger_Settings_Interface $settings Basic settings required for the logger.
88     */
89    public function __construct( Logger_Settings_Interface $settings ) {
90
91        /**
92         * We are not using {@see is_plugin_active()} here because "Call to undefined function" error (it may be an admin function).
93         *
94         * @param string $plugin_basename The main plugin file's path relative to WP_PLUGIN_DIR.
95         */
96        $is_plugin_active = function ( string $plugin_basename ): bool {
97            /** @var array<int,string> $active_plugins */
98            $active_plugins = apply_filters( 'active_plugins', get_option( 'active_plugins', array() ) );
99            return in_array( $plugin_basename, $active_plugins, true );
100        };
101
102        if ( $settings instanceof WooCommerce_Logger_Settings_Interface && $is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
103
104            $logger = new WC_PSR_Logger( $settings );
105
106            // Add context to WooCommerce logs.
107            $wc_log_handler = new Log_Context_Handler( $settings );
108            add_filter( 'woocommerce_format_log_entry', array( $wc_log_handler, 'add_context_to_logs' ), 10, 2 );
109
110            // TODO: What's the log file name when it's a wc-log?
111
112        } else {
113
114            $log_directory       = wp_normalize_path( WP_CONTENT_DIR . '/uploads/logs' );
115            $log_level_threshold = $settings->get_log_level();
116
117            /**
118             * Add the `{context}` template string,
119             * then provide `'appendContext' => false` to Lineformatter (since it is already takes care of).
120             *
121             * @see LineFormatter::SIMPLE_FORMAT
122             * "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";
123             */
124            $log_format = "%datetime% %level_name% %message%\n%context%\n";
125
126            // /path/to/.../wp-content/uploads/logs/bh-wp-logger-test-plugin-2024-04-20.log
127            $logfile = sprintf(
128                '%s/%s-%s.log',
129                $log_directory,
130                $settings->get_plugin_slug(),
131                gmdate( 'Y-m-d' )
132            );
133
134            /**
135             * `c` is chosen to match WooCommerce's choice.
136             *
137             * ISO8601: "2004-02-12T15:19:21+00:00".
138             *
139             * @see WC_Log_Handler::format_time()
140             */
141            $formatter = new LineFormatter(
142                format: $log_format,
143                dateFormat: 'c',
144                allowInlineLineBreaks: true,
145                ignoreEmptyContextAndExtra: true,
146                includeStacktraces: false
147            );
148
149            $logger  = new \Monolog\Logger( $settings->get_plugin_slug() );
150            $handler = new StreamHandler( $logfile, $log_level_threshold );
151            $handler->setFormatter( $formatter );
152            $logger->pushHandler( $handler );
153            $logger->pushProcessor( new PsrLogMessageProcessor() );
154
155            // Make the logs directory inaccessible to the public.
156            $private_uploads_settings = new class( $settings ) implements Private_Uploads_Settings_Interface {
157                use Private_Uploads_Settings_Trait;
158
159                /**
160                 * Constructor.
161                 *
162                 * @param Logger_Settings_Interface $logger_settings The plugin logger settings, whose plugin slug we need.
163                 */
164                public function __construct(
165                    /**
166                     * The settings provided for the logger. We need the plugin slug as a uid for the private uploads instance.
167                     */
168                    protected Logger_Settings_Interface $logger_settings
169                ) {
170                }
171
172                /**
173                 * This is used as a unique id for the Private Uploads instance.
174                 */
175                public function get_plugin_slug(): string {
176                    return $this->logger_settings->get_plugin_slug() . '_logger';
177                }
178
179                /**
180                 * Use wp-content/uploads/logs as the logs directory.
181                 */
182                public function get_uploads_subdirectory_name(): string {
183                    return 'logs';
184                }
185            };
186
187            // Mute debug logs from library: the handler drops records below INFO.
188            $private_uploads_logger = new \Monolog\Logger( $settings->get_plugin_slug() . '-private-uploads' );
189            $private_uploads_logger->pushHandler( new PsrHandler( $logger, \Monolog\Logger::INFO ) );
190
191            // Don't use the Private_Uploads singleton in case the parent plugin also needs it.
192            $private_uploads_api = new Private_Uploads( $private_uploads_settings, $private_uploads_logger );
193            new BH_WP_Private_Uploads_Hooks( $private_uploads_api, $private_uploads_settings, $private_uploads_logger );
194        }
195
196        parent::__construct( $settings, $logger );
197    }
198}