Skip to content

Log revamp #255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 29 commits into from
Closed

Log revamp #255

wants to merge 29 commits into from

Conversation

maarzt
Copy link
Contributor

@maarzt maarzt commented Jan 27, 2017

Proposals concerning pull request #253:

  • Don't publish stdout/stderr to log channels, log messages and stream are too different in their concepts.
  • Refactoring: Use CopyOnWriteArrayList to manage LogListeners, extract LogFormatter from StderrLogService, make LogLevel an enum.
  • Make DefaultLogService::channel(String name) thread safe
  • Add the ability to listen to all log channels at once. Use this in StderrLogService.

@maarzt maarzt force-pushed the log-revamp branch 2 times, most recently from 02af11d to 20c7e1d Compare February 22, 2017 10:12
@maarzt maarzt force-pushed the log-revamp branch 3 times, most recently from 779b675 to c7baaa6 Compare March 6, 2017 16:25
@ctrueden
Copy link
Member

Hey @maarzt, just wanted to chime in quickly that I have not forgotten about this work! It is high up on my priority list now. I probably will not have time to check it today, but will try to go through everything on Monday.

@maarzt
Copy link
Contributor Author

maarzt commented Mar 13, 2017

Hi @ctrueden,
that's great. I used the new api in @fjug's Tr2d plugin, but sadly, it didn't worked out very well. There are still open questions not answered by the current design. And I don't feel very comfortable with making design changes without discussion.

  • How do we want to set the log level, now that there are multiple log channels? Who is responsible/able to set the Level?
  • In Tr2d many log messages are lost, before everything is fully set up. A coherent concept, how log messages are delivered to it's destination and how to set up loggers, seems to be necessary.
  • You wanted to forward system.out and err to log channels, to get a context for them. What would you like to use this information for?
    Sorry for the many questions... Matthias

@ctrueden
Copy link
Member

Quick update: I still, frustratingly, have not gotten to this code review + discussion. It is still at/near the very top of my list. Unfortunately, I first need to fix the broken HTTPS certs on one of the SciJava server machines (so that e.g. https://maven.imagej.net/ starts working again, and continues working into the future, including automatic cert renewals). There is no way I will complete that work, and circle back around to this PR, before the end of this week. However, I just wanted to let you know that it is still very much on my radar, in queue.

@maarzt
Copy link
Contributor Author

maarzt commented Mar 22, 2017

Hey @ctrueden,

I thought about this logger stuff again and again. Mainly I could not find any useful application of the LogService.channel feature introduced in this PR. (Since I don't like forwarding stdout and err to log channels.)

After some discussions, I think the current solution in master is quite close to a pretty good solution. When executing a Command, we give an object to it (LogService), which is used by to Command for logging. This pattern is simple and powerful. As the caller, who provides LogService, has full control of the log messages.

This gives a simple solution to the user story I described in #253:
2. User story: Plugin A uses the functionality of plugin B. Plugin A does not want plugin B to open a logging window. But plugin A still wants plugin B's log messages to be visible in a central logging window or in the logging panel of plugin A.

Plugin A simply creates it's custom LogService and hands it over the plugin B when calling pulgin B. This gives plugin A full control over plugin B's log messages. Plugin A can display the messages in it's on logging panel, and it can forward the logging messages to a central logging window.

Problem solved without changing scijav. Cheers

@maarzt
Copy link
Contributor Author

maarzt commented Mar 22, 2017

But there is a second goal. We want the Console window to be smarter when dealing with log messages. First of all the Console window should listen to ConsoleService to get stdout and stderr. Second the Console window should listen directly to LogService to get the all log messages with all meta data. Then I will add functionality to the Console window like search, filtering, log level adjustment and customizable pop up conditions, and it will be much smarter.

Here comes the third goal. We want meta data, that tells us where the log messages come from. But how do we retrieve this information. I'm afraid inspection the stack trace will be slow an buggy. Because retrieving the stack trace is slow (throwing a exception is slightly dirty hack). And the question: Which class to pick from the call stack is hard to answer, and will result in unexpected behavior.
I would suggest a different solution, which is against the rules, because I would like to have several "LogServices". One LogService instance per Command that is executed. Each LogService knows which Command it belongs to, and it append's this information to the log messages it get's. Then the log messages are forwarded to a central log service like thing.

@ctrueden What do you think. Might this idea be better than inspecting the stack trace? What would be the way to go, without breaking the rules.

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SciJava style is to never use wildcard imports.

And in general: please keep changes in a patch set atomic, and matching what the commit message says is being changed. If you really wanted to change the imports like this, it should be its own commit.

@@ -49,10 +47,7 @@

private String name;

/** List of listeners for logging events. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why delete the javadoc? It wasn't hurting anything.

private ArrayList<LogListener> listeners;

private LogListener[] cachedListeners;
private List<LogListener> listeners = new CopyOnWriteArrayList<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very cool to know about. Thanks!

listeners.add(l);
cacheListeners();
}
listeners.add(Objects.requireNonNull(l));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we should be requiring non-null here at this level. However, that is a separate conceptual change, which would be better as its own commit. Or else the commit message here should state that it is also changing that behavior.

import org.junit.Before;
import org.junit.Test;

import java.util.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the IDE's import organization function to enumerate all the classes, instead of using wildcards.

private static <T> T removeFuture(Future<T> future) {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SciJava style is:

try {
	doStuff();
}
catch (final MyException exc) {
	handleStuff();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add this coding conventions to http://imagej.net/Coding_style.
And put the description of how to the Eclipse coding style for IntelliJ to a more prominent position.

Iterator<?> ia = a.iterator();
Iterator<?> ib = b.iterator();
boolean result = a.size() == b.size();
while(result && ia.hasNext() && ib.hasNext())
Copy link
Member

@ctrueden ctrueden Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SciJava style is while (condition) with leading space. Same for if, for etc.

return result;
}

private static <T> boolean allEqual(Iterable<T> l, BiFunction<T, T, Boolean> equals) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super general! Shall we put this into org.scijava.test.TestUtils?

public void run() throws InterruptedException {
List<List<Logger>> results = runParallelCollectResults(10, this::getLoggers);
boolean passed = allEqual(results, MultiThreadingTest::referencesInListEqual);
assertTrue(passed);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could collapse this into an assertAllEqual static method in org.scijava.test.TestUtils. Of course, there is value in having allEqual which returns boolean as well. How about if that lives in org.scijava.util somewhere? Right now we have ArrayUtils and ListUtils. However, we are moving more toward the ImgLib2-style (and core Java style) of plurals for utility classes. Any suggestions? Happy to move/reorganize/deprecate existing stuff into better structure along these lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if assertAllEqual will be used more than once.
There are many class in org.scijava.util what would be more appropriate ListUtils or MiscUtils?

/**
* @author Matthias Arzt
*/
class LogLevelStrategy {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cool idea.

However, could we make LogLevelStrategy an interface, and have a DefaultLogLevelStrategy which implements it along these lines? Then people can more easily override the behavior.

I also prefer everything public whenever possible. You'll notice SJC has basically no package-private level classes... I can articulate my reasons for that if you really want to know. But I pretty much prefer either public or private whenever possible. And protected only when absolutely necessary, and package-private nearly never.

@@ -107,7 +125,7 @@ public void removeLogListener(final LogListener l) {
}

@Override
public void notifyListeners(final int level, final Object msg,
public void notifyListeners(final LogLevel level, final Object msg,
Copy link
Member

@ctrueden ctrueden Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Integer enum pattern has a lot of drawbacks.

So, in general I very much agree with you. However, in this case, I think using integers, or maybe even doubles, actually makes more sense, for similar reasons to why we use doubles for priority (see org.scijava.Priority).

You can imagine wanting to be more verbose than TRACE. You can also imagine wanting to be "more verbose than INFO, but less than WARN, which would be an argument for using double here.

What means level == 0 or LogLevel.NONE? (It's possible meaning "unknown log level" is better expressed by a null reference.)

The idea of 0 i.e. NONE is that nothing should be logged, of course. That is different than "unknown" or null. Actually, I would not allow nulls here... another reason int happens to be handy, since primitives cannot be null.

What means level == -1? (Better throw an exception if it's an error.)

I agree that negative levels are not defined here, since 0/NONE is the bottommost behavior. So, we should indeed perform bounds checking and throw an exception if negative values are given anywhere in this API.

What's the maximum log level? (Now it's clearly LogLevel.TRACE.)

Does there need to be an explicit maximum? I don't think so. It is always possible to be "more verbose" than everything else that has ever existed...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And there is another reason not to introduce LogLevel: it breaks backwards API compatibility. The changes done earlier in this topic branch were done very carefully so that existing consumers of LogService will actually keep working without recompilation—at least in theory. 😉 If you want to introduce LogLevel, we need to keep all the existing signatures that use int level also, deprecate them, and have them delegate to the new signatures. Otherwise, this change needs to target SJC 3.0.0, which is the next major i.e. breaking release.

throw new IllegalArgumentException();
}

public boolean isHigherThan(LogLevel level) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not implement Comparable? Although again: I personally do not think we need/want an enum here. Too restrictive. And an extensible enum pattern seems like overkill, since there is no additional metadata we need to attach to each level, besides its int value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, there is one single piece of metadata: the label. E.g., 0=NONE, 3=INFO. This would be nice if it were extensible... and actually, it's easy to do so by following the extensible enum pattern. Take a look at net.imagej.axis.AxisType and net.imagej.axis.Axes. Would this sort of pattern satisfy you here, @maarzt? It is significantly more complex, but would allow custom log levels to name themselves. Otherwise, we could achieve the same thing by adding a method to LogService which lets people register new labels with custom log levels. This seems overly fancy and potentially error-prone to me, since we need to implement lots of methods to support it. E.g.: what if two LogLevels exist with the same int level but different labels? Easier to simply stick with int.

@ctrueden
Copy link
Member

Some general comments about the commit message style: we use imperative tense, with no period in the subject line. Please avoid prefixes like "Refactor:" and "Refactoring:". See the Coding style page, which links to this and that articles on Git commit messages.

@@ -0,0 +1,57 @@
/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the conflicts list from the commit message. It's not relevant to the actual patch, only some transient rebase.

import java.io.StringWriter;

/**
* @author Matthias Arzt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add at least a one-sentence description of the class to the javadoc here.

/**
* @author Matthias Arzt
*/
public class DefaultLogFormatter implements LogFormatter{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SciJava style is implements FooBar { with leading space before the curly brace.

import org.scijava.Named;

/**
* @author Matthias Arzt
Copy link
Member

@ctrueden ctrueden Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add javadoc. The easiest and most standard for tests is simply:

/**
 * Tests {@link DefaultLogFormatter}.
 *
 * @author Matthias Arzt
 */

return t;
}

class NamedObject implements Named {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not private class, just to be safe?

final PrintWriter message = new PrintWriter(sw);
message.print(level.prefix());
final String s = source.getName();
if (!s.equals("default"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems really hacky, to do a string check for the name "default" instead of something based on Java types.

@Override
public void alwaysLog(final LogLevel level, final Object msg, final Throwable t) {
final String message = level.prefix() + msg;
// NB: Emit severe messages to stderr, and less severe ones to stdout.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate refactoring to make things easier to read and understand, but it is very important to keep the NB notes instead of throwing them away. Every NB note indicates something of particular note, and often unintuitive, to developers reading the code. See http://imagej.net/Coding_style#Javadoc_and_comments

ctrueden and others added 3 commits May 26, 2017 18:05
We are going to add support for multiple Logger objects in the same
SciJava context. The LogService instance will itself be the default
Logger, but there will be others as well.
This change eliminates the various protected log methods of
AbstractLogService, in favor of a single method:

    alwaysLog(int level, Object message, Throwable t)

All other logging methods in the Logger interface now have default
implementations which funnel to this method, which should make it
simpler to implement this interface.

This change also introduces new methods for logging information at any
arbitrary log level:

    log(int level, Object message)
    log(int level, Throwable t)
    log(int level, Object message, Throwable t)

All level-specific logging methods now lean on these general-purpose log
methods, which do the level threshold check, and then call alwaysLog.
Move code for log level estimation to new class LogLevelStrategy
to separate responsibilities.
maarzt added 19 commits June 21, 2017 17:04
Extract code for calling class detection to new class
CallingClassUtils.  Classes that should be ignored by the calling
class detection should be annotated with IgnoreAsCallingClass.
ListenableLogger provides an interface for sending
and receiving log messages. ListenableLogger extends the
even simpler Logger interface. The Logger interface
should be used in client code that only produces log messages,
but doesn't process those messages.
This provides an Implementation of ListenableLogger that can be used
independently from AbstractLogService. It will be usefull for the
implementation of sub loggers.
Logger A being a sub logger of logger B, means logger A
forwards the messages it receives to B.
A client might want to log to a given Logger instance, and at the
same time listen to it's own (and only to it's own) log messages.
The client can achieve this, by deriving a ListenableLogger with the
new methods from the give Logger instance. The client will have to
log and listen to the ListenableLogger. The messages will automatically
be forwarded to the given Logger instance.
This PreprocessorPlugin affects modules with a single Parameter of type
Logger. It will get a logger from LogService, that is named like the
module's class and assign it to the Parameter.
A client can retrieve a private logger by calling Loggers.newRoot().
This ListenableLogger will be independent of LogService.

If the logging of a client should be disable Loggers.silent() can be
used as Logger.

NB: DefaultListenableLogger is not public because it's implementation
might change for maintenance reasons, and client code should not depend
on it.
Beside of being hard to understand, the initialization of
MultiplePrintStream in DefaultConsoleService was not synchronized
correctly. The global resources System.out and System.err where modified
like this: read, modify, write. This can fail if two
DefaultConsoleServices (in two scijava contexts) do it in parallel.

This problem is solved in ListenableSystemStreams by using singletons.
…essage

Added a method containsLog to OutputEvent,

For OutputEvents containing a log message, the method containsLog will
return true.
Log messages need to be stored, before they can be displayed.
LogRecorder is suited for this purpose. It can act as LogListener
and record log messages for several concurrent threads. At
the same time stored log messages can be red asynchronously
using Iterators.
@ctrueden
Copy link
Member

To make it easier for us to work on this together, I have taken the latest work here, rebased it to do some cleanups, and then pushed it as log-revamp to this repository. @maarzt I invited you to join the scijava-common team so that you have write access, so we can work on the same branch.

Hence: closing in favor of #272.

@ctrueden ctrueden closed this Jun 22, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants