Java Logging com um arquivo por pacote raiz

Oct 22 2020

Eu tenho um conjunto de classes em vários pacotes e quero que qualquer registro de dentro de um único pacote "com.name.root.xxx" e todos os seus pacotes filho seja registrado em um arquivo diferente. por exemplo:

//Package Name                    Log To     \\
//-------------------------------------------\\
com.name.root.router         C:\com\router.log
com.name.root.router.utils   C:\com\router.log
com.name.root.init           C:\com\init.log
com.name.root.database       C:\com\database.log
com.name.root.web            C:\com\web.log
com.name.root.web.rest       C:\com\web.log
com.name.root.web.http       C:\com\web.log

etc.

Eu criei uma classe auxiliar que rastreia quais caminhos de arquivo de log já foram configurados com manipuladores de arquivo e também quais pacotes já tiveram seus loggers configurados; e fornece em conformidade.

Estou interessado em revisar a eficiência e se houver uma maneira melhor (mais simples / mais limpa / mais compreensível) de fazer isso:

package com.name.root.util.log;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LogProvider
{
    private static final String rootPackageName = "com.name.root";
    private static final int rootPackageNameLength = rootPackageName.length();
    private static final HashMap<String,FileHandler> fileHandlersByPath = new HashMap<>();
    private static final HashSet<String> alreadyProvidedPackages = new HashSet<>();

    public static Logger getConfiguredLogger(Class<?> callingClass, String logPathIfNotAlreadySet)
    {
        return getConfiguredLogger(callingClass, logPathIfNotAlreadySet, Level.FINEST); // default log level
    }

    public static Logger getConfiguredLogger(Class<?> callingClass, String logPathIfNotAlreadySet, Level maxLogLevelIfNotAlreadySet)
    {
        String fqClassName = callingClass.getCanonicalName(); // e.g. com.name.root.router.utils
        String packageName = fqClassName; // default
        if (fqClassName.startsWith(rootPackageName))
        {
            // we want to just get as far as the main package after "com.name.root" - e.g. "com.name.root.router"
            packageName = fqClassName.substring(0,fqClassName.indexOf(".", rootPackageNameLength+1));
        }
        
        return getConfiguredLogger(packageName, logPathIfNotAlreadySet, maxLogLevelIfNotAlreadySet);
    }

    private static Logger getConfiguredLogger(String packageName, String logPathIfNotAlreadySet, Level maxLogLevelIfNotAlreadySet)
    {
        Logger logger = Logger.getLogger(packageName); // get the logger for the package
        if (alreadyProvidedPackages.contains(packageName))
        {
            return logger; // we've already configured this logger
        }
        else
        {
            alreadyProvidedPackages.add(packageName);

            logger.setLevel(maxLogLevelIfNotAlreadySet);
            
            String logPath = (logPathIfNotAlreadySet == null || logPathIfNotAlreadySet.isBlank() ? "C:\\com\\output.log" : logPathIfNotAlreadySet);
            
            try
            {
                // reuse an existing file handler if possible, so we don't get multiple output files if two packages want to log to the same file
                FileHandler fh = null;
                
                if (fileHandlersByPath.containsKey(logPath))
                {
                    fh = (fileHandlersByPath.get(logPath));
                }
                else
                {
                    fh = new FileHandler(logPath, false);
                    fh.setFormatter(new customSingleLineLogFormatter()); // The formatter itself is out of scope for review
                    fileHandlersByPath.put(logPath, fh);
                }
                
                logger.addHandler(fh);
            }           
            catch (SecurityException | IOException e)
            {
                e.printStackTrace();
            }
            
            return logger;
        }
    }
}

Exemplo de uso:

Para classes utilitárias (por exemplo, classes de acesso de banco de dados estático), estou apenas passando o logger para cada método que o usa, porque ele pode ser usado a partir de vários pacotes e deve registrar como se fosse parte da classe de chamada:

package com.name.root.util.database
public class StringUtils
{
    // just an example
    public static long parseStringToEpoch(String s, Logger logger)
    {
        logger.entering("parseStringToEpoch"); // should turn up in the calling class's log file
    }
}

mas para todas as outras classes, cada classe tem sua própria instância final estática do Logger que é instanciada junto com a classe, chamando o método getConfiguredLogger:

package com.name.root.router.base
public abstract class BaseRouter
{
    private static final Logger logger = LogProvider.getConfiguredLogger(BaseRouter.class, "C:\\com\\Router.log");`
// etc, including static methods that log
}

package com.name.root.router.impl
public class ChildRouter
{
    private static final Logger logger = LogProvider.getConfiguredLogger(ChildRouter.class, "C:\\com\\Router.log");`
// etc, including main and static methods that log

   // sample usage of utilities methods
   private static final long testEpoch = StringUtils.parseStringToEpoch("1234567",logger);
}

Respostas

1 Bobby Oct 29 2020 at 19:16
public class LogProvider

Essa classe provavelmente deve ser finale ter um privateconstrutor, para deixar claro que é um utilitário estático.


    private static final HashMap<String,FileHandler> fileHandlersByPath = new HashMap<>();
    private static final HashSet<String> alreadyProvidedPackages = new HashSet<>();

Sempre tente usar a classe ou interface comum mais baixa ao declarar variáveis, neste caso Mape Set.


Não tenho certeza de seus nomes, normalmente esperaria static finalvariáveis ​​para constantes, portanto nomeadas com UPPER_SNAKE_CASE.


public class LogProvider

A classe não fornece logs, ela fornece loggers, por isso deve ser nomeada LoggerProvider.

LoggerFactoryPorém, mais comumente é o nome para essas classes.


public static Logger getConfiguredLogger(Class<?> callingClass, String logPathIfNotAlreadySet)

Eu retiraria o "Configurado" do nome, pois não há como obter um registrador "não configurado" e também não importa para o usuário da API se ela está configurada ou não. Você pode até escapar tendo apenas um getmétodo, como este:

Logger LOGGER = LoggerFactory.get(SomeClass.class);

String fqClassName = callingClass.getCanonicalName();

O nome da variável está incorreto, é o nome canônico, não o nome totalmente qualificado. Eles podem ou não ser os mesmos.


String packageName = fqClassName;

Este também está incorreto, não é o nome do pacote (recuperado por meio Class.getPackageName()), mas é o nome canônico.


if (fqClassName.startsWith(rootPackageName))

Essa verificação está incorreta, também se aplica a com.name.rootbutdifferentpackage.sub.Class.


            catch (SecurityException | IOException e)
            {
                e.printStackTrace();
            }

Você deve usar um logger para registrar isso ... mas, na verdade, você provavelmente deseja falhar, porque, neste caso, você não pode mais fornecer recursos de registro, o que pode prejudicar a operação do aplicativo.

Sim, não registrar informações é um problema sério em ambientes corporativos, em aplicativos de desktop de forma alguma, mas se você tiver um datacenter com 10k instâncias em execução e 5k delas não registram, você tem um problema.


Pelo que posso ver, sua classe não é segura para thread, o que pode levar à falha ou que um FileHandlerseja criado duas vezes para o mesmo arquivo, possivelmente corrompendo os dados de log.

Você precisa de um conceito de quando sincronizar o acesso às informações armazenadas. A maneira mais fácil é usar uma lista sincronizada usando um wrapper criado por Collections.synchronized*. No entanto, isso apenas eliminará possíveis exceções, pois ainda pode corromper o estado armazenado. Você precisará sincronizar em algo, um objeto de bloqueio para garantir que nunca crie o mesmo arquivo duas vezes.

// Requires to be synchronized to allow adding and checking at the same time.
private static Map<String,FileHandler> fileHandlersByPath = Collections.synchronizedMap(new HashMap<>());

private static Object insertionLockObject = new Object();

// ...

// Assuming that this method is thread-safe and only delivers the same
// instance once for the same parameter.
Logger logger = Logger.getLogger(packageName);

// First "cheap" check to see if it is set.
if (!alreadyProvidedPackages.contains(packageName)) {
    // If it is not, we must acquire the lock to insert it.
    synchronized(insertionLockObject) {
        // Second check, because another thread could have acquired
        // the lock before us, and already did all the set up.
        if (!alreadyProvidedPackages.contains(packageName)) {
            // Code goes here.
        }
    }
}

return logger;

Para classes utilitárias (por exemplo, classes de acesso de banco de dados estático), estou apenas passando o logger para cada método que o usa, porque ele pode ser usado a partir de vários pacotes e deve registrar como se fosse parte da classe de chamada:

Isso cheira mal. Mas eu vejo por que você está fazendo isso.

Não há uma maneira fácil de fazer o que você deseja aqui. Pode-se encontrar uma solução para analisar o StackTrace atual para recuperar a classe e o pacote de chamada, mas essa pode ser uma solução frágil sem pensar muito sobre ela.


private static final Logger logger = LogProvider.getConfiguredLogger(BaseRouter.class, "C:\\com\\Router.log");

Como disse antes, eu esperaria essa variável para UPPER_SNAKE_CASE.

Eu perdi isso antes, mas por que você está passando por todo o caminho? Seria muito melhor se você não passasse um caminho, mas, em vez disso, defina um LogProvidere crie um caminho / arquivo para isso com base no passado em classe. Isso seria muito menos frágil e determinístico.

ZoranJankov Oct 29 2020 at 03:46

Não tenho certeza do que você está tentando realizar aqui, mas você deve armazenar todos os seus recursos, ou seja, seus arquivos de log, em uma pasta de origem do recurso e em código, você pode acessar esses recursos com:

// the stream holding the file content
InputStream is = getClass().getClassLoader().getResourceAsStream("file.txt");

// for static access, uses the class name directly
InputStream is = JavaClassName.class.getClassLoader().getResourceAsStream("file.txt");