Java Logging dengan file per paket root

Oct 22 2020

Saya memiliki satu set kelas di beberapa paket, dan saya ingin setiap logging dari dalam satu paket "com.name.root.xxx" dan semua paket anaknya, untuk dicatat ke file yang berbeda. misalnya:

//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

dll.

Saya telah membuat kelas helper yang melacak jalur file log mana yang telah disiapkan dengan file handler, dan juga paket mana yang telah menyiapkan loggernya; dan menyediakan yang sesuai.

Saya tertarik dengan ulasan untuk efisiensi, dan jika ada cara yang lebih baik (lebih sederhana / lebih bersih / lebih mudah dimengerti) untuk melakukan ini:

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;
        }
    }
}

Contoh Penggunaan:

Untuk kelas utilitas (misalnya kelas akses basis data statis), saya hanya meneruskan pencatat ke setiap metode yang menggunakannya, karena dapat digunakan dari beberapa paket dan harus mencatat seolah-olah itu adalah bagian dari kelas pemanggil:

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
    }
}

tetapi untuk semua kelas lainnya, setiap kelas memiliki instance Logger akhir statisnya sendiri yang dibuat bersama dengan kelas, memanggil metode 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);
}

Jawaban

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

Kelas itu kemungkinan besar harus finaldan memiliki privatekonstruktor, untuk memperjelas bahwa itu adalah utilitas statis.


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

Selalu coba gunakan kelas atau antarmuka umum terendah saat mendeklarasikan variabel, dalam hal ini Mapdan Set.


Saya tidak yakin dengan nama Anda, biasanya saya mengharapkan static finalvariabel menjadi konstanta, oleh karena itu dinamai dengan UPPER_SNAKE_CASE.


public class LogProvider

Kelas tidak menyediakan log, itu menyediakan logger, jadi harus dinamai LoggerProvider.

Lebih umum adalah nama LoggerFactoryuntuk kelas-kelas seperti itu.


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

Saya akan menghapus "Configured" dari namanya, karena tidak ada cara untuk mendapatkan logger "tidak dikonfigurasi", dan juga tidak masalah bagi pengguna API apakah sudah dikonfigurasi atau tidak. Anda bahkan mungkin lolos hanya dengan memiliki getmetode, seperti ini:

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

String fqClassName = callingClass.getCanonicalName();

Nama variabel salah, itu nama kanonik, bukan nama yang sepenuhnya memenuhi syarat. Mereka mungkin sama atau mungkin tidak sama.


String packageName = fqClassName;

Yang ini juga salah, ini bukan nama paket (diambil melalui Class.getPackageName()) tetapi merupakan nama kanonik.


if (fqClassName.startsWith(rootPackageName))

Cek itu salah, itu juga berlaku untuk com.name.rootbutdifferentpackage.sub.Class.


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

Anda harus menggunakan logger untuk mencatat itu ... tetapi sebenarnya, Anda kemungkinan besar ingin gagal, karena dalam kasus ini Anda tidak dapat menyediakan kemampuan logging lagi, yang dapat membahayakan pengoperasian aplikasi.

Ya, tidak mencatat informasi adalah masalah serius di lingkungan perusahaan, di aplikasi desktop tidak sama sekali, tetapi jika Anda memiliki pusat data dengan 10k instans yang berjalan, dan 5k di antaranya tidak masuk, Anda memiliki masalah.


Sejauh yang saya lihat, kelas Anda tidak aman untuk thread, yang dapat menyebabkan kegagalan atau bahwa a FileHandlerdibuat dua kali untuk file yang sama, mungkin merusak data log.

Anda memerlukan konsep kapan harus menyinkronkan akses ke informasi yang disimpan. Cara termudah mungkin adalah menggunakan daftar tersinkronisasi dengan menggunakan pembungkus yang dibuat oleh Collections.synchronized*. Namun, itu hanya akan menghilangkan kemungkinan pengecualian, itu mungkin masih merusak status yang disimpan. Anda perlu melakukan sinkronisasi pada sesuatu, objek kunci untuk memastikan bahwa Anda tidak pernah membuat file yang sama dua kali.

// 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;

Untuk kelas utilitas (misalnya kelas akses basis data statis), saya hanya meneruskan pencatat ke setiap metode yang menggunakannya, karena dapat digunakan dari beberapa paket dan harus mencatat seolah-olah itu adalah bagian dari kelas pemanggil:

Baunya. Tapi saya mengerti mengapa Anda melakukannya.

Tidak ada cara mudah untuk melakukan apa yang Anda inginkan di sini, seseorang dapat menemukan solusi untuk mengurai StackTrace saat ini untuk mengambil kelas dan paket pemanggil, tetapi itu mungkin merupakan solusi yang rapuh tanpa terlalu memikirkannya.


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

Seperti yang dikatakan sebelumnya, saya mengharapkan variabel itu menjadi UPPER_SNAKE_CASE.

Saya melewatkan ini sebelumnya, tetapi mengapa Anda melewati seluruh jalan? Akan jauh lebih baik jika Anda tidak memberikan jalur sama sekali, tetapi menetapkan satu LogProviderjalur dan kemudian membuat jalur / file untuk ini berdasarkan yang diteruskan di kelas. Itu akan jauh lebih rapuh dan deterministik.

ZoranJankov Oct 29 2020 at 03:46

Saya tidak sepenuhnya yakin apa yang Anda coba capai di sini tetapi Anda harus menyimpan semua sumber daya Anda, yaitu file log Anda, dalam folder sumber sumber daya, dan dalam kode Anda dapat mengakses sumber daya tersebut dengan:

// 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");