Log configuration controls how logs look (format), where they go (console, files), and how much is shown (levels). Spring Boot offers two ways: simple application.properties settings, or a full Logback file named logback-spring.xml. The XML file gives complete control over appenders, rolling rules, filters, and different settings per profile (dev or prod). It is used when you need rolling files, a separate error log, or different logging behavior in development and production.
Features
- Custom log format with date, level, thread, class, and message
- Multiple destinations at once: console, application file, error-only file
- Rolling by size and date, with compressed archives
- Automatic cleanup using
maxHistoryandtotalSizeCap - Different logging per profile using
<springProfile> - Values from
application.propertiesused inside the XML with<springProperty> - Level change at runtime through
LoggingSystem, with no restart
Hands-on Experiment: Creating a spring boot application to understand how logging format, destination, and levels can be customized.
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: logconfig
- Name: logconfig
- Package name: com.example.logconfig
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver.
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>student-log-config-demo</artifactId>
<version>1.0.0</version>
<name>student-log-config-demo</name>
<description>Log Configuration - rolling logs with Logback (logback-spring.xml)</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- spring-boot-starter-web already includes SLF4J + Logback -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 4: application.properties
spring.application.name=student-log-config-demo
# ---------- MySQL ----------
# Create the database first: CREATE DATABASE student_log_config_db;
spring.datasource.url=jdbc:mysql://localhost:3306/student_log_config_db
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
server.port=8082
# ---------- Which logging profile to use: dev or prod ----------
# logback-spring.xml reads this. Switch with: mvn spring-boot:run -Dspring-boot.run.profiles=prod
spring.profiles.active=dev
# ---------- Custom value read by logback-spring.xml (<springProperty>) ----------
# Folder where all log files are written
app.log.dir=logsCode language: PHP (php)
Step 5: Create Entity Class
package com.example.logconfig.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
private String course;
public Student() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getCourse() { return course; }
public void setCourse(String course) { this.course = course; }
}
Step 6: Create below Repository Interface
package com.example.logconfig.repository;
import com.example.logconfig.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
boolean existsByEmail(String email);
}
Step 7: Create below Service class
package com.example.logconfig.service;
import com.example.logconfig.entity.Student;
import com.example.logconfig.repository.StudentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@Service
public class StudentService {
private static final Logger log = LoggerFactory.getLogger(StudentService.class);
private final StudentRepository studentRepository;
private final LoggingSystem loggingSystem; // lets us change levels while the app is running
@Value("${app.log.dir:logs}")
private String logDir;
public StudentService(StudentRepository studentRepository, LoggingSystem loggingSystem) {
this.studentRepository = studentRepository;
this.loggingSystem = loggingSystem;
}
// ---------------- Student operations ----------------
public Student create(Student student) {
log.info("Creating student with email={}", student.getEmail());
if (studentRepository.existsByEmail(student.getEmail())) {
log.warn("Create rejected - email already exists: {}", student.getEmail());
throw new IllegalArgumentException("Email already registered: " + student.getEmail());
}
try {
Student saved = studentRepository.save(student);
log.info("Student created successfully with id={}", saved.getId());
log.debug("Saved student details: name={}, course={}", saved.getName(), saved.getCourse());
return saved;
} catch (Exception ex) {
log.error("Failed to create student with email={}", student.getEmail(), ex);
throw ex;
}
}
public List<Student> getAll() {
log.info("Fetching all students");
List<Student> students = studentRepository.findAll();
log.debug("Total students found: {}", students.size());
return students;
}
public Student getById(Long id) {
log.info("Fetching student with id={}", id);
return studentRepository.findById(id).orElseThrow(() -> {
log.warn("Student not found with id={}", id);
return new IllegalArgumentException("Student not found: " + id);
});
}
// ---------------- Log configuration helpers (for the experiment) ----------------
// Writes many lines quickly so the 10KB limit is crossed and the file rolls over
public int generateLogs(int count) {
log.info("Generating {} sample log lines", count);
for (int i = 1; i <= count; i++) {
log.info("Sample log line {} - Student processing batch in progress", i);
}
return count;
}
// Writes an ERROR with a stack trace so it appears in student-error.log
public void simulateError() {
try {
Object value = null;
value.toString(); // deliberate NullPointerException
} catch (Exception ex) {
log.error("Simulated failure while processing student batch", ex);
}
}
// Changes the level of one logger at runtime (no restart needed)
public void changeLevel(String loggerName, String level) {
LogLevel logLevel = LogLevel.valueOf(level.toUpperCase());
loggingSystem.setLogLevel(loggerName, logLevel);
log.warn("Log level of {} changed to {} at runtime", loggerName, logLevel);
}
// Reads the last N lines of student-app.log ("app") or student-error.log ("error")
public List<String> tailLog(String type, int lines) throws IOException {
String fileName = "error".equalsIgnoreCase(type) ? "student-error.log" : "student-app.log";
Path path = Paths.get(logDir, fileName);
if (!Files.exists(path)) {
throw new IllegalArgumentException("Log file not found: " + path.toAbsolutePath());
}
List<String> all = Files.readAllLines(path, StandardCharsets.UTF_8);
int from = Math.max(0, all.size() - lines);
return new ArrayList<>(all.subList(from, all.size()));
}
// Lists current files and rolled-over archive files with their sizes
public List<String> listLogFiles() throws IOException {
List<String> result = new ArrayList<>();
Path dir = Paths.get(logDir);
if (Files.exists(dir)) {
try (Stream<Path> files = Files.list(dir)) {
for (Path p : (Iterable<Path>) files.filter(Files::isRegularFile).sorted()::iterator) {
result.add(p.getFileName() + " (" + Files.size(p) + " bytes)");
}
}
}
Path archive = dir.resolve("archive");
if (Files.exists(archive)) {
try (Stream<Path> files = Files.list(archive)) {
for (Path p : (Iterable<Path>) files.sorted()::iterator) {
result.add("archive/" + p.getFileName() + " (" + Files.size(p) + " bytes)");
}
}
}
return result;
}
}
Step 8: Create Controller class
package com.example.logconfig.controller;
import com.example.logconfig.entity.Student;
import com.example.logconfig.service.StudentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/students")
public class StudentController {
private static final Logger log = LoggerFactory.getLogger(StudentController.class);
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@PostMapping
public ResponseEntity<?> create(@RequestBody Student student) {
log.info("POST /students called");
try {
return ResponseEntity.status(HttpStatus.CREATED).body(studentService.create(student));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}
@GetMapping
public List<Student> getAll() {
log.info("GET /students called");
return studentService.getAll();
}
@GetMapping("/{id}")
public ResponseEntity<?> getById(@PathVariable Long id) {
log.info("GET /students/{} called", id);
try {
return ResponseEntity.ok(studentService.getById(id));
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));
}
}
// ---------- Log configuration helpers (for the experiment - learning only) ----------
@PostMapping("/logs/generate")
public Map<String, Object> generate(@RequestBody Map<String, Integer> request) {
int count = request.getOrDefault("count", 100);
log.info("POST /students/logs/generate called with count={}", count);
return Map.of("generated", studentService.generateLogs(count));
}
@PostMapping("/logs/simulate-error")
public Map<String, String> simulateError() {
log.info("POST /students/logs/simulate-error called");
studentService.simulateError();
return Map.of("message", "ERROR written - check logs/student-error.log");
}
@PostMapping("/logs/level")
public ResponseEntity<?> changeLevel(@RequestBody Map<String, String> request) {
String loggerName = request.get("logger");
String level = request.get("level");
if (loggerName == null || level == null) {
return ResponseEntity.badRequest().body(Map.of("error", "'logger' and 'level' are required"));
}
try {
studentService.changeLevel(loggerName, level);
return ResponseEntity.ok(Map.of("logger", loggerName, "level", level.toUpperCase()));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Invalid level. Use TRACE, DEBUG, INFO, WARN, ERROR, OFF"));
}
}
@GetMapping("/logs/files")
public ResponseEntity<?> files() {
try {
return ResponseEntity.ok(studentService.listLogFiles());
} catch (Exception ex) {
log.error("Could not list log files", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", "Could not list log files"));
}
}
@GetMapping("/logs/tail")
public ResponseEntity<?> tail(@RequestParam(defaultValue = "app") String type,
@RequestParam(defaultValue = "20") int lines) {
try {
return ResponseEntity.ok(studentService.tailLog(type, lines));
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));
} catch (Exception ex) {
log.error("Could not read log file", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", "Could not read log file"));
}
}
}
Step 9: Main Application
package com.example.logconfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LogConfigApplication {
public static void main(String[] args) {
SpringApplication.run(LogConfigApplication.class, args);
}
}
Step 10: Run the LogConfigApplication


