javaandroidlogginglog4j2android-assets

Cannot resolve log4j configuration in Android


I am trying to implement log4j in android, using v2.24.1 dependencies in gradle:

implementation 'org.apache.logging.log4j:log4j-api:2.24.1'
implementation 'org.apache.logging.log4j:log4j-core:2.24.1'

I have a log4j2.properties configuration in src/main/assets directory:

# Root logger configuration
status = error
name = PropertiesConfig

# Appenders
appender.console.type = Console
appender.console.name = Console
appender.console.target = SYSTEM_OUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5p- %m%n

# Root logger level and appenders
rootLogger.level = INFO
rootLogger.appenderRefs = stdout
rootLogger.appenderRef.stdout.ref = Console

logger.com.example.log4japi.MainActivity.name = com.example.log4japi.MainActivity
logger.com.example.log4japi.MainActivity.level = DEBUG
logger.com.example.log4japi.MainActivity.additivity = false
logger.com.example.log4japi.MainActivity.appenderRefs = stdout
logger.com.example.log4japi.MainActivity.appenderRef.stdout.ref = Console

In my main activity, I am attempting to use the logger for com.example.log4japi.MainActivity defined above. The activity contains a text view to display the current Log Level, a button to open a dialog and change the Log Level, and additional buttons to print log messages at each level:

package com.example.log4japi;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.apache.logging.log4j.core.config.Configurator;
import java.util.Arrays;

public class MainActivity extends AppCompatActivity {

    private int logLevelIdx;

    Logger log = LogManager.getLogger(MainActivity.class);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //TextView displaying Log Level
        TextView logLevelTxt = findViewById(R.id.logLevelTxt);
        logLevelTxt.setText(log.getLevel().name());
        
        //Change log level
        Button setLogLevelBtn = findViewById(R.id.setLogLevelBtn);
        setLogLevelBtn.setOnClickListener(v -> {
            AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
            alertDialog.setTitle("Set log level");

            String[] logLevels = new String[]{"OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", "ALL"};
            logLevelIdx = Arrays.asList(logLevels).indexOf(log.getLevel().name());

            alertDialog.setSingleChoiceItems(logLevels, logLevelIdx, (dialog, which) -> logLevelIdx = which);
            alertDialog.setPositiveButton("Select", (dialog, which) -> {
                Configurator.setLevel(log.getName(), Level.valueOf(Arrays.asList(logLevels).get(logLevelIdx)));
                logLevelTxt.setText(log.getLevel().name());
                dialog.dismiss();
            });
            alertDialog.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss());
            alertDialog.create();
            alertDialog.show();
        });
        
        
        //Log messages
        Button fatalLogBtn = findViewById(R.id.fatalLogBtn);
        fatalLogBtn.setOnClickListener(v -> log.fatal("Current log level is: " + log.getLevel().name()));

        Button errorLogBtn = findViewById(R.id.errorLogBtn);
        errorLogBtn.setOnClickListener(v -> log.error("Current log level is: " + log.getLevel().name()));

        Button warnLogBtn = findViewById(R.id.warnLogBtn);
        warnLogBtn.setOnClickListener(v -> log.warn("Current log level is: " + log.getLevel().name()));

        Button infoLogBtn = findViewById(R.id.infoLogBtn);
        infoLogBtn.setOnClickListener(v -> log.info("Current log level is: " + log.getLevel().name()));

        Button debugLogBtn = findViewById(R.id.debugLogBtn);
        debugLogBtn.setOnClickListener(v -> log.debug("Current log level is: " + log.getLevel().name()));

        Button traceLogBtn = findViewById(R.id.traceLogBtn);
        traceLogBtn.setOnClickListener(v -> log.trace("Current log level is: " + log.getLevel().name()));
    }
}

For some reason, the configuration is not being read or initialised. This results in a default log level of ERROR for any loggers I attempt to use. I also cannot change the level of these loggers using Configurator.setLevel (unless I use the root logger).

I have tried to manually import the configuration from the properties file as follows:

// Manually read in log4j2.properties
LoggerContext context = (LoggerContext) LogManager.getContext(false);
try {
    InputStream inputStream = getAssets().open("log4j2.properties");
    ConfigurationSource source = new ConfigurationSource(inputStream);
    context.start(ConfigurationFactory.getInstance().getConfiguration(context, source));
} catch (IOException e) {
    e.printStackTrace();
}

but this results in the following stack trace:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.log4japi, PID: 10971
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.log4japi/com.example.log4japi.MainActivity}: java.lang.UnsupportedOperationException: This parser does not support specification "Unknown" version "0.0"
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.UnsupportedOperationException: This parser does not support specification "Unknown" version "0.0"
        at javax.xml.parsers.DocumentBuilderFactory.setXIncludeAware(DocumentBuilderFactory.java:475)
        at org.apache.logging.log4j.core.config.xml.XmlConfiguration.enableXInclude(XmlConfiguration.java:226)
        at org.apache.logging.log4j.core.config.xml.XmlConfiguration.newDocumentBuilder(XmlConfiguration.java:186)
        at org.apache.logging.log4j.core.config.xml.XmlConfiguration.<init>(XmlConfiguration.java:87)
        at org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory.getConfiguration(XmlConfigurationFactory.java:46)
        at org.apache.logging.log4j.core.config.ConfigurationFactory$Factory.getConfiguration(ConfigurationFactory.java:565)
        at com.example.log4japi.MainActivity.onCreate(MainActivity.java:40)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7656) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 

How do I resolve the issues reading the config?

Update 1:

After moving the log4j2.properties from src/main/assets to src/main/resource, the configuration is now being read.

However, when I try to construct a logger in the MainActivity:

Logger log = LogManager.getLogger(MainActivity.class);

the application crashes with the error below:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.log4japi, PID: 29055
    java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.log4japi/com.example.log4japi.MainActivity}: org.apache.logging.log4j.core.config.ConfigurationException: No name attribute provided for Logger com
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3365)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: org.apache.logging.log4j.core.config.ConfigurationException: No name attribute provided for Logger com
        at org.apache.logging.log4j.core.config.properties.PropertiesConfigurationBuilder.createLogger(PropertiesConfigurationBuilder.java:258)
        at org.apache.logging.log4j.core.config.properties.PropertiesConfigurationBuilder.build(PropertiesConfigurationBuilder.java:175)
        at org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory.getConfiguration(PropertiesConfigurationFactory.java:56)
        at org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory.getConfiguration(PropertiesConfigurationFactory.java:34)
        at org.apache.logging.log4j.core.config.ConfigurationFactory$Factory.getConfiguration(ConfigurationFactory.java:544)
        at org.apache.logging.log4j.core.config.ConfigurationFactory$Factory.getConfiguration(ConfigurationFactory.java:463)
        at org.apache.logging.log4j.core.config.ConfigurationFactory.getConfiguration(ConfigurationFactory.java:321)
        at org.apache.logging.log4j.core.LoggerContext.reconfigure(LoggerContext.java:720)
        at org.apache.logging.log4j.core.LoggerContext.reconfigure(LoggerContext.java:750)
        at org.apache.logging.log4j.core.LoggerContext.start(LoggerContext.java:263)
        at org.apache.logging.log4j.core.impl.Log4jContextFactory.getContext(Log4jContextFactory.java:153)
        at org.apache.logging.log4j.core.impl.Log4jContextFactory.getContext(Log4jContextFactory.java:46)
        at org.apache.logging.log4j.LogManager.getContext(LogManager.java:138)
        at org.apache.logging.log4j.LogManager.getLogger(LogManager.java:555)
        at com.example.log4japi.MainActivity.<init>(MainActivity.java:29)
        at java.lang.Class.newInstance(Native Method)
        at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
        at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:45)
        at android.app.Instrumentation.newActivity(Instrumentation.java:1253)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3353)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7656) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 

If I define a logger for "com" in the configuration, by adding below to log4j2.properties:

logger.com.name = com
logger.com.level = DEBUG
logger.com.additivity = false
logger.com.appenderRefs = stdout
logger.com.appenderRef.stdout.ref = Console

The application will run, but any loggers I attempt to use (tested using both root logger and com.example.log4japi.MainActivity) will default to using these settings, and I am unable to dynmically update using Configurator.setLevel


Solution

  • For some reason, the configuration is not being read or initialised.

    In your project the configuration file is in src/main/assets, which is not deployed at the root of the classpath, but in an assets subfolder. To deploy the file at the root of the classpath, put it in src/main/resources.

    I have tried to manually import the configuration from the properties file as follows:

    // Manually read in log4j2.properties
    LoggerContext context = (LoggerContext) LogManager.getContext(false);
    try {
        InputStream inputStream = getAssets().open("log4j2.properties");
        ConfigurationSource source = new ConfigurationSource(inputStream);
        context.start(ConfigurationFactory.getInstance().getConfiguration(context, source));
    } catch (IOException e) {
       e.printStackTrace();
    }
    

    The ConfigurationSource class is similar to an uploaded file: you need to specify both the contents and the type of the uploaded file. Log4j Core deduces the type of configuration file from the file extension of ConfigurationSource.getLocation(). If this method return null or an unknown file extension the default XML configuration format is assumed. This triggers the LOG4J2-3531 bug, due to the lack of XInclude capabilities in the Android XML parser.

    To workaround this problem, you need to specify the name of the configuration file, when you create the ConfigurationSource:

    private ConfigurationSource getConfiguration(String asset) throws IOException {
        // Create a Source to specify the name of the configuration file
        Source source = new Source(URI.create("log4j2.properties"));
        // Create a ConfigurationSource with the contents of the configuration file
        byte[] buffer = new byte[1024];
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (InputStream inputStream = getAssets().open(asset)) {
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
        }
        return new ConfigurationSource(source, baos.toByteArray(), 0L);
    }