제가 현재 진행하고 있는 강연 중 하나는 일반적인 관찰 가능성과 특히 분산 추적에 초점을 맞추며, OpenTelemetry 구현을 다룹니다. 데모에서는 Apache APISIX API Gateway, Spring Boot를 사용한 Kotlin 앱, Flask를 사용한 Python 앱, Axum을 사용한 Rust 앱으로 구성된 간단한 분산 시스템의 추적을 어떻게 볼 수 있는지 보여줍니다.
올해 초, FOSDEM의 관찰 가능성 룸에서 강연하고 참석했습니다. 그 중 하나의 강연에서는 Grafana 스택을 데모했습니다: Mimir는 지표용, Tempo는 추적용, Loki는 로그용입니다. 한 가지에서 다른 것으로 원활하게 이동할 수 있는 방법에 기뻐했습니다. 따라서 Grafana 스택에 종속되지 않고 OpenTelemetry를 통해 같은 결과를 달성하고 싶었습니다.
이 블로그 게시물에서는 로그와 Loki에 초점을 맞추고자 합니다.
Loki 기초 및 첫 번째 프로그램
Loki의 핵심은 로그 저장 엔진입니다:
Loki는 Prometheus에서 영감을 받아 만들어진 수평적으로 확장 가능하고 고가용성, 다중 테넌트 로그 집계 시스템입니다. 비용 효율적이고 운영이 쉬운 것을 목표로 설계되었으며, 로그의 내용을 인덱싱하는 대신 각 로그 스트림에 대한 레이블 세트를 사용합니다.
Loki는 로그를 저장하고 읽을 수 있는 RESTful API를 제공합니다. Java 앱에서 로그를 푸시하는 방법을 살펴보겠습니다. Loki는 다음과 같은 페이로드 구조를 기대합니다:
I’ll use Java, but you can achieve the same result with a different stack. The most straightforward code is the following:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
var template = "'{' \"streams\": ['{' \"stream\": '{' \"app\": \"{0}\" '}', \"values\": [[ \"{1}\", \"{2}\" ]]'}']'}'"; //1
var now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
var nowInEpochNanos = NANOSECONDS.convert(now.getEpochSecond(), SECONDS) + now.getNano();
var payload = MessageFormat.format(template, "demo", String.valueOf(nowInEpochNanos), "Hello from Java App"); //1
var request = HttpRequest.newBuilder() //2
.uri(new URI("http://localhost:3100/loki/api/v1/push"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); //3
}
- 이것이 예전에 문자열 보간을 수행하던 방식입니다
- 요청 생성
- 전송하기
프로토타입은 Grafana에서 볼 수 있듯이 작동합니다:
그러나 코드에는 많은 제한이 있습니다:
- 레이블이 하드코딩되어 있습니다. 단일 레이블을 보낼 수 있고 꼭 해야 합니다.
- 모든 것이 하드코딩되어 있으며, 구성 가능한 것은 없습니다. 예를 들어 URL
- 코드는 로그 당 하나의 요청을 보내며, 버퍼링이 없기 때문에 매우 비효율적입니다.
- HTTP 클라이언트는 동기식이므로 Loki를 기다리는 동안 스레드를 차단합니다.
- 오류 처리가 전혀 없습니다.
- Loki는 gzip 압축과 Protobuf를 모두 제공하지만, 내 코드는 어떤 것도 지원하지 않습니다.
-
마지막으로, 이것은 우리가 로그를 사용하는 방식과 완전히 무관합니다.예를 들면:
Javavar logger = // 로거 획득 logger.info("매개변수를 가진 내 메시지 {}, {}", foo, bar);
스테로이드로 일반 로깅
위의 문장을 사용하려면 로깅 구현체를 선택해야 합니다. 저는 SLF4J와 Logback에 더 익숙하니 이것들을 사용하겠습니다. 걱정하지 마세요. 동일한 접근 방식이 Log4J2에도 적용됩니다.
관련 종속성을 추가해야 합니다:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <!--1-->
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <!--2-->
<version>1.4.8</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.github.loki4j</groupId>
<artifactId>loki-logback-appender</artifactId> <!--3-->
<version>1.4.0</version>
<scope>runtime</scope>
</dependency>
- SLF4J는 인터페이스입니다.
- Logback은 구현체입니다.
- Logback 추가기가 SLF4J에 특화되어 있습니다
이제 특정 Loki 추가기를 추가합니다:
<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender"> <!--1-->
<http>
<url>http://localhost:3100/loki/api/v1/push</url> <!--2-->
</http>
<format>
<label>
<pattern>app=demo,host=${HOSTNAME},level=%level</pattern> <!--3-->
</label>
<message>
<pattern>l=%level h=${HOSTNAME} c=%logger{20} t=%thread | %msg %ex</pattern> <!--4-->
</message>
<sortByTime>true</sortByTime>
</format>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
- Loki 추가기
- Loki URL
- 원하는만큼 많은 라벨
- 일반적인 Logback 패턴
우리의 프로그램은 훨씬 간단해졌습니다:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana는 다음을 표시합니다:
도커 로깅
I’m running most of my demos on Docker Compose, so I’ll mention the Docker logging trick. When a container writes on the standard out, Docker saves it to a local file. The docker logs
command can access the file content.
그러나 로컬 파일에 저장하는 것 이외의 다른 옵션도 있습니다. 예를 들어, syslog
, Google Cloud, Splunk 등이 있습니다. 다른 옵션을 선택하려면 로깅 드라이버를 설정합니다. 전체 Docker 수준이나 각 컨테이너 단위로 드라이버를 구성할 수 있습니다.
Loki는 자체 플러그인을 제공합니다. 설치하려면:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
이 시점에서 컨테이너 애플리케이션에서 사용할 수 있습니다:
services:
app:
build: .
logging:
driver: loki #1
options:
loki-url: http://localhost:3100/loki/api/v1/push #2
loki-external-labels: container_name={{.Name}},app=demo #3
- Loki 로깅 드라이버
- 푸시할 URL
- 추가 라벨
결과는 다음과 같습니다. 기본 라벨에 유의하세요.
결론
높은 角 도식으로 보면, Loki는 특별한 것이 아닙니다: 그것은 단순한 저장 엔진으로, 위에 RESTful API가 있습니다.
API를 사용하는 몇 가지 접근 방식이 있습니다. 단순한 방법 이외에도, 우리는 Java 로깅 프레임워크 추가기와 Docker를 보았습니다. 다른 접근 방식에는 로그 파일을 스크래핑하는 것이 있습니다. 예를 들어, Promtail, Kubernetes 사이드카를 통해서 말이죠. 애플리케이션과 Loki 사이에 OpenTelemetry Collector를 추가하여 변환을 수행할 수도 있습니다.
옵션은 사실상 무한합니다. 컨텍스트에 가장 적합한 것을 선택하는 것에 주의하세요.
더 깊이 들어가기: