网上有通过Fluentbit将Logback的日志,写入到openobserve的方案,参考:
https://charones.com/archives/24052908.html
但是,我不想通过这个方案,想直接在配置文件中定义,最开始推荐通过引入appender的方式,但是找不到logback的appender,只有log4j的。于是,使用Http的方式写入。
第一步:
在logback-plus.xml中引入信息:
<!-- 自定义 Logback OpenObserve Appender(零依赖,完全兼容) -->
<appender name="openobserve_native" class="com.pcporg.log.NativeOpenObserveAppender">
<openObserveEndpoint>https://localhost:5080/api/你的 OpenObserve 租户名/suanli_frame/_json</openObserveEndpoint>
<tenant>你的 OpenObserve 租户名</tenant> <!-- 你的 OpenObserve 租户名 -->
<apiKey>API 密钥</apiKey> <!-- 你的 OpenObserve API 密钥(对应官方示例中的编码原文) -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</layout>
<!-- 其他配置保持不变 -->
</appender>
<!-- 异步包装(保持你的项目原有风格,避免阻塞业务线程) -->
<appender name="async_openobserve" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="openobserve_native"/>
</appender>
第二步,在root节点,新增:
<!-- 新增:引用OpenObserve异步Appender -->
<appender-ref ref="async_openobserve" />
第三步,编写自己的类,如下:
package com.pcporg.log; // 保持包名不变
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.Layout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import lombok.Getter;
import lombok.Setter;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Logback 零依赖 OpenObserve 自定义 Appender
* 修复:403 Forbidden 错误(认证方式改为 Basic)
* 保留:HTTPS 证书忽略验证功能
* 兼容:OpenObserve json_batch 格式要求
*/
@Setter
@Getter
public class NativeOpenObserveAppender extends AppenderBase<ILoggingEvent> {
// === 可在 Logback 配置中配置的参数 ===
private String openObserveEndpoint;
private String tenant; // 新增:OpenObserve 租户名(如 yqs@outlook.at)
private String apiKey; // 新增:OpenObserve API 密钥(替代原 authToken)
private int connectTimeout = 5000;
private int readTimeout = 10000;
private int queueSize = 512;
private Layout<ILoggingEvent> layout;
// === 异步发送线程池 ===
private ThreadPoolExecutor asyncExecutor;
private LinkedBlockingQueue<Runnable> taskQueue;
/**
* 初始化忽略 HTTPS 证书验证的 SSL 上下文
*/
private void initIgnoreSslContext() {
try {
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManager}, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HostnameVerifier hostnameVerifier = (hostname, session) -> true;
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
} catch (Exception e) {
addError("初始化忽略 HTTPS 证书验证的 SSL 上下文失败!", e);
}
}
/**
* 构建 Basic 认证字符串(核心修复:兼容 OpenObserve 认证要求)
* @return Basic 认证头值(格式:Basic + Base64(租户名:API密钥))
*/
private String buildBasicAuthHeader() {
if (tenant == null || tenant.trim().isEmpty() || apiKey == null || apiKey.trim().isEmpty()) {
addError("租户名(tenant)和 API 密钥(apiKey)不能为空!");
return "";
}
// 拼接 租户名:API密钥
String authStr = tenant.trim() + ":" + apiKey.trim();
// JDK 原生 Base64 编码(零依赖)
String base64AuthStr = Base64.getEncoder().encodeToString(authStr.getBytes(StandardCharsets.UTF_8));
// 返回 Basic 认证格式
return "Basic " + base64AuthStr;
}
/**
* Appender 初始化方法
*/
@Override
public void start() {
// 校验必填参数(更新为 tenant + apiKey)
if (openObserveEndpoint == null || openObserveEndpoint.trim().isEmpty()) {
addError("OpenObserve 端点(openObserveEndpoint)不能为空!");
return;
}
if (tenant == null || tenant.trim().isEmpty()) {
addError("OpenObserve 租户名(tenant)不能为空!");
return;
}
if (apiKey == null || apiKey.trim().isEmpty()) {
addError("OpenObserve API 密钥(apiKey)不能为空!");
return;
}
if (layout == null) {
addError("日志格式化器(layout)不能为空!");
return;
}
// HTTPS 证书忽略初始化
if (openObserveEndpoint.trim().toLowerCase().startsWith("https")) {
initIgnoreSslContext();
addInfo("检测到 HTTPS 协议,已启用忽略 HTTPS 证书验证功能!");
}
// 初始化异步线程池和任务队列
taskQueue = new LinkedBlockingQueue<>(queueSize);
asyncExecutor = new ThreadPoolExecutor(
1,
2,
60,
TimeUnit.SECONDS,
taskQueue,
new ThreadPoolExecutor.DiscardPolicy()
);
super.start();
addInfo("NativeOpenObserveAppender 初始化成功!");
}
/**
* 核心方法:处理日志事件
*/
@Override
protected void append(ILoggingEvent loggingEvent) {
if (!isStarted()) {
addWarn("NativeOpenObserveAppender 未初始化完成,忽略当前日志!");
return;
}
asyncExecutor.execute(() -> {
try {
String logContent = layout.doLayout(loggingEvent);
sendLogToOpenObserve(logContent, loggingEvent);
} catch (Exception e) {
addError("发送日志到 OpenObserve 失败!", e);
}
});
}
/**
* 发送日志到 OpenObserve(修复认证 + 兼容 json 格式)
*/
private void sendLogToOpenObserve(String logContent, ILoggingEvent loggingEvent) throws Exception {
if (logContent == null || logContent.trim().isEmpty()) {
return;
}
HttpURLConnection connection = null;
try {
URL url = new URL(openObserveEndpoint);
connection = (HttpURLConnection) url.openConnection();
// 设置连接参数
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setUseCaches(false);
connection.setConnectTimeout(connectTimeout);
connection.setReadTimeout(readTimeout);
// 构建 Basic 认证头(核心修复:替换原 Bearer 认证)
String basicAuthHeader = buildBasicAuthHeader();
if (basicAuthHeader.isEmpty()) {
return;
}
// 设置请求头(对齐官方示例)
connection.setRequestProperty("Authorization", basicAuthHeader); // 改用 Basic 认证
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
connection.setRequestProperty("Accept", "application/json");
// 构建请求体(兼容 json_batch 格式,单条日志也可正常接收)
String escapedLogContent = escapeJson(logContent);
String level = escapeJson(loggingEvent.getLevel().toString());
String loggerName = escapeJson(loggingEvent.getLoggerName());
// 保持 json 格式规范,避免解析失败
String requestBody = String.format(
"{\"log\":\"%s\",\"level\":\"%s\",\"logger\":\"%s\"}",
escapedLogContent,
level,
loggerName
);
// 写入请求体并发送
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write(requestBody);
writer.flush();
}
// 校验响应状态码
int responseCode = connection.getResponseCode();
if (responseCode >= 200 && responseCode < 300) {
addInfo("日志发送成功,响应码:" + responseCode);
} else {
addError("发送日志失败,响应码:" + responseCode + ",端点:" + openObserveEndpoint + ",请求体:" + requestBody);
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* JSON 字符串转义
*/
private String escapeJson(String content) {
if (content == null) {
return "";
}
return content.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
/**
* Appender 销毁方法
*/
@Override
public void stop() {
if (asyncExecutor != null) {
asyncExecutor.shutdown();
try {
if (!asyncExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
asyncExecutor.shutdownNow();
}
} catch (InterruptedException e) {
asyncExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
super.stop();
addInfo("NativeOpenObserveAppender 已销毁!");
}
}
最后,编译调试即可。上面这个java类,按xml中配置的位置放置即可,全程均由豆包老师辅助完整,给出了很多方案,仅此方案通过。成功后的样子:

评论0
暂时没有评论