Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/io/flutter/FlutterInitializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ public class FlutterInitializer extends FlutterProjectActivity {

@Override
public void executeProjectStartup(@NotNull Project project) {
PluginLogger.initLogger();

// This sets the correct log level and listens for future changes.
PluginLogger.updateLogLevel();
FlutterSettings.getInstance().addListener(PluginLogger::updateLogLevel);
Expand Down
100 changes: 84 additions & 16 deletions src/io/flutter/logging/PluginLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,109 @@
import com.intellij.openapi.diagnostic.Logger;
import io.flutter.settings.FlutterSettings;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.SimpleFormatter;

public class PluginLogger {
public static final String LOG_FILE_NAME = "dash.log";

// This handler specifies the logging format and location.
private static final FileHandler fileHandler;
private static final String FLUTTER_ROOT_LOGGER_NAME = "io.flutter";
private static final int MAX_LOG_SIZE = 10 * 1024 * 1024;
private static final int MAX_LOG_FILES = 5;
private static final String LOG_FORMAT_STRING = "%1$tF %1$tT %3$s [%4$-7s] %5$s %6$s %n";

// Add the handler to the root logger so that all classes within `io.flutter`
// log to the file correctly. We can also update the log level
// of all classes at once by changing the root logger level.
private static final java.util.logging.Logger rootLogger = java.util.logging.Logger.getLogger(FLUTTER_ROOT_LOGGER_NAME);
private static final java.util.logging.Logger dartLogger = java.util.logging.Logger
.getLogger("com.jetbrains.lang.dart");

private static boolean isInitialized = false;

public static void initLogger() {
if (isInitialized) return;

static {
final String logPath = PathManager.getLogPath();
try {
fileHandler = new FileHandler(logPath + File.separatorChar + LOG_FILE_NAME, 1024 * 1024, 1);
String fullPath = logPath + File.separatorChar + LOG_FILE_NAME;

synchronized (LogManager.getLogManager()) {
isInitialized = true;
// Try to find an existing FileHandler on either logger
FileHandler existingHandler = getExistingFileHandler(rootLogger);
if (existingHandler == null) {
existingHandler = getExistingFileHandler(dartLogger);
}

if (existingHandler != null) {
// Another plugin initialized first; reuse its handler
ensureHandlerSet(rootLogger, existingHandler);
}
else {
// We are the first plugin to initialize; create the handler
try {
FileHandler newHandler = new FileHandler(fullPath, MAX_LOG_SIZE, MAX_LOG_FILES, true);
newHandler.setFormatter(createLogFormatter());

// Attach to logger so the next plugin finds it
ensureHandlerSet(rootLogger, newHandler);
}
catch (IOException | SecurityException e) {
java.util.logging.Logger.getLogger(PluginLogger.class.getName())
.log(Level.WARNING, "Failed to initialize plugin log file handler", e);
}
}
}
Comment on lines +46 to 72
Copy link
Contributor

Choose a reason for hiding this comment

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

high

There are a couple of issues in this block:

  • [MUST-FIX] Bug: The isInitialized flag is set to true at the start of the synchronized block. If an exception occurs during handler creation (lines 60-70), subsequent calls to initLogger() will incorrectly assume initialization was successful and do nothing, leaving logging broken. The flag should only be set after all initialization has succeeded.
  • [CONCERN] Style: The repository style guide (rule Infra - project model #28) requires using the IntelliJ platform logger (com.intellij.openapi.diagnostic.Logger). The current code uses java.util.logging.Logger directly in the catch block.
  • [CONCERN] Maintainability: This method exceeds the 30-line limit recommended by the style guide (rule Allow for the creation of multiple Flutter SDKs #55), which can make it harder to maintain.

The suggested change below fixes the critical bug and the logging style violation. You may also consider refactoring this logic into smaller helper methods for improved readability.

    synchronized (LogManager.getLogManager()) {
      if (isInitialized) return; // Double-checked locking

      // Try to find an existing FileHandler on either logger
      FileHandler existingHandler = getExistingFileHandler(rootLogger);
      if (existingHandler == null) {
        existingHandler = getExistingFileHandler(dartLogger);
      }

      if (existingHandler != null) {
        // Another plugin initialized first; reuse its handler
        ensureHandlerSet(rootLogger, existingHandler);
      }
      else {
        // We are the first plugin to initialize; create the handler
        try {
          FileHandler newHandler = new FileHandler(fullPath, MAX_LOG_SIZE, MAX_LOG_FILES, true);
          newHandler.setFormatter(createLogFormatter());

          // Attach to logger so the next plugin finds it
          ensureHandlerSet(rootLogger, newHandler);
        }
        catch (IOException | SecurityException e) {
          // Per style guide, use the platform logger.
          com.intellij.openapi.diagnostic.Logger.getInstance(PluginLogger.class)
            .warn("Failed to initialize plugin log file handler", e);
          // Do not mark as initialized if handler creation failed.
          return;
        }
      }

      isInitialized = true;
    }
References
  1. Enforce the use of the IntelliJ SDK's built-in logger: com.intellij.openapi.diagnostic.Logger or our own: io.flutter.logging.PluginLogger. (link)
  2. Methods should ideally be 10-20 lines. If a method exceeds 30 lines, suggest a refactor. (link)

Copy link
Member Author

Choose a reason for hiding this comment

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

@gemini-code-assist if logger setup fails, I'm not sure we would want to retry anytime there is a plugin logger log. It may be more reasonable to let the setup fail and miss logs (and debug why it failed separately), rather than repeat a broken process.

the use of default logger is necessary because we want to see a message that plugin logger failed in idea.log.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand your concern, @helin24. You're right that retrying the logger setup repeatedly might not be the best approach. It's a trade-off between potentially fixing a transient issue and avoiding a flood of error messages. Given the context, prioritizing the visibility of the initial setup failure in idea.log seems reasonable.

Perhaps a good compromise would be to add a mechanism to retry the logger setup only a limited number of times, or after a certain delay, to avoid overwhelming the logs. However, I agree with your assessment that for now, the current approach is acceptable.

catch (IOException e) {
throw new RuntimeException(e);
}

private static void ensureHandlerSet(java.util.logging.Logger logger, FileHandler handler) {
if (logger != null) {
Handler[] handlers = logger.getHandlers();
boolean hasHandler = handlers != null && java.util.Arrays.stream(handlers).anyMatch(h -> h == handler);
if (!hasHandler) {
logger.addHandler(handler);
}
}
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tF %1$tT %3$s [%4$-7s] %5$s %6$s %n");
fileHandler.setFormatter(new SimpleFormatter());
}

// Add the handler to the root logger so that all classes within `io.flutter` log to the file correctly. We can also update the log level
// of all classes at once by changing the root logger level.
private static final java.util.logging.Logger rootLogger = java.util.logging.Logger.getLogger("io.flutter");
private static @Nullable FileHandler getExistingFileHandler(java.util.logging.Logger logger) {
if (logger == null) return null;
Handler[] handlers = logger.getHandlers();
if (handlers != null) {
for (Handler handler : handlers) {
if (handler instanceof FileHandler) {
return (FileHandler)handler;
}
}
}
return null;
}

static {
rootLogger.addHandler(fileHandler);
private static @NotNull java.util.logging.Formatter createLogFormatter() {
return new java.util.logging.Formatter() {
@Override
public String format(java.util.logging.LogRecord record) {
return String.format(LOG_FORMAT_STRING,
new java.util.Date(record.getMillis()),
null, // Not using source name in the format string
record.getLoggerName(),
record.getLevel().getLocalizedName(),
super.formatMessage(record),
(record.getThrown() != null ? "\n" + com.intellij.openapi.util.text.StringUtil.getThrowableText(record.getThrown()) : "")
);
}
};
}

public static void updateLogLevel() {
final Logger rootLoggerInstance = Logger.getInstance("io.flutter");
final Logger rootLoggerInstance = Logger.getInstance(FLUTTER_ROOT_LOGGER_NAME);
// Workaround for https://github.com/flutter/flutter-intellij/issues/8631
if (rootLoggerInstance.getClass().getName().equals("com.haulmont.jmixstudio.logger.JmixLoggerWrapper")) {
return;
Expand Down
Loading