diff --git a/spring-boot-modules/spring-boot-logging-loki/README.md b/spring-boot-modules/spring-boot-logging-loki/README.md
new file mode 100644
index 0000000000..e320af31b4
--- /dev/null
+++ b/spring-boot-modules/spring-boot-logging-loki/README.md
@@ -0,0 +1 @@
+### Relevant Articles:
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-logging-loki/pom.xml b/spring-boot-modules/spring-boot-logging-loki/pom.xml
new file mode 100644
index 0000000000..5b2887562b
--- /dev/null
+++ b/spring-boot-modules/spring-boot-logging-loki/pom.xml
@@ -0,0 +1,35 @@
+
+
+ 4.0.0
+ spring-boot-logging-loki
+ 0.1-SNAPSHOT
+ loki
+
+
+ com.baeldung.spring-boot-modules
+ spring-boot-modules
+ 1.0.0-SNAPSHOT
+
+
+
+
+ com.github.loki4j
+ loki-logback-appender
+ ${loki-logback.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ 1.4.2
+
+
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-logging-loki/src/main/java/com/baeldung/loki/DemoService.java b/spring-boot-modules/spring-boot-logging-loki/src/main/java/com/baeldung/loki/DemoService.java
new file mode 100644
index 0000000000..89fdcc48e8
--- /dev/null
+++ b/spring-boot-modules/spring-boot-logging-loki/src/main/java/com/baeldung/loki/DemoService.java
@@ -0,0 +1,15 @@
+package com.baeldung.loki;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DemoService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DemoService.class);
+
+ public void log() {
+ LOG.info("DemoService.log invoked");
+ }
+}
diff --git a/spring-boot-modules/spring-boot-logging-loki/src/main/resources/logback-spring.xml b/spring-boot-modules/spring-boot-logging-loki/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000000..1ba53d7278
--- /dev/null
+++ b/spring-boot-modules/spring-boot-logging-loki/src/main/resources/logback-spring.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ http://localhost:3100/loki/api/v1/push
+
+
+
+
+
+ {
+ "level":"%level",
+ "class":"%logger{36}",
+ "thread":"%thread",
+ "message": "%message",
+ "requestId": "%X{X-Request-ID}"
+ }
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-logging-loki/src/test/java/com/baeldung/loki/DemoServiceLiveTest.java b/spring-boot-modules/spring-boot-logging-loki/src/test/java/com/baeldung/loki/DemoServiceLiveTest.java
new file mode 100644
index 0000000000..4c44888dca
--- /dev/null
+++ b/spring-boot-modules/spring-boot-logging-loki/src/test/java/com/baeldung/loki/DemoServiceLiveTest.java
@@ -0,0 +1,85 @@
+package com.baeldung.loki;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URI;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@SpringBootTest(classes = DemoService.class)
+public class DemoServiceLiveTest {
+
+ /**
+ * This test assumes that loki service is already up.
+ * For more details please check section #2 Running Loki and Grafana Service
+ * Which spin up Loki server using docker-compose
+ */
+ @Test
+ public void givenLokiContainerRunning_whenDemoServiceInvoked_thenLokiAppenderCollectLogs() throws JsonProcessingException, InterruptedException {
+ DemoService service = new DemoService();
+ service.log();
+
+ String baseUrl = "http://localhost:3100/loki/api/v1/query_range";
+ // Set up query parameters
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ String query = "{level=\"INFO\"} |= `DemoService.log invoked`";
+
+ // Get current time in UTC
+ LocalDateTime currentDateTime = LocalDateTime.now(ZoneOffset.UTC);
+ String currentTimeUtc = currentDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
+
+ LocalDateTime tenMinsAgo = currentDateTime.minusMinutes(10);
+ String startTimeUtc = tenMinsAgo.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
+
+ URI uri = UriComponentsBuilder.fromUriString(baseUrl)
+ .queryParam("query", query)
+ .queryParam("start", startTimeUtc)
+ .queryParam("end", currentTimeUtc)
+ .build()
+ .toUri();
+
+ RestTemplate restTemplate = new RestTemplate();
+ ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(headers), String.class);
+
+ List messages = new ArrayList<>();
+ ObjectMapper objectMapper = new ObjectMapper();
+ assertEquals(response.getStatusCode(), HttpStatus.OK);
+
+ String responseBody = response.getBody();
+ JsonNode jsonNode = objectMapper.readTree(responseBody);
+ JsonNode result = jsonNode.get("data")
+ .get("result")
+ .get(0)
+ .get("values");
+ result.iterator()
+ .forEachRemaining(e -> {
+ Iterator elements = e.elements();
+ elements.forEachRemaining(f -> messages.add(f.toString()));
+ });
+
+ String expected = "DemoService.log invoked";
+ assertThat(messages).anyMatch(e -> e.contains(expected));
+ }
+}