Trang chủHướng dẫnLog4j2 Java: Hướng dẫn cấu hình, Log level & Appender qua ví dụ

Log4j2 Java: Hướng dẫn cấu hình, Log level & Appender qua ví dụ

CyStack blog 31 phút để đọc
CyStack blog27/10/2025
Locker Avatar

Bao Tran

Web Developer

Locker logo social
Reading Time: 31 minutes

Chào mừng bạn đến với Hướng dẫn cấu hình Log4j2. Nếu bạn hỏi một dev có kinh nghiệm rằng điều gì gây khó chịu nhất khi làm việc với một ứng dụng, có lẽ câu trả lời sẽ liên quan đến việc ghi log. Nếu ứng dụng không có hệ thống log phù hợp, việc bảo trì sẽ trở thành ác mộng.

Phần lớn các ứng dụng đều trải qua các giai đoạn như kiểm thử trong quá trình phát triển (development testing), kiểm thử đơn vị (unit testing), kiểm thử tích hợp (integration testing). Nhưng khi vận hành thực tế (production), bạn sẽ luôn gặp những tình huống và ngoại lệ không lường trước. Cách duy nhất để tìm ra nguyên nhân trong từng trường hợp cụ thể là phân tích log.

Nhiều framework có cung cấp cơ chế ghi log mặc định, nhưng tốt nhất vẫn là sử dụng những công cụ ghi log theo tiêu chuẩn của ngành. Log4j là một trong những framework ghi log phổ biến nhất. Log4j 2 là phiên bản kế nhiệm, với hiệu năng và tính năng vượt trội so với Log4j cũ.

Nội dung chính của hướng dẫn Log4j2

Trong bài hướng dẫn này, bạn sẽ học cách bắt đầu với Apache Log4j2. Chúng ta cũng sẽ khám phá các khái niệm như kiến trúc Log4j2, cấu hình, các cấp độ log, appender, filter và nhiều nội dung khác.

  1. Tổng quan về Log4j2
  2. Kiến trúc Log4j2
  3. Cấu hình Log4j2
  4. Các cấp độ trong Log4j2
  5. Log4j2 Lookups
  6. Appender trong Log4j2
  7. Log4j2 Filters
  8. Log4j2 Layouts
  9. Nên sử dụng cấp độ Log4j2 nào?
  10. Tóm tắt hướng dẫn Log4j2

Tổng quan về Log4j2

Việc sử dụng API ghi log trong ứng dụng không phải là điều xa xỉ, mà là điều bắt buộc. Log4j là một thư viện mã nguồn mở được phát hành và cấp phép bởi Apache Software Foundation.

Bạn có thể debug một ứng dụng bằng Eclipse Debugger hoặc các công cụ khác, nhưng điều đó không đủ và không khả thi trong môi trường vận hành thực tế. Cơ chế ghi log mang lại nhiều lợi ích mà việc debug thông thường không thể đáp ứng.

So sánh: Gỡ lỗi (Debugging) vs Ghi log (Logging)

Tiêu chí / Hoạt động Debugging Logging
Cần can thiệp của con người Có. Cần thao tác thủ công. Không. Hệ thống ghi lại tự động.
Tích hợp với bộ nhớ lưu trữ Không. Không thể tích hợp với bộ nhớ bền vững. Có thể. Lưu vào file, cơ sở dữ liệu, NoSQL, v.v.
Dùng cho mục đích kiểm toán Không khả thi. Có thể dùng nếu được cấu hình hợp lý.
Phù hợp với luồng xử lý phức tạp Không đủ, dễ bị lạc trong luồng logic. Đủ khả năng theo dõi chi tiết mọi bước xử lý.
Năng suất Thấp Cao, dễ theo dõi, hỗ trợ bảo trì.

Như bạn thấy ở bảng trên, sử dụng cơ chế ghi log sẽ hiệu quả hơn nhiều, đồng thời giảm chi phí bảo trì.

Apache Log4j là công cụ đi đầu trong việc ghi log cho các ứng dụng Java. Và bạn nên ưu tiên sử dụng framework này trong các dự án của mình.

Kiến trúc Log4j2

Trước khi đi vào phần ví dụ cụ thể về Log4j2, chúng ta hãy cùng tìm hiểu kiến trúc của Log4j2. Hình minh họa (không có ở đây) cho thấy các lớp quan trọng trong API của Log4j2.

image.png

Dưới đây là phần giải thích chi tiết về kiến trúc đó:

  • Ứng dụng sẽ yêu cầu một Logger từ LogManager với một tên cụ thể.
  • LogManager sẽ xác định LoggerContext phù hợp, sau đó lấy hoặc tạo Logger từ LoggerContext đó.
  • Nếu Logger chưa tồn tại, nó sẽ được tạo mới và gắn với một LoggerConfig, dựa trên ba lựa chọn sau:
  1. Logger sẽ được gắn với LoggerConfig có tên trùng khớp.

Ví dụ: getLogger(App.class) sẽ tạo ra Logger với tên là "com.journaldev.App". Tên LoggerConfig sẽ là tên đầy đủ của lớp (Fully Qualified Class Name).

  1. Logger sẽ được gắn với LoggerConfig có package cha tương ứng.

Ví dụ: getLogger("com.journaldev") sẽ được gắn với LoggerConfig tên "com.journaldev" nếu nó tồn tại.

  1. Logger sẽ được gắn với Root LoggerConfig nếu: Không có file cấu hình hoặc không có logger nào được định nghĩa khớp với tên đã cung cấp.
  • Các đối tượng LoggerConfig được tạo ra từ phần khai báo logger trong file cấu hình.
  • LoggerConfig chịu trách nhiệm xử lý các LogEvents và ủy quyền việc xuất log cho các Appender đã định nghĩa Log4j2 Appenders.
  • Root logger là trường hợp ngoại lệ vì nó luôn tồn tại, nằm ở đỉnh của hệ thống logger.
  • Bạn có thể lấy root logger bằng một trong hai cách sau:
Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
Logger logger = LogManager.getRootLogger();
  • Lưu ý: Tên logger trong Log4j2 có phân biệt chữ hoa – chữ thường.
  • Ngoại trừ root loger, chúng ta có thể lấy tất cả các logger khác thông qua việc truyền tên của chúng vào phương thức LogManager.getLogger() .
  • LoggerContext là trung tâm của hệ thống ghi log, vì một ứng dụng có thể có nhiều LoggerContext khác nhau. Mỗi LoggerContext cần phải được gán một cấu hình đang hoạt động.
  • Cấu hình Log4j2 chứa tất cả các thành phần của hệ thống ghi log, bao gồm: LoggerConfig , Appender , Filter và các thành phần khác.
  • Việc gọi LogManager.getLogger() với cùng một tên logger sẽ luôn trả về cùng một thể hiện logger đã được tạo trước đó, đảm bảo việc sử dụng tài nguyên hiệu quả và thống nhất.
  • Cấu hình hệ thống ghi log thường được thực hiện ngay khi ứng dụng khởi động. Việc cấu hình có thể được thực hiện: bằng mã lệnh (programmatically), hoặc thông qua file cấu hình.

Mỗi logger đều được gắn với một đối tượng LoggerConfig. Tập hợp các LoggerConfig tạo nên một cấu trúc phân cấp (Hierarchy) của các logger – còn gọi là Logger Hierarchy. Cấu trúc này có mối quan hệ cha-con, trong đó thành phần cao nhất luôn là Root Logger.

Nếu Log4j2 không tìm thấy file cấu hình, nó sẽ chỉ sử dụng Root Logger để ghi log, với mức log mặc định là ERROR. Trong trường hợp này, bạn sẽ nhận được cảnh báo như hình minh họa sau: Error StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.

image.png

Bảng sau minh họa mối quan hệ cha – con trong Cấu trúc Phân cấp Logger (Logger Hierarchy):

LoggerConfig (là) Root com com.journaldev com.journaldev.logging
Root X Con hậu duệ hậu duệ
com Cha X Con hậu duệ
com.journaldev Tổ tiên Cha X Con
com.journaldev.logging Tổ tiên Tổ tiên Cha X

Giải thích quan hệ Cha – Con theo bảng trên:

  • Root là cha của com.
  • Root là tổ tiên (ancestor) của com.journaldev.
  • Root cũng là tổ tiên của com.journaldev.logging.
  • com là con của Root.
  • com là cha của com.journaldev.
  • com là tổ tiên của com.journaldev.logging.
  • com.journaldev.logging là con của com.journaldev, và tương tự cho các mức khác.

Một thể hiện của LoggerConfig được gọi là tổ tiên của một LoggerConfig khác nếu tên của nó, kèm theo dấu chấm, là tiền tố của tên LoggerConfig hậu duệ.

Một LoggerConfig được gọi là cha của một LoggerConfig khác nếu không có tên xen giữa nào tồn tại giữa chúng trong cấu trúc phân cấp.

Cấu hình Log4j2

Có nhiều cách để cấu hình Log4j2 trong ứng dụng của bạn:

  1. Sử dụng tệp cấu hình được viết bằng XML, JSON, YAML hoặc file .properties.
  2. Cấu hình bằng mã lập trình, thông qua việc tạo một configuration factoryconfiguration implementation.
  3. Cấu hình bằng API, thông qua các phương thức được cung cấp bởi giao diện cấu hình.
  4. Cấu hình nội bộ, thông qua việc gọi các phương thức trực tiếp trên lớp Logger nội bộ.

Trong hướng dẫn này, chúng ta sẽ chủ yếu tập trung vào cách cấu hình bằng tệp. Tuy nhiên, việc biết được cách cấu hình bằng mã cũng rất hữu ích, đặc biệt khi bạn cần áp dụng một chiến lược logging cụ thể cho một logger nhất định.

Trước tiên, chúng ta sẽ xem xét trường hợp không cung cấp tệp cấu hình. Khi đó, Log4j2 sẽ giả định rằng có một biến hệ thống tên là log4j.configurationFile trỏ đến vị trí của tệp cấu hình.

package com.journaldev;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class App
{
    public static void main( String[] args ) {
    	Logger logger = LogManager.getRootLogger();
    	logger.trace("Configuration File Defined To Be :: "+System.getProperty("log4j.configurationFile"));
    }
}

Ví dụ đơn giản về file cấu hình Log4j2 (configuration.xml):

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="Console">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="trace">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

Giải thích đoạn mã:

  • Ứng dụng tham chiếu đến Root logger bằng cách gọi phương thức LogManager.getRootLogger().
  • Khi logger được tham chiếu, hệ thống Log4j bắt đầu khởi động.
  • Log4j sẽ kiểm tra biến hệ thống log4j.configurationFile để xác định tệp cấu hình cần sử dụng.
  • Tệp cấu hình Log4j có thể được viết bằng định dạng XML, JSON, hoặc YAML.
  • Bạn có thể thiết lập biến hệ thống log4j.configurationFile thông qua gọi lệnh trong Java: System.setPropertiess("log4j.configurationFile","FILE_PATH") hoặc thông qua JVM parameter khi chạy chương trình.

    Lưu ý: tiền tố file: cần có khi chỉ định đường dẫn tuyệt đối đến file cấu hình.

    image.png

Trường hợp không định nghĩa biến hệ thống nào, thứ tự ưu tiên tìm kiếm tệp cấu hình sẽ như sau:

  1. Property ConfigurationFactory sẽ tìm log4j2-test.properties trong classpath.
  2. YAML ConfigurationFactory sẽ tìm log4j2-test.yaml hoặc log4j2-test.yml trong classpath.
  3. JSON ConfigurationFactory sẽ tìm log4j2-test.jsn hoặc log4j2-test.json trong classpath.
  4. XML ConfigurationFactory sẽ tìm log4j2-test.xml trong classpath.
  5. Property ConfigurationFactory sẽ tìm log4j2.properties trong classpath.
  6. YAML ConfigurationFactory sẽ tìm log4j2.yaml hoặc log4j2.yml trong classpath.
  7. JSON ConfigurationFactory sẽ tìm log4j2.json hoặc log4j2.jsn trong classpath.
  8. XML ConfigurationFactory sẽ tìm log4j2.xml trong classpath.

Nếu không tìm thấy bất kỳ tệp cấu hình nào, cấu hình mặc định (DefaultConfiguration) sẽ được áp dụng và dẫn đến một số hành vi mặc định sau:

  • Root Logger sẽ được sử dụng.
  • Mức log của Root Logger sẽ được thiết lập là ERROR.
  • Root Logger sẽ gửi thông điệp log ra console.
  • PatternLayout mặc định sẽ là:

    %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n

Sử dụng tệp cấu hình log4j2 giúp đơn giản hóa quá trình cấu hình. Tuy nhiên, ta cũng có thể cấu hình bằng lập trình. Dưới đây là ví dụ cấu hình thông qua ConfigurationFactory:

package com.journaldev;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
public class App
{
    public static void main( String[] args ) throws FileNotFoundException, IOException {
 
    	// Get instance of configuration factory; your options are default ConfigurationFactory, XMLConfigurationFactory,
    	// 	YamlConfigurationFactory & JsonConfigurationFactory
    	ConfigurationFactory factory =  XmlConfigurationFactory.getInstance();
 
    	// Locate the source of this configuration, this located file is dummy file contains just an empty configuration Tag
    	ConfigurationSource configurationSource = new ConfigurationSource(new FileInputStream(new File("C:/dummyConfiguration.xml")));
 
    	// Get a reference from configuration
    	Configuration configuration = factory.getConfiguration(configurationSource);
 
    	// Create default console appender
    	ConsoleAppender appender = ConsoleAppender.createDefaultAppenderForLayout(PatternLayout.createDefaultLayout());
 
    	// Add console appender into configuration
    	configuration.addAppender(appender);
 
    	// Create loggerConfig
    	LoggerConfig loggerConfig = new LoggerConfig("com",Level.FATAL,false);
 
    	// Add appender
    	loggerConfig.addAppender(appender,null,null);
 
    	// Add logger and associate it with loggerConfig instance
    	configuration.addLogger("com", loggerConfig);
 
    	// Get context instance
    	LoggerContext context = new LoggerContext("JournalDevLoggerContext");
 
    	// Start logging system
    	context.start(configuration);
 
    	// Get a reference for logger
    	Logger logger = context.getLogger("com");
 
    	// LogEvent of DEBUG message
    	logger.log(Level.FATAL, "Logger Name :: "+logger.getName()+" :: Passed Message ::");
 
    	// LogEvent of Error message for Logger configured as FATAL
    	logger.log(Level.ERROR, "Logger Name :: "+logger.getName()+" :: Not Passed Message ::");
 
    	// LogEvent of ERROR message that would be handled by Root
    	logger.getParent().log(Level.ERROR, "Root Logger :: Passed Message As Root Is Configured For ERROR Level messages");
    }
}

Giải thích đoạn mã:

  • Bạn có thể sử dụng bất kỳ ConfigurationFactory nào được cung cấp bởi Log4j2, hoặc dùng factory mặc định. Ở đây ta dùng XmlConfigurationFactory.
  • Factory sẽ cung cấp một thể hiện của Configuration, dựa trên tệp cấu hình được truyền vào.
  • Thể hiện Configuration sẽ được sử dụng cùng với LoggerContext để khởi chạy hệ thống logging.
  • Một ConsoleAppender mặc định đã được thêm vào cấu hình, với layout mặc định để in log ra console.
  • LoggerConfig được tạo ra với tên “com”, mức FATAL, không filter. Appender đã tạo được gán cho LoggerConfig này.
  • LoggerConfig được thêm vào configuration.
  • Tạo một LoggerContext mới với tên tùy chọn.
  • Gán cấu hình cho LoggerContext và khởi động context.
  • Từ LoggerContext, lấy ra logger với tên “com”.
  • Logger bắn ra ba sự kiện:
    • FATAL: được ghi lại (được phép theo cấu hình).
    • ERROR: không được ghi lại bởi logger “com” vì mức FATAL không chấp nhận ERROR.
    • Một log ERROR khác được ghi lại bởi Root logger (vì Root mặc định nhận mức ERROR).

    Cấu hình tương tự cũng có thể được thực hiện thông qua YAML, JSON hoặc tệp properties.

    Tuy nhiên, cấu hình tệp properties trong Log4j2 khác với Log4j phiên bản cũ, vì vậy hãy đảm bảo rằng bạn không dùng tệp cấu hình của Log4j (1.x) cho Log4j2, nếu không bạn sẽ gặp lỗi sau:

    ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
    

    Khi chạy đoạn mã trên, bạn sẽ nhận được kết quả sau:

    
    Logger Name :: com :: Passed Message ::
    00:01:27.705 [main] ERROR - Root Logger:: Passed Message As Root Is Configured For ERROR Level messages
    
    • Dòng đầu tiên là log từ logger có tên com, được ghi ở mức FATAL.
    • Dòng thứ hai là log từ Root Logger, ghi ở mức ERROR.

    Lưu ý rằng thông điệp ERROR từ logger com không được in ra, vì logger này chỉ được cấu hình để ghi các thông điệp ở mức FATAL

    Các cấp độ trong Log4j2

    Như bạn thấy trong các ví dụ mã trước, mỗi lần định nghĩa một LoggerConfig, bạn đều phải chỉ định logging level (mức ghi log).

    Mặc định, Log4j2 có cơ chế “additive”, tức là các logger cấp cha sẽ cùng được sử dụng khi một logger con được kích hoạt.

    Hình ảnh minh họa sẽ giúp bạn hình dung rõ hơn:

    image.png

Giải thích các điểm chính

  • Như đã đề cập trước đó, mỗi logger được liên kết với một thể hiện của LoggerConfig. Cấu hình của LoggerConfig được định nghĩa trong tệp cấu hình của Log4j2.
  • Mức độ ghi log (Level) được xác định ở cấp độ của LoggerConfig.
  • Bạn có thể tạo logger theo:
    • Tên đầy đủ của logger (ví dụ: com.journaldev.logging),
    • Gói cha của nó (ví dụ: com.journaldev, com),
    • Hoặc sử dụng trực tiếp Root Logger (là nút cao nhất trong cây LoggerConfig).
  • Khi bạn lấy logger com.journaldev và phát một logEvent, thì:
    • LoggerConfig tương ứng với com.journaldev sẽ ghi log.
    • Log event này sẽ tiếp tục được truyền lên logger cha (com) và Root Logger, bỏ qua mức độ log của các logger cha.
    • Nghĩa là: nếu Root Logger được thiết lập ghi log ở mức thấp hơn, nó vẫn sẽ ghi lại thông điệp.
  • Tương tự, nếu bạn sử dụng logger com, thông điệp log:
    • Sẽ được ghi bởi LoggerConfig của com.
    • Sau đó được truyền lên Root Logger để ghi lại tiếp.
  • Tất cả logger con (ví dụ com.journaldev.logging) đều hoạt động theo cơ chế additive nếu không được cấu hình ngược lại.
  • Bạn có thể ngăn chặn việc truyền log lên Logger cha bằng cách:
  1. Thiết lập thuộc tính additivity="false" trong LoggerConfig.
  2. Hoặc sử dụng Filter để chặn log theo điều kiện.
  • Nếu mức level của LoggerConfig cao hơn mức log của sự kiện, logger sẽ bỏ qua log event đó.

Bây giờ, chúng ta sẽ cùng xem ví dụ minh họa về cơ chế additivity:

import net.NetApp;
import net.journaldev.NetJournalDevApp;
import com.ComApp;
import com.journaldev.ComJournalDevApp;
public class Main {
	public static void main(String [] args){
		new ComApp();
		new ComJournalDevApp();
		new NetApp();
		new NetJournalDevApp();
	}
}
package com.journaldev;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class ComJournalDevApp {
	public ComJournalDevApp(){
		Logger logger = LogManager.getLogger(ComJournalDevApp.class);
		logger.trace("COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::");
	}
}
package net;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class NetApp {
	public NetApp(){
		Logger logger = LogManager.getLogger(NetApp.class);
		logger.error("NET :: LEVEL :: NetApp ERROR Message ::");
	}
}
package net.journaldev;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class NetJournalDevApp {
	public NetJournalDevApp(){
		Logger logger = LogManager.getLogger(NetJournalDevApp.class);
		logger.error("NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::");
	}
}

Tệp cấu hình log4j2 trông như sau:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="Console">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="Console"/>
    </Root>
  	<logger name="com" level="TRACE">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="com.journaldev" level="TRACE">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net.journaldev" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  </Loggers>
</Configuration>

Kết quả xuất ra khi chạy chương trình:

10:34:47.168 [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
10:34:47.168 [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
10:34:47.170 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
10:34:47.170 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
10:34:47.170 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
10:34:47.171 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
10:34:47.171 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
10:34:47.171 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
10:34:47.171 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
10:34:47.171 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::

Giải thích đoạn mã:

  • Tệp cấu hình log4j2.xml định nghĩa 5 thực thể LoggerConfig: Root, com, com.journaldev, net, và net.journaldev.
  • Mức log (level) được cấu hình như sau:
    • Root: ERROR (mặc định)
    • com, com.journaldev: TRACE
    • net, net.journaldev: ERROR
  • Logger của các class ComAppComJournalDevApp in ra log nhiều lần (2–3 lần) vì:
    • Log được ghi bởi logger chính (ví dụ: com.journaldev)
    • Sau đó được truyền lên logger cha (com) và Root), do thuộc tính additivity=true theo mặc định.
  • Tương tự, các logger trong package net cũng ghi lại log nhiều lần vì lý do tương tự.

Cấp độ log và Logger Hierarchy cùng phối hợp xác định log nào được ghi.

Nếu bạn thay đổi mức log của LoggerConfig dành cho package com từ TRACE thành INFO mà vẫn giữ nguyên toàn bộ chương trình, thì bạn sẽ nhận được kết quả như sau:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="Console">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="Console"/>
    </Root>
  	<logger name="com" level="INFO">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="com.journaldev" level="TRACE">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net.journaldev" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  </Loggers>
</Configuration>

Kết quả khi chạy chương trình:

11:08:10.305 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
11:08:10.305 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
11:08:10.305 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
11:08:10.307 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
11:08:10.307 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
11:08:10.308 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
11:08:10.308 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
11:08:10.308 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::

Chắc chắn bạn sẽ nhận thấy rằng log event của ComApp đã bị bỏ qua, và lý do là bởi mức log (level) được khai báo trong LoggerConfig của package com là INFO (400), thấp hơn so với mức của log event là TRACE (600). Do đó, thông điệp log từ ComApp sẽ không được hiển thị nữa.

Để hiển thị được log này, bạn cần chỉnh sửa mức LoggerConfig của com thành TRACE (600) hoặc ALL (Integer.MAX_VALUE).

Để một log event được hiển thị, LoggerConfig.level phải lớn hơn hoặc bằng log event.level.

Bảng dưới đây liệt kê các mức log của Log4j2 và trọng số tương ứng:

Nhưng điều gì sẽ xảy ra nếu chúng ta loại bỏ LoggerConfig của com.journaldev khỏi cấu hình và thay vào đó thêm một LoggerConfig mới cho com.journaldev.logging, khiến file cấu hình trông như sau:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="Console">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="Console"/>
    </Root>
  	<logger name="com" level="TRACE">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="com.journaldev.logging" level="TRACE">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net.journaldev" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  </Loggers>
</Configuration>

Bạn có thể thấy hình minh họa dưới đây sẽ giúp bạn dễ hiểu hơn điều gì đã xảy ra trong cấu hình Log4j2 ở trên.

image.png

Một số điểm làm rõ cho sơ đồ phía trên và cách nó ảnh hưởng đến hành vi của các sự kiện ghi log:

  • Khi một sự kiện log được phát ra bởi một Logger có tên là com.journaldev.logging, LoggerConfig tương ứng với tên đó (tức là com.journaldev.logging) sẽ được dùng để xử lý và in ra thông báo.
  • Vì thuộc tính additive của LoggerConfig com.journaldev.logging được đặt mặc định là true, sự kiện log sẽ được truyền lên Logger cha – trong trường hợp này là com.journaldev.
  • Tuy nhiên, vì LoggerConfig com.journaldev không được định nghĩa trong file cấu hình, nên sẽ không có hành động nào xảy ra tại đây, và sự kiện log sẽ tiếp tục được truyền lên com, rồi tiếp tục lên Root.
  • comRoot sẽ nhận sự kiện log và in ra thông báo, bất kể cấp độ (level) của sự kiện đó là gì.

Kết quả đầu ra sẽ như sau:

14:08:37.634 [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
14:08:37.634 [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
14:08:37.636 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
14:08:37.636 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
14:08:37.637 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:08:37.637 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:08:37.637 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:08:37.638 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
14:08:37.638 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
14:08:37.640 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
14:08:37.640 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
14:08:37.640 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::

Những điểm cần lưu ý:

  • Sự kiện log tại package com xuất hiện 2 lần: một lần từ LoggerConfig com, và một lần từ Root.
  • Sự kiện log tại com.journaldev cũng xuất hiện 2 lần: một lần từ com và một lần từ Root. Trước đây từng xuất hiện 3 lần, nhưng giờ LoggerConfig của com.journaldev đã bị loại bỏ, nên không còn xử lý log tại mức này nữa — sự kiện được truyền tiếp lên comRoot.
  • Sự kiện log tại com.journaldev.logging xuất hiện 3 lần: một lần từ LoggerConfig com.journaldev.logging, một lần từ com, và một lần từ Root. Theo nguyên tắc Logger Hierarchy, đáng lý nó sẽ xuất hiện 4 lần, nhưng do thiếu LoggerConfig com.journaldev, nên chỉ còn 3.

Trong trường hợp bạn đã định nghĩa một LoggerConfig cho com.journaldev mà không chỉ định cấp độ (Level), thì nó sẽ kế thừa cấp độ từ Logger cha của nó.

Điều gì sẽ xảy ra nếu bạn định nghĩa LoggerConfig cho com.journaldev trong file cấu hình nhưng không chỉ định level?

May mắn thay, khái niệm Logger Hierarchy sẽ giúp bạn. Trong trường hợp này, com.journaldev sẽ kế thừa cấp độ log từ LoggerConfig cha của nó. Dưới đây là một ví dụ về file cấu hình cùng bảng chỉ ra level tương ứng cho từng Logger:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="Console">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="Console"/>
    </Root>
  	<logger name="com" level="TRACE">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="com.journaldev">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="com.journaldev.logging" level="TRACE">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net.journaldev" level="ERROR">
  		<AppenderRef ref="Console"/>
  	</logger>
  </Loggers>
</Configuration>
Tên Logger LoggerConfig được gán Mức LoggerConfig Mức Logger
Root Root ERROR ERROR
com com TRACE TRACE
com.journaldev com TRACE TRACE
com.journaldev.logging com.journaldev.logging TRACE TRACE
  • Gói com.journaldev.logging đã được gán với một LoggerConfig có mức log là TRACE.
  • Gói com.journaldev đã được gán với một LoggerConfig nhưng không chỉ định mức log, nên nó sẽ kế thừa mức log từ LoggerConfig cha, và trong trường hợp này giá trị đó là TRACE từ gói com.
  • Gói com đã được gán với một LoggerConfig có mức log là TRACE.
  • Mặc định, LoggerConfig gốc (Root) có mức log là ERROR.
  • Trong trường hợp không khai báo gói com, LoggerConfig của com.journaldev sẽ kế thừa mức log từ Root.

Dưới đây là kết quả khi thực thi chương trình trong trường hợp com.journaldev kế thừa mức log từ com:

14:41:37.419 [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
14:41:37.419 [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
14:41:37.421 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
14:41:37.421 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
14:41:37.421 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
14:41:37.422 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:41:37.422 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:41:37.422 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:41:37.422 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:41:37.423 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
14:41:37.423 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
14:41:37.423 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
14:41:37.423 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
14:41:37.423 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::

Và đây là kết quả nếu bạn xóa khai báo LoggerConfig cho gói com:

14:43:28.809 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:43:28.809 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:43:28.809 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
14:43:28.811 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
14:43:28.811 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
14:43:28.812 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
14:43:28.812 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::
14:43:28.812 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::

Bạn có thể nhận thấy rằng không có thông điệp nào được log cho gói comcom.journaldev. Dưới đây là lý do:

  • Việc xóa LoggerConfig liên kết với gói com khiến mọi sự kiện log ở gói này bị bỏ qua.
  • Vì LoggerConfig cho com không được định nghĩa, LoggerConfig của com.journaldev sẽ cố gắng kế thừa mức log từ LoggerConfig cha. Nhưng vì com không có, Logger Hierarchy sẽ đi lên đến Logger gốc (Root). Root có mức log là ERROR (200), trong khi sự kiện log ở com.journaldev có mức TRACE (600) — như trong lớp ComJournalDevApp. Theo công thức đã đề cập, LoggerConfig chỉ xử lý sự kiện log khi mức LoggerConfig lớn hơn hoặc bằng mức sự kiện log. Trường hợp này không đúng, do đó thông điệp log không được hiển thị.

Cuối cùng nhưng không kém phần quan trọng, bảng dưới đây cho thấy tất cả các trường hợp log có thể xảy ra khi sử dụng hệ thống Logging:

LoggerConfig Level OFF(0) FATAL(100) ERROR(200) WARN(300) INFO(400) DEBUG(500) TRACE(600) ALL(MAX)
Event Level
OFF(0) YES NO NO NO NO NO NO NO
FATAL(100) NO YES YES YES YES YES YES YES
ERROR(200) NO NO YES YES YES YES YES YES
WARN(300) NO NO NO YES YES YES YES YES
INFO(400) NO NO NO NO YES YES YES YES
DEBUG(500) NO NO NO NO NO YES YES YES
TRACE(600) NO NO NO NO NO NO YES YES
ALL(MAX) NO NO NO NO NO NO NO YES

Không có phương thức trực tiếp nào để ghi log ở mức OFF/ALL. Thông thường, để ghi log ở hai mức này, bạn có thể sử dụng logger.log(Level.OFF, “Msg”) or logger.log(LEVEL.ALL,“Msg”).

Phương thức log() sẽ chịu trách nhiệm xử lý sự kiện log dựa trên công thức đã nêu trước đó:

Nếu mức LoggerConfig lớn hơn hoặc bằng mức của sự kiện log, thì sự kiện đó sẽ được chấp nhận để xử lý tiếp theo.

Việc sự kiện log được chấp nhận để xử lý tiếp theo là rất quan trọng, vì bạn vẫn có thể ngăn sự kiện đó được xử lý, ngay cả khi nó đã được chấp nhận, bằng cách sử dụng Log4j2 Filters.

Bạn cũng có thể thiết lập thuộc tính additivity="false" để ngăn việc lan truyền sự kiện log đến các Logger cha. Dưới đây là ví dụ giống như trước, nhưng lần này có thêm thuộc tính additivity, để bạn có thể thấy được sự khác biệt.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="Console"/>
    </Root>
  	<logger name="com" level="TRACE" additivity="false">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="com.journaldev" additivity="false">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="com.journaldev.logging" level="TRACE" additivity="false">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net" level="ERROR" additivity="false">
  		<AppenderRef ref="Console"/>
  	</logger>
  	<logger name="net.journaldev" level="ERROR" additivity="false">
  		<AppenderRef ref="Console"/>
  	</logger>
  </Loggers>
</Configuration>

Và đây là kết quả sau khi thực thi:

17:55:30.558 [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
17:55:30.560 [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
17:55:30.561 [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
17:55:30.561 [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
17:55:30.562 [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::

Bạn có thể thấy rằng không có sự lan truyền sự kiện log lên các Logger cha do thuộc tính additivity="false" đã được đặt.

Log4j2 Lookups

Về lý tưởng, bạn có thể định nghĩa Lookups như là cách để truyền giá trị vào file cấu hình Log4j2. Log4j2 cung cấp một tập hợp các loại Lookups mà bạn có thể sử dụng độc lập để lấy giá trị từ các ngữ cảnh khác nhau:

  • Context Map Lookup
  • Date Lookup
  • Environment Lookup
  • Java Lookup
  • JNDI Lookup
  • JVM Input Argument Lookup (JMX)
  • Main Arguments Lookup
  • Map Lookup
  • Structured Data Lookup
  • System Properties Lookup
  • Web Lookup

Bạn có thể tham khảo tài liệu chính thức của Log4j2 để hiểu rõ hơn từng loại Lookup, nhưng hãy cùng xem một ví dụ đơn giản dưới đây để hiểu cơ bản về Lookup trong Log4j2.

Ví dụ về Environment Lookup là cách bạn truyền một biến môi trường (qua Linux /etc/profile, biến môi trường hệ thống Windows, hoặc script khởi động ứng dụng). Hầu hết chúng ta đều biết rằng có thể định nghĩa các biến môi trường để ứng dụng sử dụng.

  1. Thiết lập biến môi trường bằng tiện ích môi trường của Windows:
  • Nhấp chuột phải vào biểu tượng “This PC” (hoặc “Computer”) và chọn “Properties”.

    Bảng điều khiển hệ thống (Control Panel Home) sẽ được hiển thị.

  • Nhấn vào “Advanced system settings” (Cài đặt hệ thống nâng cao), sau đó mở cửa sổ Environment Variables (Biến môi trường).
  • Trong phần System Variables (Biến hệ thống), tạo một biến mới có tên là JournalDevVar và gán giá trị là JournalDev.
  • Cập nhật PatternLayout trong tập tin log4j2.xml của bạn để sử dụng biến môi trường vừa tạo.

image.png

  1. Thiết lập biến môi trường bằng cách sử dụng script khởi động (Startup script):
  • Thay vì sử dụg các script mặc định thông thường, bạn có thể sử dụng tiện ích chạy script của Eclipse IDE. Vào menu Run, chọn Run Configurations…
  • Điều hướng đến tab Environment. Tại đây, thêm biến môi trường mới.

image.png

Hãy cùng xem tệp log4j2.xml đã được chỉnh sửa và cách sử dụng biến môi trường trong đó:

<Console name="Console" target="SYSTEM_OUT">
    <PatternLayout pattern="%d{HH:mm:ss.SSS} $${env:JournalDevVar} $${env:JournalDevSecondVar} [%t] %-5level %logger{36} - %msg%n"/>
</Console>

Kết quả khi chạy chương trình sẽ trông như sau:

23:57:02.511 JournalDev www.journaldev.com [main] TRACE com.ComApp - COM :: LEVEL :: ComApp TRACE Message ::
23:57:02.517 JournalDev www.journaldev.com [main] TRACE com.journaldev.ComJournalDevApp - COM :: JournalDev :: LEVEL :: ComJournalDevApp TRACE Message ::
23:57:02.520 JournalDev www.journaldev.com [main] TRACE com.journaldev.logging.ComJounralDevLoggingApp - COM :: JournalDev :: LOGGING :: LEVEL :: ComJounralDevLoggingApp TRACE Message ::
23:57:02.523 JournalDev www.journaldev.com [main] ERROR net.NetApp - NET :: LEVEL :: NetApp ERROR Message ::
23:57:02.527 JournalDev www.journaldev.com [main] ERROR net.journaldev.NetJournalDevApp - NET :: JournalDev :: LEVEL :: NetJournalDevApp ERROR Message ::

Tuy nhiên, bạn có thể gặp một vấn đề nhỏ. Đó là cơ chế cache biến môi trường trong Eclipse. Lý tưởng là bạn có thể kiểm tra các biến môi trường mà Eclipse đang sử dụng tại: Run – Run Configuration – Environment tab – Click Select button

image.png

Vì vậy, bạn có thể cảm thấy bối rối khi đã định nghĩa biến môi trường nhưng ứng dụng lại không nhận ra nó. Ngay cả khi bạn khởi động lại Eclipse, vấn đề vẫn chưa được giải quyết. Để khắc phục, bạn phải chạy Eclipse bằng lệnh eclipse.exe -clean tại thư mục cài đặt Eclipse.

Kiểm tra biến môi trường đã được định nghĩa đúng cách

Để đảm bảo rằng biến môi trường đã được định nghĩa đúng và hệ thống của bạn có thể tìm thấy nó, bạn có thể sử dụng plugin tương ứng mà Log4j2 API cung cấp.

Cụ thể, bạn có thể tạo một instance của EnvironmentLookup và yêu cầu nó tra cứu biến bạn đã định nghĩa. Nếu biến tồn tại, bạn sẽ thấy giá trị của nó được trả về dễ dàng:

EnvironmentLookup lookup = new EnvironmentLookup();
LogManager.getRootLogger().error(lookup.lookup("JournalDevSecondVar"));

Các Appender trong Log4j2

Ở phần trước, bạn đã thấy cách sử dụng Lookups để truyền biến môi trường vào tệp cấu hình. Tuy nhiên, đôi khi bạn không muốn ghi log ra console mà muốn chuyển hướng thông điệp sang file hoặc cơ sở dữ liệu để đảm bảo các bản ghi được lưu giữ lâu dài.

Log4j2 đã cung cấp rất nhiều Appender để ++hỗ trợ điều này. Bạn có thể tham khảo tài liệu chính thức của Log4j2 để biết thêm chi tiết, nhưng dưới đây là danh sách tóm tắt các loại Appender có sẵn:

  1. ConsoleAppender
  2. AsyncAppender
  3. FailoverAppender
  4. FileAppender
  5. FlumeAppender
  6. JDBCAppender
  7. JMSAppender
  8. JPAAppender
  9. MemoryMappedFileAppender
  10. NoSQLAppender
  11. OutputStreamAppender
  12. RandomAccessFileAppender
  13. RewriteAppender
  14. RollingFileAppender
  15. RollingRandomAccessFileAppender
  16. RoutingAppender
  17. SMTPAppender
  18. SocketAppender
  19. SyslogAppender

Trong số đó, những phương tiện ghi log phổ biến nhất là: Console, File, Database. Vì file thường lưu lại phần tin nhắn của bạn, database thường được dùng cho mục đích audit. Chính vì vậy, phần tiếp theo sẽ tập trung vào cách sử dụng JDBCAppender một cách hiệu quả để lưu log vào cơ sở dữ liệu.

JDBCAppender

Mục tiêu chính của JDBCAppender là ghi các sự kiện log vào bảng quan hệ (relational table) thông qua các kết nối JDBC. Bài viết này không đi sâu vào việc tối ưu hóa connection pool, vì đó không phải là mục tiêu chính. Tuy nhiên, bạn sẽ có một ví dụ hoàn chỉnh giúp ghi log vào cơ sở dữ liệu một cách thực tế.

Trước khi tiếp tục, hãy cùng xem các tham số cần thiết để cấu hình JDBCAppender cùng với mô tả của từng tham số:

Tên Tham Số Kiểu Dữ Liệu Mô Tả
Name String Bắt buộc. Tên của Appender.
ignoreExceptions boolean Mặc định là true, nghĩa là các exception phát sinh sẽ được ghi log rồi bỏ qua. Nếu đặt là false, exception sẽ được truyền ngược về caller.
filter Filter Bộ lọc được sử dụng để quyết định liệu sự kiện log có được xử lý bởi Appender này hay không.
bufferSize int Mặc định là 0, tức không buffer sự kiện log. Nếu lớn hơn 0, Appender sẽ buffer các sự kiện log và flush chúng khi đạt đến giới hạn.
connectionSource ConnectionSource Bắt buộc. Nguồn kết nối dùng để truy xuất kết nối cơ sở dữ liệu.
tableName String Bắt buộc. Tên bảng nơi các sự kiện log sẽ được lưu.
columnConfigs ColumnConfig[] Bắt buộc. Dùng để định nghĩa thông tin chi tiết cho từng cột và cách dữ liệu được lưu lên mỗi cột. Được cấu hình bằng nhiều phần tử <Column>.
Tên Tham Số Kiểu Dữ Liệu Mô Tả
jndiName String Bắt buộc. Tên đầy đủ của JNDI mà javax.sql.DataSource được liên kết tới.
Tên Tham Số Kiểu Dữ Liệu Mô Tả
class String Bắt buộc. Tên đầy đủ của class có chứa phương thức tĩnh để tạo kết nối JDBC.
method String Bắt buộc. Tên của phương thức tĩnh đó.
Tên Tham Số Kiểu Dữ Liệu Mô Tả
name String Bắt buộc. Tên của cột trong cơ sở dữ liệu.
pattern String Cho phép định nghĩa bất kỳ pattern hợp lệ nào để định dạng sự kiện log.
literal String Giá trị cố định (literal) cho cột, ví dụ: SEQ.NEXTVAL.
isEventTimestamp boolean Chỉ định cột này có lưu timestamp của sự kiện log hay không.
isUnicode boolean Dùng để lưu dữ liệu Unicode. Tham khảo thêm trong tài liệu Log4j2.
isClob boolean Dùng để lưu Character Large Object (CLOB). Tham khảo thêm trong tài liệu Log4j2.

Vì bạn bắt buộc phải sử dụng JNDI, ví dụ của chúng ta sẽ cấu hình nguồn dữ liệu kết nối (DataSource) cho cơ sở dữ liệu Oracle và Apache Tomcat 7.

  • Nếu bạn chưa cài đặt Oracle Database trong môi trường làm việc, rất khuyến khích bạn cài đặt. Nếu bạn chưa quen với Oracle, mình đề xuất bạn cài đặt phiên bản Oracle Express Edition để sử dụng.
  • Cài đặt Apache Tomcat 7 vào môi trường của bạn.
  • Tạo một dự án Maven WebApp trong Eclipse.

image.png

  • Hãy đảm bảo rằng dự án của bạn đã được tạo thành công, và nếu bạn phát hiện bất kỳ lỗi nào trong file pom.xml, hãy chắc chắn rằng bạn đã sửa chúng đúng cách.
  • Thêm các dependency cho Log4j2:
<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-api</artifactId>
   <version>2.2</version>
</dependency>
<dependency>
   <groupId>org.apache.logging.log4j</groupId>
   <artifactId>log4j-core</artifactId>
   <version>2.2</version>
</dependency>
  • Cấu hình ngữ cảnh (context) của bạn để khai báo nguồn dữ liệu MySQL:

Theo tài liệu chính thức của Apache, file cấu hình này phải được đặt trong thư mục META-INF của ứng dụng web (Web Application).

image.png

<Context path="/JournalDevWebLogging"
	privileged="true" antiResourceLocking="false" antiJARLocking="false">
	<Resource name="jdbc/JournalDevDB" auth="Container"
			factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
			type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000"
			username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
			url="jdbc:mysql://localhost:3306/journaldev" />
</Context>
  • Hãy cấu hình cơ sở dữ liệu của bạn và tạo bảng ghi log bằng câu lệnh sau:

image.png

CREATE TABLE `logging` (
  `EVENT_ID` int(11) NOT NULL AUTO_INCREMENT,
  `EVENT_DATE` datetime DEFAULT NULL,
  `LEVEL` varchar(45) DEFAULT NULL,
  `LOGGER` varchar(45) DEFAULT NULL,
  `MSG` varchar(45) DEFAULT NULL,
  `THROWABLE` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`EVENT_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  • Hãy cấu hình log4j2.xml của bạn để trông giống như:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
	<Appenders>
		<Console name="Console" target="SYSTEM_OUT">
			<PatternLayout
				pattern="%d{HH:mm:ss.SSS} $${env:JournalDevVar} $${env:JournalDevSecondVar} [%t] %-5level %logger{36} - %msg%n" />
		</Console>
		<JDBC name="databaseAppender" tableName="journaldev.logging">
			<DataSource jndiName="java:/comp/env/jdbc/JournalDevDB" />
			<Column name="EVENT_DATE" isEventTimestamp="true" />
			<Column name="LEVEL" pattern="%level" />
			<Column name="LOGGER" pattern="%logger" />
			<Column name="MSG" pattern="%message" />
			<Column name="THROWABLE" pattern="%ex{full}" />
		</JDBC>
	</Appenders>
	<Loggers>
		<Root level="ERROR">
			<AppenderRef ref="Console" />
		</Root>
		<logger name="com" level="TRACE" additivity="false">
			<AppenderRef ref="databaseAppender" />
		</logger>
		<logger name="com.journaldev" additivity="false">
			<AppenderRef ref="databaseAppender" />
		</logger>
	</Loggers>
</Configuration>

Hãy tạo một tài nguyên Web (Servlet) giúp bạn lấy được đối tượng logger và ghi một sự kiện log:

package com.journaldev;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class JournalDevServlet extends HttpServlet{
	private static final long serialVersionUID = 1L;
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
			Logger logger = LogManager.getLogger(JournalDevServlet.class);
			logger.trace("JournalDev Database Logging Message !");
	}
}

Bạn cũng có thể cấu hình một ServletContextListener để đảm bảo DataSource được khởi tạo đúng cách:

package com.journaldev;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.logging.log4j.LogManager;
public class JournalDevServletContextListener implements ServletContextListener{
	private InitialContext context = null;
	public void contextDestroyed(ServletContextEvent event) {
	}
	public void contextInitialized(ServletContextEvent event) {
		try {
			// Get initial context
			context = new InitialContext();
			// Get a reference for sub context env
			Context envContext = (Context)context.lookup("java:comp/env");
			// Get a reference for sub context jdbc and then locating the data source defined
			LogManager.getRootLogger().error(((Context)envContext.lookup("jdbc")).lookup("JournalDevDB"));
		} catch (NamingException e) {
			LogManager.getRootLogger().error(e);
		}
	}
}
  • Sau đó, hãy khai báo Servlet của bạn trong tệp web.xml.
  • Khi chạy ứng dụng và truy cập Servlet đã định nghĩa ở trên, bạn sẽ thấy các log như sau xuất hiện trong console hoặc file log (tùy cấu hình):
Mar 15, 2015 2:31:41 PM org.apache.catalina.core.AprLifecycleListener init
INFO: The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: C:\\Program Files\\Java\\jdk1.6.0_26\\bin;C:\\Windows\\Sun\\Java\\bin;C:\\Windows\\system32;C:\\Windows;C:\\Program Files\\Java\\jdk1.6.0_26\\jre\\bin;C:/Program Files/Java/jdk1.6.0_26/bin/../jre/bin/server;C:/Program Files/Java/jdk1.6.0_26/bin/../jre/bin;C:/Program Files/Java/jdk1.6.0_26/bin/../jre/lib/amd64;D:\\OracleWebCenter\\OracleWC\\Oracle11g\\app\\oracle\\product\\11.2.0\\server\\bin;;C:\\Program Files\\Common Files\\Microsoft Shared\\Windows Live;C:\\Program Files (x86)\\Common Files\\Microsoft Shared\\Windows Live;D:\\OracleDB\\app\\product\\11.2.0\\dbhome_1\\bin;org.C:\\Program Files (x86)\\Common Files\\NetSarang;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Program Files (x86)\\Intel\\OpenCL SDK\\2.0\\bin\\x86;C:\\Program Files (x86)\\Intel\\OpenCL SDK\\2.0\\bin\\x64;D:\\SpringRoo\\spring-roo-1.2.5.RELEASE\\bin;D:\\Ant\\apache-ant-1.9.2\\bin;C:\\Python27;C:\\Program Files\\Java\\jdk1.6.0_26\\bin;D:\\Maven\\apache-maven-3.2.1/bin;D:\\bower-master\\bin;C:\\Program Files (x86)\\Git\\cmd;C:\\Program Files\\nodejs\\;C:\\Program Files\\Microsoft Windows Performance Toolkit\\;D:\\Grails\\grails-2.4.0\\bin;D:\\Gradle\\gradle-2.0\\bin;C:\\Program Files (x86)\\Windows Live\\Shared;C:\\Program Files\\TortoiseSVN\\bin;D:\\Strawberry\\perl\\bin;D:\\Strawberry\\perl\\site\\bin;D:\\Strawberry\\c\\bin;C:\\Users\\mohammad.amr\\AppData\\Roaming\\npm;D:\\JournalDev\\eclipse;;.
Mar 15, 2015 2:31:41 PM org.apache.tomcat.util.digester.SetPropertiesRule begin
WARNING: [SetPropertiesRule]{Server/Service/Engine/Host/Context} Setting property 'source' to 'org.eclipse.jst.j2ee.server:JournalDevWebLogging' did not find a matching property.
Mar 15, 2015 2:31:41 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-bio-8080"]
Mar 15, 2015 2:31:41 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["ajp-bio-8009"]
Mar 15, 2015 2:31:41 PM org.apache.catalina.startup.Catalina load
INFO: Initialization processed in 1020 ms
Mar 15, 2015 2:31:41 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service Catalina
Mar 15, 2015 2:31:41 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet Engine: Apache Tomcat/7.0.35
14:31:43.847 [localhost-startStop-1] ERROR  - org.apache.tomcat.jdbc.pool.DataSource@10fd0a62{ConnectionPool[defaultAutoCommit=null; defaultReadOnly=null; defaultTransactionIsolation=-1; defaultCatalog=null; driverClassName=com.mysql.jdbc.Driver; maxActive=100; maxIdle=30; minIdle=10; initialSize=10; maxWait=10000; testOnBorrow=false; testOnReturn=false; timeBetweenEvictionRunsMillis=5000; numTestsPerEvictionRun=0; minEvictableIdleTimeMillis=60000; testWhileIdle=false; testOnConnect=false; password=root; url=jdbc:mysql://localhost:3306/journaldev; username=root; validationQuery=null; validatorClassName=null; validationInterval=30000; accessToUnderlyingConnectionAllowed=true; removeAbandoned=false; removeAbandonedTimeout=60; logAbandoned=false; connectionProperties=null; initSQL=null; jdbcInterceptors=null; jmxEnabled=true; fairQueue=true; useEquals=true; abandonWhenPercentageFull=0; maxAge=0; useLock=false; dataSource=null; dataSourceJNDI=null; suspectTimeout=0; alternateUsernameAllowed=false; commitOnReturn=false; rollbackOnReturn=false; useDisposableConnectionFacade=true; logValidationErrors=false; propagateInterruptState=false; }
Mar 15, 2015 2:31:43 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-bio-8080"]
Mar 15, 2015 2:31:43 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["ajp-bio-8009"]
Mar 15, 2015 2:31:43 PM org.apache.catalina.startup.Catalina start
INFO: Server startup in 1909 ms

image.png

Bộ lọc (Filter) trong Log4j2

Ngay cả khi đã có một LoggerConfig được xác định để xử lý một Log event, bạn vẫn có thể cấu hình để ngăn không cho Log event đó được chuyển tiếp đến các Appender phía sau. Việc này có thể thực hiện bằng cách sử dụng Filter của Log4j2.

Phần này không nhằm mục đích cung cấp một tài liệu chuyên sâu, toàn diện hoặc đào sâu vào từng loại bộ lọc có trong Log4j2 – vì mỗi bộ lọc có thể cần một bài hướng dẫn riêng biệt. Tuy nhiên, tại đây bạn sẽ được thấy cách sử dụng một bộ lọc đơn giản nhất để hiểu rõ khái niệm về Filter là gì và cách hoạt động của nó.


Một trong những bộ lọc đơn giản nhất bạn có thể sử dụng là BurstFilter – bộ lọc này cung cấp cơ chế kiểm soát tốc độ xử lý các LogEvent bằng cách âm thầm loại bỏ các log vượt quá giới hạn định sẵn.

Dưới đây là các tham số cần thiết để cấu hình BurstFilter:

Tên tham số Kiểu dữ liệu Mô tả
level String Mức log cần lọc (Level của thông điệp log).
rate float Số lượng log tối đa trung bình được phép xử lý mỗi giây.
maxBurst integer Tổng số log tối đa có thể xảy ra trước khi các log vượt ngưỡng bị loại bỏ. Mặc định là 10 lần giá trị rate.
onMatch String Hành động sẽ thực hiện khi log khớp với bộ lọc. Giá trị có thể là ACCEPT, DENY, hoặc NEUTRAL. Mặc định là NEUTRAL.
onMismatch String Hành động sẽ thực hiện khi log không khớp với bộ lọc. Giá trị tương tự như onMatch, mặc định là NEUTRAL.

Ví dụ: Đặt BurstFilter trong Appender ghi log vào cơ sở dữ liệu

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
	<Appenders>
		<Console name="Console" target="SYSTEM_OUT">
			<PatternLayout
				pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
		</Console>
		<JDBC name="databaseAppender" tableName="journaldev.logging">
			<DataSource jndiName="java:/comp/env/jdbc/JournalDevDB" />
			<BurstFilter level="TRACE" rate="20" maxBurst="2"/>
			<Column name="EVENT_DATE" isEventTimestamp="true" />
			<Column name="LEVEL" pattern="%level" />
			<Column name="LOGGER" pattern="%logger" />
			<Column name="MSG" pattern="%message" />
			<Column name="THROWABLE" pattern="%ex{full}" />
		</JDBC>
	</Appenders>
	<Loggers>
		<Root level="ERROR">
			<AppenderRef ref="Console" />
		</Root>
		<logger name="com" level="TRACE" additivity="false">
			<AppenderRef ref="databaseAppender" />
		</logger>
		<logger name="com.journaldev" additivity="false">
			<AppenderRef ref="databaseAppender" />
		</logger>
	</Loggers>
</Configuration>

Appender ghi vào cơ sở dữ liệu (Database Appender) có xét đến BurstFilter, trong khi Appender ghi ra console (Console Appender) thì không.

Việc sử dụng Console logger sẽ khiến tất cả các log event đều được ghi ra, nhưng Appender ghi vào cơ sở dữ liệu thì không như vậy, vì BurstFilter sẽ chặn một số log không cho tiếp tục được xử lý.

Việc từ chối các LogEvent này vẫn xảy ra ngay cả khi Logger đã được định cấu hình để xử lý những log đó. Điều này hoàn toàn hợp lý, như minh họa trong ví dụ JournalDevServlet dưới đây.

image.png

image.png

package com.journaldev;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class JournalDevServlet extends HttpServlet{
	private static final long serialVersionUID = 1L;
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
		Logger logger = LogManager.getLogger(JournalDevServlet.class);
		for(int i = 0 ; i < 1000 ; i++){
			logger.trace("Index :: "+i+" :: JournalDev Database Logging Message !");
			LogManager.getRootLogger().error("Index :: "+i+" :: JournalDev Database Logging Message !");
		}
	}
}

Mặc dù các LoggerConfig đó là ứng viên cho việc xử lý các Log Event được tạo ra ở đó, nhưng bộ lọc (Filter) đã ngăn không cho một số log được xử lý và ghi lại. Bạn có thể xem đây là một phần của khái niệm “Không gian ghi log” (Logging Space) để hiểu toàn diện hơn về cách thức hoạt động của hệ thống log.

Layouts trong Log4j2

Do các Appender khác nhau sẽ tiêu thụ các Log Event theo những cách khác nhau, Layout được tạo ra nhằm định dạng lại các Log Event sao cho phù hợp với mục đích sử dụng của Appender đó. Trong Log4j 1.x và cả API của Logback, việc chuyển đổi Layout sẽ đưa Log Event thành chuỗi ký tự (String). Tuy nhiên, trong Log4j2, việc chuyển đổi Layout được thực hiện theo một cách hoàn toàn mới: LogEvent được chuyển thành mảng byte (byte array). Việc sử dụng byte array buộc bạn phải cấu hình đúng bảng mã ký tự (Charset) để đảm bảo dữ liệu được chuyển đổi chính xác. Bạn nên truy cập trang chính thức của Apache Log4j2 để tìm hiểu thêm về các loại Layout mà Log4j2 hỗ trợ.

Trong phần này, chúng ta sẽ cùng tìm hiểu về Layout phổ biến nhất, được nhiều lập trình viên sử dụng – PatternLayout.

Log4j2 PatternLayout

PatternLayout là một kiểu định dạng linh hoạt, có thể cấu hình bằng cách sử dụng các chuỗi mẫu (pattern strings) nhằm định dạng các Log Event theo một kiểu cụ thể.

Việc định dạng này dựa vào khái niệm “chuỗi chuyển đổi” (conversion pattern).

Phần này sẽ giới thiệu đến bạn những tính năng quan trọng nhất mà PatternLayout cung cấp.

Khái niệm chuỗi chuyển đổi trong PatternLayout khá giống với cách mà printf trong ngôn ngữ C hoạt động.

Nói một cách đơn giản, chuỗi chuyển đổi gồm có:

  • Phần văn bản cố định (literal text)
  • Và các biểu thức điều khiển định dạng gọi là các chỉ thị chuyển đổi (conversion specifiers)

Hình minh họa dưới đây sẽ giúp bạn hình dung một chuỗi chuyển đổi gồm những thành phần nào:

image.png

Hình minh họa phía trên là một nỗ lực nhằm đơn giản hóa chuỗi chuyển đổi (Conversion Pattern), tuy nhiên chắc chắn sẽ tốt hơn nếu bạn tham khảo thêm tài liệu chính thức của Apache Log4j2 để hiểu rõ hơn về Layouts nói chung và Pattern Layout nói riêng.

Ngoài ra, bạn cũng có thể nhìn lại các Log Event ở phần trên và quan sát xem trong mỗi lần ghi log, chuỗi chuyển đổi nào đã được sử dụng để định dạng thông điệp.

Nên dùng cấp độ Log4j2 nào?

Một trong những câu hỏi quan trọng nhất mà bạn có thể đặt ra đó là: nên dùng cấp độ ghi log nào trong từng trường hợp cụ thể?

Trong môi trường phát triển (development), thông thường chúng ta sử dụng cấp độ DEBUG, trong khi đó ở môi trường vận hành (production), nên ưu tiên INFO hoặc WARN.

Bảng dưới đây sẽ giúp bạn định hướng rõ hơn:

Cấp độ Log Event Khi nào nên sử dụng
OFF Khi bạn không muốn ghi bất kỳ log nào
FATAL Khi xảy ra lỗi nghiêm trọng khiến ứng dụng không thể tiếp tục chạy
ERROR Khi xảy ra lỗi trong ứng dụng, có thể phục hồi
WARN Khi có sự kiện cảnh báo có thể dẫn tới lỗi
INFO Khi cần ghi lại thông tin hệ thống, các sự kiện đáng lưu ý
DEBUG Khi cần gỡ lỗi chung, theo dõi các luồng logic của chương trình
TRACE Khi cần ghi log chi tiết, rất tinh vi, để theo dõi luồng xử lý
ALL Khi muốn ghi lại tất cả mọi thứ

Tóm tắt hướng dẫn Log4j2

Log4j2 là phiên bản cải tiến mạnh mẽ của framework ghi log Apache Log4j. Nó mang đến nhiều tính năng mới và cải tiến hiệu suất đáng kể so với Log4j 1.x.

Mục tiêu của bài hướng dẫn này là giúp bạn nắm được tổng quan đầy đủ tại một nơi duy nhất, từ đó bạn có thể bắt đầu áp dụng Log4j2 vào ứng dụng của mình.

Do một số khái niệm không dễ để trình bày hết trong một lần duy nhất, nên bài viết đã cố gắng giải thích từng phần bằng lý thuyết kết hợp với ví dụ minh họa rõ ràng. Các phần như Appenders, Filters, Layouts và Lookups đều được đề cập theo hướng này.

Một vài điểm cần lưu ý để đảm bảo ứng dụng chạy tốt:

  • IDE Eclipse của bạn phải hỗ trợ Maven
  • Apache Tomcat của bạn phải chứa JAR mysql-connector trong thư mục lib
  • Bạn cần biết cách sử dụng Maven cơ bản

Vậy là bạn đã hoàn thành bài hướng dẫn Log4j2!

Hy vọng những điểm quan trọng nhất đã được trình bày đầy đủ để giúp bạn bắt đầu áp dụng Log4j2 vào dự án thực tế.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất