Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,7 @@ scouter.agent.batch/.settings/org.eclipse.core.resources.prefs
scouter.server.jar
AGENTS.md
.DS_Store
<<<<<<< Updated upstream
=======
/.vscode
>>>>>>> Stashed changes
10 changes: 2 additions & 8 deletions build_package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,11 @@ if [ -z "$JAVA_VER" ]; then
fi

if [ "$JAVA_VER" -ge 8 ]; then
echo "JDK 8+ detected. Building java-8-plus profile"
echo "JDK $JAVA_VER detected. Building with the matching Maven profile"
$MVN -Dmaven.test.skip=true clean install
$MVN -Dmaven.test.skip=true -f ./scouter.agent.java/pom.xml -Pjava-8-plus package
else
echo "Scouter requires at least JDK 8."
exit 1
fi

if [ "$JAVA_VER" -ge 21 ]; then
echo "JDK 21+ detected. Building java-21-plus profile"
$MVN -Dmaven.test.skip=true -f ./scouter.agent.java/pom.xml -Pjava-21-plus package
fi

$MVN -Dmaven.test.skip=true -f ./scouter.deploy/pom.xml clean package
$MVN -Dmaven.test.skip=true -f ./scouter.deploy/pom.xml clean package
5 changes: 4 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<module>scouter.weaver</module>
<module>scouter.server.boot</module>
<module>scouter.server</module>
<module>scouter.plugin.server.xprof</module>
<module>scouter.agent.host</module>
<module>scouter.agent.java</module>
<module>scouter.agent.batch</module>
Expand Down Expand Up @@ -64,6 +65,7 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<scouter.build.directory>target</scouter.build.directory>

<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
<build.datetime>${maven.build.timestamp} GMT</build.datetime>
Expand Down Expand Up @@ -436,6 +438,7 @@
</dependencyManagement>

<build>
<directory>${project.basedir}/${scouter.build.directory}</directory>
<pluginManagement>
<plugins>
<plugin>
Expand Down Expand Up @@ -561,4 +564,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
89 changes: 89 additions & 0 deletions scouter.plugin.server.xprof/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# XLog/Profile Kafka 서버 플러그인

XLog와 프로파일을 Kafka 토픽에 JSON으로 발행합니다. 토픽 기본값은
`scouter.xlog.profile`, 키는 `objHash:txid`입니다. 기존 JSON 필드는 유지합니다.

이벤트 콜백에서는 필요한 필드와 프로파일 바이트를 복사하고 제한된 큐에 넣습니다.
프로파일 해석, 텍스트 조회, JSON 변환, Kafka 초기화와 `send()`는 단일 데몬
스레드에서 수행합니다. Kafka 메타데이터/버퍼 대기가 수집 스레드로 전파되지 않습니다.
단, 데이터 복사와 큐 삽입에 드는 CPU/메모리 비용은 발생합니다.

수집 처리를 우선하는 best-effort 방식입니다. 큐 포화, 크기 제한 초과, 초기화
실패 시 데이터가 유실될 수 있으며 재발행하거나 디스크에 보관하지 않습니다.
큐는 제출 순서대로 처리하지만 동시 호출 간 순서나 Kafka 전달 성공을 보장하지 않습니다.

## 설정

서버 `conf/scouter.conf`:

```properties
ext_plugin_kafka_profile_enabled=true
ext_plugin_kafka_profile_bootstrap=127.0.0.1:9092
ext_plugin_kafka_profile_topic=scouter.xlog.profile
ext_plugin_kafka_profile_include_objHashs=
ext_plugin_kafka_profile_queue_capacity=256
ext_plugin_kafka_profile_max_profile_bytes=1048576
ext_plugin_kafka_profile_max_block_ms=100
ext_plugin_kafka_profile_debug=false
```

- 큐 크기는 최초 클래스 로딩 시 적용하며 변경 후 서버 재시작이 필요합니다.
- 프로파일 바이트 크기 제한은 복사 전에 적용합니다. 기본 큐가 보관하는 원본
프로파일 바이트는 최대 약 256 MiB이며, 작업 중 데이터와 JSON/Kafka 버퍼는 별도입니다.
서버 여유 메모리에 맞춰 큐 크기를 낮추세요. JSON 변환 후 메시지 크기는
`ext_plugin_kafka_profile_max_request_size`(기본 1048576)의 제한도 받습니다.
- `max_block_ms`는 발행 작업 스레드의 메타데이터/버퍼 대기 제한입니다.
- 기존 `client_id`, `acks`, `retries`, `linger_ms`, `batch_size`, `compression`,
`max_request_size`, `delivery_timeout_ms`, `request_timeout_ms`, `buffer_memory`
설정도 동일한 `ext_plugin_kafka_profile_` 접두사로 지원합니다.
- producer 설정은 최초 초기화 성공 시 적용합니다. 초기화 실패 시 30초 동안
발행 데이터를 버리고 이후 이벤트에서 재시도합니다.
- 오류 및 드롭 로그는 전체 합산 최대 30초에 한 번 출력합니다. 큐/크기/초기화
거부 누적 수는 로그의 `dropped`와 `KafkaProfilePlugin.getDroppedCount()`로 확인합니다.
Kafka 비동기 전송 실패 수는 이 거부 카운터에 포함되지 않습니다.
- 종료 시 신규 제출을 거부하고 대기열을 버립니다. 현재 작업을 인터럽트하고
worker에서 Kafka close를 최대 1초 수행하며 호출자는 최대 2초 기다립니다.
종료 시 미전송 데이터가 유실될 수 있습니다.

## 빌드 및 설치

```sh
mvn -pl scouter.plugin.server.xprof -am -Dmaven.test.skip=true clean package
```

동일 버전의 server/common이 로컬 Maven 저장소에 설치되어 있다면:

```sh
mvn -f scouter.plugin.server.xprof/pom.xml test -Dmaven.test.skip=false -DskipTests=false
mvn -f scouter.plugin.server.xprof/pom.xml -Dmaven.test.skip=true clean package
```

빌드 산물은 플러그인 코드와 공유 의존 라이브러리로 분리됩니다.

```text
target/
scouter-plugin-server-xprof-2.21.4.jar
lib/
kafka-clients-3.7.2.jar
jackson-databind-2.15.4.jar
jackson-core-2.15.4.jar
jackson-annotations-2.15.4.jar
slf4j-api-1.7.25.jar
lz4-java-1.8.0.jar
snappy-java-1.1.10.5.jar
zstd-jni-1.5.6-4.jar
```

플러그인 JAR와 `target/lib/*.jar`를 서버 설치 디렉터리의 `lib/`에 함께
복사하고 재시작합니다. 서버가 `lib/*.jar`를 클래스패스에 포함해야 합니다.
의존 라이브러리는 패키지 재배치 없이 원래 JAR로 생성되어 다른 플러그인도 공유할 수 있습니다.
이미 같은 라이브러리가 설치되어 있으면 호환되는 한 버전을 함께 사용하세요.

`scouter-server`와 `scouter-common`은 `provided`로 서버가 제공하며 복사 대상에서
제외합니다. 이들에만 필요한 전이 의존성 및 테스트 의존성도 `target/lib/`에 복사하지 않습니다.
플러그인의 compile/runtime 의존성과 그 전이 의존성은 자동으로 복사합니다.

`original-*` JAR는 더 이상 생성하지 않습니다. 이전 Shade 빌드 산물이나
제거된 의존성이 남지 않도록 `clean package`를 사용하세요.
서버에 설치했던 이전 fat JAR는 새 플러그인 JAR로 교체하고, sources/javadoc JAR는 배포하지 않습니다.
서버 로그의 `KafkaProfilePlugin=>xlog`, `KafkaProfilePlugin=>profile` 등록을 확인하세요.
80 changes: 80 additions & 0 deletions scouter.plugin.server.xprof/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.github.scouter-project</groupId>
<artifactId>scouter-parent</artifactId>
<version>2.21.4</version>
</parent>

<artifactId>scouter-plugin-server-xprof</artifactId>
<packaging>jar</packaging>
<name>scouter.plugin.server.xprof</name>

<properties>
<skipTests>false</skipTests>
</properties>

<dependencies>
<!-- The collector provides these APIs at runtime. -->
<dependency>
<groupId>io.github.scouter-project</groupId>
<artifactId>scouter-server</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.github.scouter-project</groupId>
<artifactId>scouter-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.7.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-runtime-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<!-- Runtime includes compile dependencies, but excludes provided/test. -->
<includeScope>runtime</includeScope>
<excludeArtifactIds>scouter-server,scouter-common</excludeArtifactIds>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package scouter.plugin.server.kafka.profile;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

/** A bounded, best-effort queue. Producers never execute publishing work. */
final class AsyncPublisher {
private final ArrayBlockingQueue<Runnable> queue;
private final Thread worker;
private final Runnable maintenance;
private volatile boolean closed;
private volatile Runnable cleanup;

AsyncPublisher(int capacity) {
this(capacity, () -> {});
}

AsyncPublisher(int capacity, Runnable maintenance) {
this.maintenance = maintenance;
queue = new ArrayBlockingQueue<Runnable>(capacity);
worker = new Thread(this::run, "scouter-kafka-profile-publisher");
worker.setDaemon(true);
worker.start();
}

synchronized boolean offer(Runnable task) {
return !closed && queue.offer(task);
}

private void run() {
try {
while (!closed) {
maintenance.run();
Runnable task = queue.poll(1, TimeUnit.SECONDS);
if (task != null && !closed) {
task.run();
}
}
} catch (InterruptedException ignored) {
// Closing interrupts any current Kafka metadata/buffer wait.
} finally {
queue.clear();
Runnable action = cleanup;
if (action != null) {
// Kafka close must not inherit the worker's interrupted flag.
Thread.interrupted();
action.run();
}
}
}

void close(Runnable action) {
synchronized (this) {
if (closed) {
return;
}
cleanup = action;
closed = true;
queue.clear();
worker.interrupt();
}
try {
worker.join(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Loading