Giter Site home page Giter Site logo

wonolog's Introduction

Wonolog

Version Status Build Downloads License

Wonolog

Monolog-based logging package for WordPress.


Table of Contents


Introduction

Wonolog is a Composer package (not a plugin) that allows to log anything that happens in a WordPress site.

It is based on Monolog, which, with its over 38 millions of downloads and thousands of dependent packages, is the most popular logging library for PHP, compatible with the PSR-3 standard.

Minimum Requirements and Dependencies

Wonolog requires:

  • PHP 5.6+
  • WordPress 4.6+

Via Composer, Wonolog requires monolog/monolog (MIT).

When installed for development, via Composer, Wonolog also requires:

  • phpunit/phpunit (BSD-3-Clause)
  • brain/monkey (MIT)
  • mikey179/vfsStream (BSD-3-Clause)

Getting Started

Wonolog should be installed via Composer. Its package name is inpsyde/wonolog.

The suggested way to use Wonolog is at website level.

If you don't use Composer to manage your whole website then Wonolog is probably not for you. You might be able to use it anyway, but support is not guaranteed.

It's easily possible to develop plugins and themes compatible with Wonolog logging even without explicitly declaring it as a dependency.

A couple of noteworthy things:

  • all Wonolog configurations have to be done in a MU plugin;
  • in a WordPress multisite installation, all Wonolog configurations are naturally site-wide.

On the bright side, Wonolog comes with a super easy bootstrap routine and some out-of-the-box configurations that make it possible to have a working and effective logging system with zero effort.

To get started with defaults settings, this is required:

  1. install Wonolog via Composer;
  2. ensure Composer autoload is loaded in wp-config.php or anytime before the 'muplugins_loaded' action is fired;
  3. create a MU plugin that, at least, contains this code:
<?php
Inpsyde\Wonolog\bootstrap();

Wonolog Defaults

The three steps described above are all that is necessary to have a working logging system that uses Monolog to write logs in a file. The path of that file changes based on current date, using the following format:

  • {WP_CONTENT_DIR}/wonolog/{Y/m/d}.log,

with {Y/m/d} being replaced by date( 'Y/m/d' ).

For example, a target file could be /wp-content/2017/02/27.log.

What is actually logged depends on the value of WP_DEBUG_LOG constant.

When WP_DEBUG_LOG is set to true, Wonolog will log everything. When WP_DEBUG_LOG is set to false, Wonolog will only log events with a log level higher or equal to ERROR, according to PSR-3 log levels.

"Automatically" logged events include:

  • PHP core notices, warnings and (fatal) errors;
  • uncaught exceptions;
  • WordPress errors and events (e.g., DB errors, HTTP API errors, wp_mail() errors, and 404 errors).

This is just the default behavior.

The bootstrap() function provides entry points for many configurations and customizations.

Moreover, the package provides both action and filter hooks, and can be configured via environment variables, which makes Wonolog very flexible, and exposes all the power that Monolog provides.

Learn More

Documentation of Wonolog features, defaults, configuration and ways to extends it can be found in separate files:

License and Copyright

This repository is a free software, and is released under the terms of the MIT license. See LICENSE for complete license.

wonolog's People

Contributors

alexwoollam avatar chesio avatar chrico avatar dnaber-de avatar gmazzap avatar nielsdeblaauw avatar noplanman avatar redelschaap avatar tfrommen avatar websupporter avatar widoz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wonolog's Issues

Raise level of occuring cron events to INFO

Right now, the CronDebugListener records every cron worker event with the DEBUG severity:

[2018-04-30 14:32:21] DEBUG.DEBUG: Cron action "wp_scheduled_auto_draft_delete" performed.

Monitoring the proper execution of cron events is a typical scope of logging especially on production systems. Therefore I would suggest to raise the severity level to INFO as it is a normal operation without significant condition. Normally the threshold for log records for production environments is set to something above DEBUG to reduce the noise but right now this removes also the cron event log completely.

For sake of consistency the listener class should be renamed too, but this is way more invasive as it is considered public API, I guess?

Upgrade to Monolog 2*

Have you scheduled to upgrade Wonolog to be compatible with latest Monolog version (2.0 at least) ?
Thanks =)

Example custom listener coupled to global WP function

The Problem

The custom listener example includes a MyFilesListener class. This class is coupled to the global WordPress function current_filter(). This is not good for several obvious reasons. Alas, it does not seem possible to avoid this, because ActionListenerInterface#update() does not provide any context which could be used to determine which hook is being handled.

Suggested Solution

HookListenersRegistry#listen_hook(), the method which appears to invoke the hook callback, looks like it receives the hook name as the first $hook parameter. If this value could be passed to HookListenersRegistry#hook_callback(), and then subsequently passed to the #update() method, this would give listeners enough context to distinguish between hooks without relying on global context.

This would not break BC, because it would add a parameter to a method. Listener implementations that currently do not support this will simply get an extra argument, which will not be handled.

Possible to change default listener priority? Re: FailedLoginListener

Describe the bug
With the addition of Application Passwords in core (for some time now), I have noticed an increase in Security Notices for the FailedLoginListener action. Since wp_authenticate returns a null user on a REST request using Basic Auth via Application Password before priority 20 (in my quick tests), is there a way to change the action listener to a later priority?

To Reproduce

  1. Add user with Application Password
  2. Make REST request with Basic Auth.
  3. Repeat request three times and note SECURITY.NOTICE from Wonolog, but valid responses in REST call with no mention of authentication_failed WP_Error.

Expected behavior
No NOTICE logged.

System (please complete the following information):

  • OS: WP
  • Version 5.8.1
  • Wonolog 1.0.4

[2.x] Q&A improvments

We need to do following for version 2.x of Wonolog:

  • Add Github Actions
  • Add PSALM
  • Add Inpsyde Coding Standards 1.x

All tests should pass, which also contains renaming methods and adding return types.

Minimum PHP version will be 7.2 because Monolog requires this as version.

Monolog 2.0.0 breaks compatability with Wonolog custom handlers.

Monolog has just released version 2.0. As Wonolog pulles the latest stable version on monolog. the breaking changes in v2 look to break custom handlers used in Wonolog.

Version Information

  • PHP: 7.2.*
  • WordPress: 5.2.2

Steps to Reproduce

  1. Standard WordPress setup
  2. Wonolog installed as per readme
  3. Add custom handler:
use Inpsyde\Wonolog;
use Monolog\Handler\NativeMailerHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;

/**
 * Add email handler.
 */
$email_handler = new NativeMailerHandler(
	'[email protected]',        // to
	'Security alert from example.com', // subject
	'[email protected]',    // from
	Logger::ERROR                      // minimum level
);

Inpsyde\Wonolog\bootstrap()
  ->use_handler( $email_handler );

What I Expected

Website to load as standard.

What Happened Instead

The site is experiencing technical difficulties.

Remove the custom handler and the site works ask expected. Just without the functionality of a custom handler.

Great package needs great static analysis :)

Hello inpsyde people!

Please consider running @phpstan
It needs modern PHP to run, e.g. 7.3

phpstan.neon.dist

#$ composer require --dev phpstan/phpstan-shim szepeviktor/phpstan-wordpress
#$ vendor/bin/phpstan analyze

includes:
    - phar://phpstan.phar/conf/bleedingEdge.neon
    - vendor/szepeviktor/phpstan-wordpress/extension.neon
parameters:
    level: max
    inferPrivatePropertyTypeFromConstructor: true
    paths:
        - %currentWorkingDirectory%/src/
    ignoreErrors:
        # Uses func_get_args()
        - '#^Function apply_filters(_ref_array)? invoked with [34567] parameters, 2 required\.$#'
        - '#^Function do_action(_ref_array)? invoked with [3456] parameters, 1-2 required\.$#'
        - '#^Function add_query_arg invoked with [123] parameters?, 0 required\.$#'

CronDebugListener doesn't log on WP CLI

WP CLI allows to spawn Crons like wp cron event run --due-now. The current implementation relies on the transient doing_cron is being set but this only happens on WP's cron web hook wp-cron.php.

I would suggest to refactor CronDebugListener to be an ActionListenerInterface and listen to wp_loaded. It checks then if it runs in a cron context and applies it's filters.

PR is under progress.

[Feature Request]: Support for "wp_php_error_message"-hook

Is your feature request related to a problem?

Right nowWonolog is listening to several PHP build in functions to catch "fatal errors". But WordPress has since version 5.2.0 the
WP_Fatal_Error_Handler [1] which listens to register_shutdown_function() [2] as well.

The standard callback will just print via wp_die() [3] a generic template like There has been a critical error on this website. and no error message will be logged to our logger anymore.

[1] https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-fatal-error-handler.php#L29
[2] https://github.com/WordPress/WordPress/blob/master/wp-includes/error-protection.php#L95
[3] https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-fatal-error-handler.php#L239

Describe the desired solution

The WP_Fatal_Error_Handler provides a new hook called wp_php_error_message which has a $message and the $error which we can use to log PHP errors.

See: https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-fatal-error-handler.php#LL218C13-L218C13

Describe the alternatives that you have considered

Additional context

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

Log response body on failed HTTP requests (HttpApiListener)

The HttpApiListener::log_http_error() method currently does not provide the response body as context data. Especially when working with REST services like Elasticsearch the body can contain helpful information about the reason of the error.

So I would suggest to include $response[ 'body' ] in the context data if exists and the response code is not 404 or the response content type is text/html in order to not overload the log files.

Do not log silenced PHP errors

When the PHP code purposefully suppresses PHP errors using the error control operator @, those errors should not be logged by default.

A filter could be added to restore the old behavior in which those errors have been logged.

DateBasedStreamHandler cannot be used when folder structure contains a space

Version Information

  • PHP: 7.2.5
  • WordPress: 4.9.8

Steps to Reproduce

  1. Have a WordPress installation where the folder structure contains a space (e.g. C:\WP projects\example.com)
  2. Set up Wonolog
  3. Cause an error

What I Expected

Error to be logged to wp-content/wonolog/2018-11-12.log.

What Happened Instead

Error did not get logged in wp-content/wonolog/2018-11-12.log, only in wp-content/debug.log

Debug trace

I traced this behaviour back to \Inpsyde\Wonolog\Handler\DateBasedStreamHandler::check_file_format, which is returning false because filter_var( $file_format, FILTER_SANITIZE_URL ) === $file_format is not passing.

I think this check is used to validate the filename, but I think there are other ways to do this (like with regex).

How to Log to php://stderr or php://stdout

Is your feature request related to a problem? Please describe.
When using docker and Kubernetes the recommended way to log is to send the logs to stdout or stderr.

Describe the solution you'd like
Maybe just a question: is it possible to set the logs path to php://stderr or php://stdout ?
If yes, how to do it? maybe add documentation.
If not, can you add the feature or provide some workaround?

Describe alternatives you've considered
Wonolog exposes the bootstrap function that receives a handler, we could pass a StreamHandler pointing to php://stdout

If it is not possible and the feature doesn't / can't be developed, can I set a nondate dependent path for the logs so maybe I can create a symlink to stdout or stderr?

Additional context
none

CronDebugListener: array_merge(): Expected parameter 1 to be an array, null given

Version Information

  • PHP: 7.2
  • WordPress: 5.4.2

Description

I am not sure im some actor is misbehaving in the WP setup I am seeing the problem with, but the first entry of _get_cron_array() is 0 => false when CronDebugListener grabs it

This causes errors that of course spam the logs when this value is passed into the array_reduce callback a little later.

My suggestion would be to run array_filter( $cron_array ) before passing it on

Integrating with newrelic/monolog-enricher

Hi all -

I am completely new to Monolog/Wonolog and I am trying to set it up to send all PHP/WP errors to New Relic using the New Relic Log Enricher composer package.

I have Wonolog up and running on a pretty standard WP install running inside Docker. Wonolog is writing logs to the default path and the log data looks good. However, I am unsure how to incorporate the New Relic enricher to add my app's New Relic info to the logs. I saw a few mentions of New Relic in the docs but that seemed to be more around a handler than a formatter.

Any guidance would be greatly appreciated. My simple setup is below.

wp-config.php

require 'vendor/autoload.php';
use NewRelic\Monolog\Enricher\{Formatter, Processor};
...
WP_DEBUG = true
WP_DEBUG_LOG = true
...

mu-plugins/wonolog-config.php

if ( defined( 'Inpsyde\Wonolog\LOG' ) ) {
    Inpsyde\Wonolog\bootstrap();
}

Version Information

  • PHP: 7.3.28
  • WordPress: 5.6.1
  • inpsyde/wonolog: 1.0.4
  • monolog/monolog: 1.26.0
  • newrelic/monolog-enricher: 1.0.1

Coding standards: rename wonolog.log to wonolog_log

FILE: [...]/wp-content/plugins/[...]/[...].php
--------------------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
--------------------------------------------------------------------------------
 34 | WARNING | Words in hook names should be separated using underscores.
    |         | Expected:  'wonolog_log', but found:  'wonolog.log'.
--------------------------------------------------------------------------------

When running PHPCS, using the WordPress-Extra standards, I receive the error message.

Would like to have an open discussion about this but can we setup wonolog_log as the main hook and wonolog.log as an alias for backwards compatibility?

Review (Thorsten)

Howdy Reader,

this is my take on a full code review; enjoy. :)

PHPDoc

As this is a package that we want to open source sooner or later, I suggest to implement our own PHPDoc best practices.

For Wonolog, this would mean to add missing @since annotations, document constants (const as well as define) with a short description and @since and @var annotations, provide a short description for require etc., and document parameters and return type of all functions.

Let's also get rid of @inheritdoc; yes, please?! :) We all use an IDE that serves us well here.

Name References

Referencing the package is not consistent. The file-level C-style comment should use just "Wonolog" like everywhere else, and not "Inpsyde wonolog".

Also, when using namespaces, we usually also use these for the @package annotation, which currently is just wonolog no matter for what file/domain.

Code

Inpsyde\Wonolog\Data\Error

This should be a final class, right?

Inpsyde\Wonolog\Data\FailedLogin

The constructor should cast $username to a string.

The get_level() method is too verbose (and contains error-prone redundand data). I would suggest it like so:

public function level() {

	$attempts = $this->count_attempts( 300 );

	switch ( TRUE ) {
		case ( $attempts > 2 && $attempts <= 100 ):
			return Logger::NOTICE;

		case ( $attempts <= 590 ) :
			return Logger::WARNING;

		case ( $attempts <= 990 ) :
			return Logger::ERROR;

		case ( $attempts > 990 ) :
			return Logger::CRITICAL;
	}

	return 0;

}

The order is now important, of course. This could go into a comment.

Also, the 0 return value should be a constant as well.

If sniff_ip() didn't only return a values array, but provide ip and from keys, it could directly be used in context():

public function context() {

	return $this->sniff_ip() + [ 'username' => $this->username ];
}

Is PHP_SAPI always set, no matter what? Otherwise, checking defined( 'PHP_SAPI' ) cannot hurt, right?

Inpsyde\Wonolog\Data\Log

The $message and $context variables in the named constructor from_wp_error() can be inlined as the according methods will be called only once anyway.

For what particular strange use case is the named constructor from_array() designed?! 🤔 Let's please just require an associative array, and be done with six lines of code here!? No kidding. :)

Also, the constructor should cast $message and $channel to strings, and $level to int.

Inpsyde\Wonolog\Data\LogDataTrait

The constructor should cast $message and $channel to strings.

Inpsyde\Wonolog\Data\NullLog

The return value, -1, of level() could be turned into a class constant (of the NullLog itself, or a better fitting class) in order to allow it to be referenced and managed consistently throughout all things.

Inpsyde\Wonolog\Data\VariableLevelLog

To be honest, I don't understand how this class should be used. I don't see any reason for making the with_level_callback() not a regular named constructor, but a PSR-7-like modifier. I'd rather have something like this:

public static function instance_with_level_callback(
	callable $level_callback,
	$message,
	$channel,
	array $context = []
) {

	$instance                 = new static( $message, $channel, $context );
	$instance->level_callback = $level_callback;

	return $instance;
}

Probably no big deal, but iff the level() method gets called more often, there's unnecessary computation overhead. This could be circumvented by having a default callback that just returns Logger::DEBUG (oh, BTW, why hard-coded DEBUG, and not provide ways to adapt this?). A possible implementation is this:

public function __construct(
	$message,
	$channel,
	array $context = [],
	callable $level_callback
) {

	parent::__construct( $message, $channel, $context );

	$this->level_callback = $level_callback ?: [ $this, 'default_level' ];
}

public static function instance_with_level_callback(
	callable $level_callback,
	$message,
	$channel,
	array $context = []
) {

	return new static( $message, $channel, $context, $level_callback );
}

public function level() {

	return $this->level_callback();
}

private function default_level() {

	// TODO: Make customizable (via constructors) or use `Logger::DEBUG` here...?
	return $this->default_level;
}

Inpsyde\Wonolog\Data\WpErrorChannel

The result of apply_filters() inside for_error() should be cast to string.

Also, why only allow to use the filter if there was a channel provided for the named constructor? That would also make very much sense in general, IMO. The channel() method would allow for this, too, but that's maybe too late, meaning too much unnecessary things got computed already.

Use Yoda conditions for stripos() in maybe_http_channel() and maybe_security_channel().


Inpsyde\Wonolog\HookListeners\*

What's the reason to have the update() method on the HookListenerInterface instead of the ActionListenerInterface? All (two) implementations of the former (i.e., CronDebugListener and WpDieHandlerListener) only return a NullLog() anyway. I'd rather have an empty HookListenerInterface, and move the update() method to the ActionListenerInterface. If someone for whatever reason needs to use both, they can easily implement both interfaces. Done.

Inpsyde\Wonolog\HookListeners\CronDebugListener

It would be nice to be able to filter the priority for the add_action() calls in filter().

Also, '-' . PHP_INT_MAX both looks strange and actually is not the lowest possible value (this would be (int) ( PHP_INT_MAX + 1 )). :)

Inpsyde\Wonolog\HookListeners\DbErrorListener

The problem with using (only!) the shutdown action hook is that you can only log regularly ended requests. Potential DB errors on redirect requests, or AJAX calls etc. that get explicitly terminated won't get logged at all, right? To capture wp_die() calls, one could, for example, also use the wp_die_ajax_handler, wp_die_xmlrpc_handler and wp_die_handler filters. Of course, this wouldn't take care of exit(); and die(); calls, though.

Inpsyde\Wonolog\HookListeners\HttpApiListener

In isError() (why is this camelCase, BTW?), one could simplify the second part in the condition to just this:

isset( $response[ 'response' ][ 'code' ] )
&& is_numeric( $response[ 'response' ][ 'code' ] )
&& 200 !== (int) $response[ 'response' ][ 'code' ]

Use Yoda condition in isCron().

In logHttpError(), why is it okay to directly access $response[ 'response' ][ 'message' ] and $response[ 'response' ][ 'code' ] without checking first? From the code, I can't see the array always having these elements/keys. Okay, for $response[ 'response' ][ 'code' ] it is checked in isError(). But $response[ 'response' ][ 'message' ] just might not be there.

Also, the formatting of logCron() and logHttpError() is different. When renaming the parameters accordingly, one could easily use compact() here to create the log context.

Inpsyde\Wonolog\HookListeners\MailerListener

Like mentioned yesterday, the on_mal_failed() method should return a NullLog() after the if statement.

Also, why is the class not final?

Inpsyde\Wonolog\HookListeners\WpDieHandlerListener

Why isn't this class hooked to wp_die_xmlrpc_handler, too?

The closure in isDbError() should be a regular private method, in my opinion.


Inpsyde\Wonolog\Channels

The string in logger() is missing something, or I don't get it:

%s is not a registered channel. Use "" to register custom channels

Inpsyde\Wonolog\FrontController

Like mentioned yesterday, it would be nice to allow for customizing the priority for the add_action() calls in setup(). Also, using 9999 for the number of arguments looks quite odd. If Core would only allow to pass null. :/ Until then, PHP_INT_MAX better carries the message "all of 'em", in my opinion. Even though it's very unlikely to pass more than, say, ten arguments to a function. :D

I would move all the hook listener registering stuff to the setup_hook_listener() method. So one doesn't have to create and pass around a local registry. Let's do this directly in setup_hook_listener(), and also fire the action and flush the registry.

The listen_hook() method would have to be adapted when moving update() from HookListenerInterface to ActionListenerInterface, like mentioned before.

Inpsyde\Wonolog\HookListenersRegistry

I'm not sure that identifying a listener instance by means of its class name is the right thing to do. What if someone already registered some listener, and now someone else (maybe you? :) ) would like to register a listener instance of the same class, but with a different configuration...? This is currently not possible, because you can register only a single instance of a specific listener class.

Also, there's currently no way to learn of listeners that did not get registered (because something was wrong with them). Should we integrate something that takes care of that? For example by means of a simple skipped_listeners() method that accesses a new class property, which gets filled in the listeners() method...?

What's the reason for this:

unset( $this->factories, $this->listeners );
$this->factories = [];
$this->listeners = [];

Is there some garbage collection magic happening, compared to when just setting the properties to an empty array?

Inpsyde\Wonolog\LogActionSubscriber

The namespace import of Inpsyde\Wonolog\Data\Debug is not needed (anymore?).

In update(), ! ( $log->level() > 0 ) should be just 1 > $log->level().

Inpsyde\Wonolog\PhpErrorController

The on_exception() method is missing a type hint. Also, the formatting is not consistent with on_error(). Inlining variables that only occur once is fine for me.

Action and Filter Hooks

In my opinion, any action or filter hook that is (to be) used in more than one unit should not be hard-coded throughout the codebase, but instead be defined as class constant. Good examples are the wonolog.loaded action hook that is currently being used in FrontController, HookListenersRegistry and LogActionSubscriber, and the wonolog.log action hook that is being used in six classes. These should be defined as FrontController::ACTION_LOADED and FrontController::ACTION_LOG.

Code Style

In the Inpsyde\Wonolog\HookListeners\HttpApiListener and Inpsyde\Wonolog\HookListeners\WpDieHandlerListener classes, there are function names in lowerCamelCase, which is odd, in my opinion.

Cheers,
Thorsten

PHP Fatal error: Interface 'Psr\Log\LoggerInterface'

Hello,

I had installed and tested Wonolog on a local vagrant machine running Ubuntu 14.04 LTS. I used composer to install. Works great.

After commiting to repo and deploying the code to a test platform, I get:

PHP Fatal error: Interface 'Psr\Log\LoggerInterface' not found in /var/wordpress/htdocs/vendor/monolog/monolog/src/Monolog/Logger.php on line 27

Note that Composer was not on that machine; I installed it, ran composer install

Any suggestions?
Thanks in advance

Missing Update Return Type Documentation

The Problem

ActionListenerInterface#update() documents that the only possible return type is LogDataInterface. However, it is not exactly true: if any other type is returned, there are no negative consequences; instead, no message is logged silently.

Alas, from the interface documentation it is not clear what the handler needs to do in order to avoid logging anything.

Suggested Solution

Add this to the documentation of ActionListenerInterface#update(), so that it becomes e.g. this:

@return LogDataInterface|mixed The data to write, if applicable; anything else otherwise.

Optionally, tighten the alternative return value to a single type, e.g. null.

Alternatively, make LogDataInterface the only valid return value, and amend the check to throw if the data is invalid (or declare the return type in PHP with : LogDataInterface.

Review (David)

Wonolog Review

In general, the code is very clean and good to read, if one have a clear understanding of the domain language and what is the purpose of Logger, Channel, Listener, Subscriber, Registry and Factory. The internals of Wonolog (Listener, Subscriber and Registry) might be addressed in the Readme. (I'm not sure here as the readme is already pretty long.)

The concept and the API is very clean and will probably save lots of hours in the future.

Documentation

  • Inline document of any hook would be nice
    /**
     * @param Foo $foo
     * @param Bar $bar
     * 
     * @return Foo
     */
     apply_filters( 'wonolog.example', $foo, $bar );
    
  • @inheritdoc makes it almost impossible to properly read code without an IDE. See http://wpkrauts.com/2015/code-usability-inheritdoc/
  • Not sure if already done but it should be highlighted that the context keys user_id and user_logged are reserved by Wonolog core and will be overwritten when used in custom logging.

Code

  • Data\FailedLoginListener: besides #2 the implementation isn't really immutable. The return value of message() depends on whether level() was called before or not. The implementation of count_attempts() is a bit confusion on first sight: one need to know how level() uses the return value to understand the meaning of $do_log for example. (Already addressed in https://github.com/inpsyde/wonolog/tree/issue-2-failed-login-level-cache )
  • Data\LogDataInterface: (beside the Interface part of the name 😉 ) this structure is a good thing. This is something that I still miss at Monolog itself.
  • Data\Log::from_array() I skipped this for now. If it's well tested, I agree with it. :) Only one thing: I would place the constructor at first place and the named constructors below.
  • Data\WpErrorChannel: This is eye-candy:
  while ( ! $channel && $codes ) {
      $code    = array_shift( $codes );
      $channel = $this->maybe_db_channel( $code );
      $channel or $channel = $this->maybe_http_channel( $code );
      $channel or $channel = $this->maybe_security_channel( $code );
  }

🙂

  • Data\WpErrorChannel: should the return value of the filter wonolog.wp-error-channel be validated right there where it is applied? Otherwise this could lead to errors later on and more difficult to debug?
  • Data\WpErrorChannel::maybe_http_channel(): could maybe be simplified using an array to store all the possible keywords in.
  • HookListeners\CronDebugListener::filter(): are you sure that _get_cron_array() always returns a list of arrays? If not, the collection of cron hook names will fail. Maybe we should check for is_array( $crons ) in the array_reduce() callback?
  • HookListeners\HttpApiListener::is_error(): Should every HTTP response code beside 200 really be considered as as error?
  • HookListeners\HttpApiListener::logHttpError(): indentation could be aligned with the logCron() method.
  • LogActionSubscriber: currently there's an unused import of Inpsyde\Wonolog\Data\Debug
  • LogLevel::check_level(): could get a description what this check is meant for.
  • Not sure about the recent update that introduces the wonolog.log.<LEVEL> hooks. IMO this is to much of variation in the way you can use Wonolog. It brings more complexity to Wonolog core without much benefit. Instead of do_action( 'wonolog.log.notice', ...) you could already do do_action( 'wonolog.log', new Notice(...) ). Having too many options leads often to confusion or inconsistent code or even to unexpected bugs: For example: what happens if I do do_action( 'wonolog.log.notice', new Warning( ... ) )?
  • General: Is it intended to mix camelCase and wp_style mehtod namings?
  • Naming domains: You use singular for exceptions (Wonolog\Exception) but plural for hook listeners: (Wonolog\HookListeners). Personally I prefer to use always singular. Was this intended?

Performance

  • Data/FailedLogin: are you sure it's a good idea to write failed login-attempts to a site-transient? I have no much experience with it but it might get a problem if. As it can be disabled, I see no problem but the implications might be mentioned in one or two sentences in the readme.

Simplify `Log::from_array()`

Log::from_array() is very complex, and in case a non-associative array is passed it tries to "guess" how to create the class based on type of array items.

There might be cases where this is useful, but it does not worth the trouble. Let's keep it simple, just require and associative array.

Maybe we can make the construction from array less "fragile" by adding class constants that hold required array key. That would also allow for code completion.

Composition of properities in trait and class might be incompatible in PHP < 7

I discoverd a weird bug on a PHP 5.6 system, when I ran Wonolog within a complete site stack using wp cli:

$ wp site list
PHP Fatal error:  Cannot instantiate abstract class Inpsyde\Wonolog\Data\Log in /.../vendor/inpsyde/wonolog/src/PhpErrorController.php on line 73

It turned out that the error that is actually triggering Wonolog's error handler has the following message:

Inpsyde\Wonolog\Data\Log and Inpsyde\Wonolog\Data\LogDataTrait define the same property ($message) in the composition of Inpsyde\Wonolog\Data\Log. This might be incompatible, to improve maintainability consider using accessor methods in traits instead. Class was composed

This E_STRICT error causes that PHP handles the Log class as abstract one, obviously.

However, here's how you can reproduce it (in PHP 5.6):

<?php # -*- coding: utf-8 -*-

namespace Inpsyde\Wonolog;

use Inpsyde\Wonolog\Data\Log;

set_error_handler( function( ...$args ) {
	//echo $args[1];
	var_dump( ( new \ReflectionClass( \Inpsyde\Wonolog\Data\Log::class ) )->isAbstract() );

	new Log();
	return FALSE;
}, E_ALL );

error_reporting( E_ALL );

$require = function() {
	require_once __DIR__ . '/vendor/psr/log/Psr/Log/LoggerInterface.php';
	require_once __DIR__ . '/vendor/monolog/monolog/src/Monolog/Logger.php';
	require_once __DIR__ . '/vendor/inpsyde/wonolog/src/Data/LogDataInterface.php';
	require_once __DIR__ . '/vendor/inpsyde/wonolog/src/Data/LogDataTrait.php';
	require_once __DIR__ . '/vendor/inpsyde/wonolog/src/Data/Log.php';
	require_once __DIR__ . '/vendor/inpsyde/wonolog/src/Channels.php';
};
$require();

Running this script via CLI will give you this error:

$ php testfile.php
bool(true)
PHP Fatal error:  Cannot instantiate abstract class Inpsyde\Wonolog\Data\Log in /.../testfile.php on line 11

The solution is to remove any duplicate declaration of properties in classes, that was declared by any used trait.

Data\FailedLogin::level() returns different values on consecutive calls (edited)

For example \Inpsyde\Wonolog\Data\FailedLogin::level() returns 0 in case the log-in failed for the first time for a specific IP. This leads to an uncaught Psr\Log\InvalidArgumentException thrown by Monolog\Logger:

Fatal error: Uncaught Psr\Log\InvalidArgumentException: Level "0" is not defined, use one of: 100, 200, 250, 300, 400, 500, 550, 600 in /var/www/vendor/monolog/monolog/src/Monolog/Logger.php:461 Stack trace:
#0 /var/www/vendor/monolog/monolog/src/Monolog/Logger.php(292): Monolog\Logger::getLevelName(0)
#1 /var/www/vendor/inpsyde/wonolog/src/LogActionSubscriber.php(148): Monolog\Logger->addRecord(0, '3 failed login ...', Array)
#2 /var/www/vendor/inpsyde/wonolog/src/LogActionSubscriber.php(90): Inpsyde\Wonolog\LogActionSubscriber->update(Object(Inpsyde\Wonolog\Data\FailedLogin))
#3 /var/www/public/wp/wp-includes/plugin.php(524): Inpsyde\Wonolog\LogActionSubscriber->listen(Object(Inpsyde\Wonolog\Data\FailedLogin))
#4 /var/www/vendor/inpsyde/wonolog/src/FrontController.php(187): do_action('wonolog.log', Object(Inpsyde\Wonolog\Data\FailedLogin))
# 5 …
in /var/www/vendor/monolog/monolog/src/Monolog/Logger.php on line 461.

Update: Okay, how this could go through this check:

if ( ! did_action( 'wonolog.loaded' ) || ! ( $log->level() > 0 ) ) {
	return FALSE;
}

🤔

Something weird is going on here:
wonolog-level-0

This is how the site transient looks like:

$ wp site option get _site_transient_wonolog.failed-login-count
array (
  '127.0.0.1' => 
  array (
    'count' => 4,
    'last_logged' => 3,
  ),
)

Update
It seems that the problem is the implementation of Inpsyde\Wonolog\Data\FailedLogin::level: it increment the log-in attempt via count_attempts() each time it gets called. That's why it can return different values on consecutive calls. Under some rarely conditions it seems to return some valid value on the first call and 0 on the next.

It looks like the counting of login attempts should be cached internally.

Errors not appearing on sematext/Raygun

Version Information

  • PHP: 7.3
  • WordPress: latest

Steps to Reproduce

  1. Add a service provider like sematext or Raygun
  2. Push errors through do_action
  3. Nothing appears
  4. Use a SyslogUdpHandler
  5. Push errors through without wonolog do_action
  6. Something appears

When do not add any external handler everything appears on the files.

What I Expected

I espected that the WordPres internal errors and my custom log errors appears on the service.
Now I only have the wonolog basic errors.

What Happened Instead

Nothing is pushed to the web service.

If I'm using directly Monolog with SyslogUdpHandler, every data is pushed to sematext.
I am missing something ?

Question: Getting wonolog to work

I am relatively new to wordpress and PHP. I would like to use wonolog to log. When I read the instructions I see it says:

  1. install Wonolog via Composer;
  2. ensure Composer autoload is loaded in wp-config.php or anytime before the 'muplugins_loaded' action is fired;
  3. create a MU plugin that, at least, contains this code:

I was able to do 1. I now see this file structure:

bash-5.0# ls
composer.json         wp-activate.php       wp-content            wp-login.php
composer.lock         wp-admin              wp-cron.php           wp-mail.php
index.php             wp-blog-header.php    wp-debug.log          wp-settings.php
license.txt           wp-comments-post.php  wp-includes           wp-signup.php
readme.html           wp-config-sample.php  wp-links-opml.php     wp-trackback.php
vendor                wp-config.php         wp-load.php           xmlrpc.php
bash-5.0# ls vendor
autoload.php  composer      inpsyde       monolog       psr

But I don't know how to do 2 and 3. Could someone provide steps for 2 and 3?

Version 1 already uses PHP 7 code

While review the RFC for version 2 I noticed that LogActionSubscriber already uses \Throwable which was introduced at PHP 7 but Wonolog states to be compatible with 5.6.

The fact that this wasn't discovered until now leads me to the assumption that PHP 5.6 isn't used any more together with Wonolog. I just leave that here as «known Issue» for the sake of completeness. If we decide to fix it we should look throughout the code base to make sure it is actually compatible with 5.6

Multiple Handlers not working

Hey guys,
I am not able to get the following to work:

$wonolog_context = Inpsyde\Wonolog\bootstrap();
$awesome_handler = new MyAwesomeHandler();
$wonolog_context->use_handler($awesome_handler);

What I expect is, that everything is logged to disk and also to MyAwesomeHandler, but it is only logged to disk. When I do the following, MyAwesomeHandler is working as expected, but obviously no Disk Logging:

$wonolog_context = Wonolog\bootstrap( null, Wonolog\USE_DEFAULT_NONE );
$awesome_handler = new MyAwesomeHandler();
$wonolog_context-> use_default_handler($awesome_handler);

Using Processors for WordPress extra information

Currently, Wonolog adds user_logged and user_id (which is by the way ambiguous I think) to the context of each record. Monolog provides therefore an extra section with each log, where Processors can add general context information to each record.

IMO it would be better to use a Wonolog WP context processor that could be filtered to be used by default in all loggers or replaced, if desired.

Here's a quick example of a processor, that adds some context like multisite information, user ID and if in AJAX, CRON or CLI context:

final class WpContextProcessor implements MonologProcessor {

	/**
	 * @var int
	 */
	private $site_id;

	/**
	 * @var bool
	 */
	private $is_rest_request = NULL;

	/**
	 * @param int $site_id ID of the initializing WP site
	 */
	public function __construct( $site_id ) {

		$this->site_id = (int) $site_id;
	}

	/**
	 * @param array $record The complete log record containing 'message', 'context'
	 *                      'level', 'level_name', 'channel', 'datetime' and 'extra'
	 *
	 * @return array
	 */
	public function __invoke( array $record ) {

		$contexts = [];
		( defined( 'DOING_CRON' ) && DOING_CRON ) and $contexts[] = 'cron';
		( defined( 'DOING_AJAX' ) && DOING_AJAX ) and $contexts[] = 'ajax';
		$this->doing_rest() and $contexts[] = 'rest';

		did_action( 'init' ) and $record[ 'extra' ][ 'wp_user_id' ] = get_current_user_id();
		$record[ 'extra' ][ 'wp_context' ]     = implode( ',', $contexts );
		$record[ 'extra' ][ 'wp_ms_switched' ] = ms_is_switched();
		$record[ 'extra' ][ 'wp_site_id' ]     = get_current_blog_id();
		$record[ 'extra' ][ 'wp_network_id' ]  = get_main_network_id();

		return $record;
	}

	/**
	 * @return bool
	 */
	private function doing_rest() {

		if ( NULL !== $this->is_rest_request ) {
			return $this->is_rest_request;
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			$this->is_rest_request = TRUE;
			return TRUE;
		}

		$home_url_path   = rtrim( parse_url( get_home_url( $this->site_id, '/' ), PHP_URL_PATH ), '/' );
		$rest_url_prefix = rest_get_url_prefix();

		$this->is_rest_request = 0 === strpos(
			filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_STRING ),
            "{$home_url_path}/{$rest_url_prefix}/"
		);

		return $this->is_rest_request;
	}
}

The interface MonologProcessor just covers the lack of an interface provided by Monolog itself.

PR is under process…

[Bug]: PHP Deprecated: Return type of Inpsyde\Wonolog\Processor\ProcessorsRegistry::count()

Description of the bug

PHP deprecation notice in PHP 8.1.

PHP Deprecated:  Return type of Inpsyde\Wonolog\Processor\ProcessorsRegistry::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /usr/local/wordpress/vendor/inpsyde/wonolog/src/Processor/ProcessorsRegistry.php on line 93

Reproduction instructions

Log into the admin, and note tailed error logs.

Expected behavior

No logs.

Environment info

  • Wonolog ^1.0
  • WordPress 6.0
  • PHP 8.1

Relevant log output

PHP Deprecated:  Return type of Inpsyde\Wonolog\Processor\ProcessorsRegistry::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /usr/local/wordpress/vendor/inpsyde/wonolog/src/Processor/ProcessorsRegistry.php on line 93

Additional context

See code examples for #[\ReturnTypeWillChange] fix in core: https://github.com/WordPress/wordpress-develop/search?q=%23%5BReturnTypeWillChange%5D

Code of Conduct

  • I agree to follow this project's Code of Conduct

Question: How to set log level

Greetings -

Can you advise how to set the minimum level for the default handler at bootstrap time? I am getting DEBUG.DEBUG level messages and I only want DEBUG.INFO.

Thanks!

Add documentation for actions and filters

All fired actions and applied filters should be documented with a full phpDoc block:

/**
 * Fires right before ... | Filters ...
 * 
 * @param string $username The current user's name.
 */

Log to custom channel

Is your feature request related to a problem? Please describe.
I'm trying to configure a custom logging channel so that the logs from our plugin can be easily distinguished from log records from other plugins that may be using wonolog.

Describe the solution you'd like
A way to set a custom channel that we can log to. Looking at the code in Channels.php, it raises an exception if a channel is specified that the comoponent is unaware of which implies, that it should be possible to add more known channels.

Describe alternatives you've considered
We can simply create a wrapper for our plugin that prefixes all log records with a custom string but this feels like a cludge given teh level of customizable features in other aspects of Wonolog.

Disable specific listener within a Channel

Awesome package guys!

I just wanted to ask if there is a way to disable a specific listener/action within a Channel, but not the whole Channel.

For example if I wanted to disable just the login failed check within the Security channel, how would I do that?

Is there a filter that lists all available actions within a channel where I can unset some of them?

Incompatibility with johnbillion/query-monitor on PHP errors handling

Hello,

I've been playing with wonolog, that I may want to include as an embed component in the future releases of our custom WP stack

So far, I'm impressed with the quality of the documentation that is very complete for a first public release, and the code / functionalities seem to match their purpose, well done here !

However, I have noticed that, when used in a WP install with plugin johnbillion/query-monitor activated, PHP errors are no longer caught and logged by wonolog.

This is pretty logical, because wonolog and query-monitor both override php error handler and shutdown function, and because query-monitor is loaded after wonolog, wonolog doesn't have control anymore on php errors.

Do you have already thought about this incompatibility ? Do you have an idea on how we should deal with it ? My purpose here would be to find a way of having both handlers functionalities working at the same time.

I've submit a PR on query-monitor that should be merged in the next release, and will allow us to disable / override the PHP error controller of the plugin.

After that, how can we make both functionnalities work ?

  1. Disable query-monitor PHP errors collector and hook on \Inpsyde\Wonolog\LOG to send data to query-monitor ?

  2. Extends wonolog PhpErrorController with a class that handles both functionalities, registering it as both wonolog controller and query-monitor collector ?

  3. Something better ?

Bests regards,
Pierre

HTTP listener false positive error for non-blocking requests

Version Information

  • PHP: 7.1.9
  • WordPress: 4.8.3

Steps to Reproduce

  1. Install Wordpress
  2. Install Wonolog
  3. Include the composer autoload script on wp-config.php
  4. Create the mu-plugin for initializing Wonolog

What I Expected

Nothing is shown on the error log since it's a brand new install without anything else.

What Happened Instead

Wonolog keeps registering the following error:

HTTP.ERROR:` WP HTTP API Error - Response code: . {"transport":"Requests","context":"response","query_args":{"method":"POST","timeout":0.01,"redirection":5,"httpversion":"1.0","user-agent":"WordPress/4.8.3; http://wordpress.local","reject_unsafe_urls":false,"blocking":false,"headers":[],"cookies":[],"body":null,"compress":false,"decompress":true,"sslverify":false,"sslcertificates":"/data/wordpress/wp-includes/certificates/ca-bundle.crt","stream":false,"filename":null,"limit_response_size":null,"_redirection":5},"url":"http://wordpress.local/wp-cron.php?doing_wp_cron=1509694724.2679729461669921875000","headers":"[object] (Requests_Utility_CaseInsensitiveDictionary: {})"} {"wp":{"doing_cron":false,"doing_ajax":true,"is_admin":true,"user_id":1}}```

Consisten return type in MailerListener::on_mail_failed()

	/**
	 * @param array $args
	 *
	 * @return Log
	 */
	private function on_mail_failed( array $args ) {

		$error = $args ? reset( $args ) : NULL;
		if ( is_wp_error( $error ) ) {

			return Log::from_wp_error( $error, Logger::ERROR, Channels::HTTP );
		}
	}

In case the first argument is not an error, the method returns nothing.

Simplify process of registering handlers and customizing default handler

At the moment Wonolg ships a default handler that needs to be configured.

It has a lot of configurations, and should probably be simplified.

Moreover, entirely replace with a custom handler is quite complex as well.

The whole thing should probably be reconsidered taking into account possibility to expose more the Monolog registry.

PHP Error logger includes a dump of wp_filter in the context

I found this in a Log file on a testing system:

2016-11-16 13:31:58] PHP-ERROR.WARNING: call_user_func_array() expects parameter 1 to be a valid callback, class '****\StorefrontChildTheme\Admin\addTags' does not have a method 'register_admin_settings' {"tag":"admin_init","arg":"","wp_filter":{"muplugins_loaded":{"20":{"Inpsyde\\Wonolog\\FrontController::boot":{"function":["Inpsyde\\Wonolog\\FrontController","boot"],"accepted_args":0}}},"plugins_loaded":{"0":{"000000005ed4348000007fdbdc2abd6e":{"function":"[object] (Closure: {})","accepted_args":1},"wp_maybe_load_widgets":{"function":"wp_maybe_load_widgets","accepted_args":1},"wp_maybe_load_embeds":{"function":"wp_maybe_load_embeds","accepted_args":1},"mlp_init":{"function":"mlp_init","accepted_args":1},"000000005ed436e800007fdbdc2815aeinit":{"function":["[object] (Woocommerce_Instagram: {\"context\":{\"integration\":null},\"api\":{\"_file\":\"/home/*****/htdocs/deploy/releases/20161116131919/public/wp-content/plugins/woocommerce-instagram/woocommerce-instagram.php\"},\"_has_video\":false,\"version\":\"1.0.10\"})","init"],"accepted_args":1}}…

Something is dumping $GLOBALS[ 'wp_filter' ] in the context of the log record. Needless to say, that this bloat the log file to an almost unusable level. A quick remote log file analysis with tail, less and grep is almost impossible with log records of this length.

A quick inspection didn't brought me to the source of what is causing this behavior but I think this isn't desired. I'll keep on looking for a reason.

Remove VariableLevelLog

As discussed in #11 class Inpsyde\Wonolog\Data\VariableLevelLog is not in use, and even if it might have use cases, at them moment it add complexity for no reason.

Let's just remove it.

.htaccess in wp-content/wonolog directory is not compatible with mod_authz_core (Apache 2.3+)

Hi,

the subject says it all :-) It's a minor issue, but it's easy to make .htaccess compatible with newer Apache versions.

Version Information

Steps to Reproduce

  1. Browse the website, trigger generation of wp-content/wonolog/ directory with .htaccess file
  2. Try to access wp-content/wonolog/

What I Expected

403 Forbidden page

What Happened Instead

500 Internal Server Error page

How to fix

<IfModule mod_authz_core.c>
	Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
	Deny from all
</IfModule>

Logging a WP_Error with no data triggers a fatal error

Version Information

  • PHP: 7.2
  • WordPress: 4.9

Steps to Reproduce

Attempt to log a WP_Error object that doesn't contain a $data property, for example by using the following PHP code:

do_action( 'wonolog.log.error', new \WP_Error( 'hello', 'world' ) );

What I Expected

The WP_Error object gets logged.

What Happened Instead

A fatal error is triggered by Wonolog:

Uncaught TypeError: Argument 4 passed to Inpsyde\Wonolog\Data\Log::__construct() must be of the type array, null given, called in src/Data/Log.php on line 74

Configuring arbitrary log paths

I see that Wonolog uses dates in constructing a file path for its file log:

eg: wonolog/2018/02/06.log

Due to system preferences here, we want an arbitrary path eg wonolog/system.log

Can you suggest how I could configure this?

Thanks!

[Bug]: PHP 8.1 PHP Deprecated: Constant Inpsyde\Wonolog\Data\FILTER_SANITIZE_STRING is deprecated

Description of the bug

Using FILTER_SANITIZE_STRING is deprecated in PHP 8.1.

PHP Deprecated:  Constant Inpsyde\Wonolog\Data\FILTER_SANITIZE_STRING is deprecated in /usr/local/wordpress/vendor/inpsyde/wonolog/src/PhpErrorController.php on line 100

Reproduction instructions

Install PHP 8.1, and turn on WP_DEBUG, note logs after logging in a traversing the admin core pages.

Expected behavior

No deprecation notices.

Environment info

  • Wonolog ^1.0
  • WordPress 6.0
  • PHP 8.1

Relevant log output

No response

Additional context

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

Allow listeners to use customize priority

Wonolog "hook listeners" internally use add_filter / add_action to trigger log events.

At the moment, there's no way to customize the priority that will be used. In fact, Wonolog always use PHP_INT_MAX - 10, a late priority to allow any real logic attached to hooks to - very likely - happen first the log happen.

This should be fine most of the times, but there should be a way to customize priority if one needs to do it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.