网上有通过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中配置的位置放置即可,全程均由豆包老师辅助完整,给出了很多方案,仅此方案通过。成功后的样子:

image.png