You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/04/16 07:40:38 UTC

(skywalking) 01/01: Add OTLP/HTTP receiver support for traces, logs, and metrics

This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch feature/otlp-http-receiver
in repository https://gitbox.apache.org/repos/asf/skywalking.git

commit ed9eb3543a06860b5083f3126557ba05b71b0cde
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu Apr 16 14:55:07 2026 +0800

    Add OTLP/HTTP receiver support for traces, logs, and metrics
    
    Each existing OTLP gRPC handler now also registers an HTTP handler
    at startup. Supported endpoints:
    - POST /v1/traces (protobuf + JSON)
    - POST /v1/logs (protobuf + JSON)
    - POST /v1/metrics (protobuf + JSON)
    
    Processing logic extracted into shared processExport() methods
    called by both gRPC and HTTP handlers. No new config needed —
    HTTP endpoints are automatically available when the OTLP handler
    is enabled.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
 docs/en/changes/changes.md                         |  1 +
 docs/en/setup/backend/otlp-trace.md                | 13 +++-
 .../otel/otlp/OpenTelemetryLogHTTPHandler.java     | 85 ++++++++++++++++++++++
 .../otel/otlp/OpenTelemetryLogHandler.java         | 21 +++++-
 .../otel/otlp/OpenTelemetryMetricHTTPHandler.java  | 85 ++++++++++++++++++++++
 .../otel/otlp/OpenTelemetryMetricHandler.java      | 10 +++
 .../otel/otlp/OpenTelemetryTraceHTTPHandler.java   | 85 ++++++++++++++++++++++
 .../otel/otlp/OpenTelemetryTraceHandler.java       | 22 +++++-
 8 files changed, 316 insertions(+), 6 deletions(-)

diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 8ba337cd97..36cd8bc134 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -28,6 +28,7 @@
 * LAL: add `sourceAttribute()` function for non-persistent OTLP resource attribute access in LAL scripts.
 * LAL: add `layer: auto` mode for dynamic layer assignment when `service.layer` is absent.
 * Add two-phase `SpanListener` SPI mechanism for extensible trace span processing. Refactor GenAI from hardcoded `SpanForward.processGenAILogic()` to `GenAISpanListener`.
+* Add OTLP/HTTP receiver support for traces, logs, and metrics (`/v1/traces`, `/v1/logs`, `/v1/metrics`). Supports both `application/x-protobuf` and `application/json` content types.
 
 #### UI
 
diff --git a/docs/en/setup/backend/otlp-trace.md b/docs/en/setup/backend/otlp-trace.md
index 6afd5f984a..8bf250556b 100644
--- a/docs/en/setup/backend/otlp-trace.md
+++ b/docs/en/setup/backend/otlp-trace.md
@@ -1,10 +1,21 @@
 # OpenTelemetry Trace Format
 
-SkyWalking can receive traces from Traces in OTLP format and convert them to Zipkin Trace format eventually. 
+SkyWalking can receive traces from Traces in OTLP format and convert them to Zipkin Trace format eventually.
 For data analysis and queries related to Zipkin Trace, please [refer to the relevant documentation](./zipkin-trace.md#zipkin-query).
 
 OTLP Trace handler references the [Zipkin Exporter in the OpenTelemetry Collector](https://opentelemetry.io/docs/specs/otel/trace/sdk_exporters/zipkin/#summary) to convert the data format.
 
+## Supported Protocols
+
+Both **OTLP/gRPC** and **OTLP/HTTP** are supported:
+
+| Protocol | Port | Endpoint |
+|----------|------|----------|
+| OTLP/gRPC | 11800 (shared gRPC port) | gRPC `TraceService/Export` |
+| OTLP/HTTP | 12800 (shared HTTP port) | `POST /v1/traces` |
+
+OTLP/HTTP supports both `application/x-protobuf` and `application/json` content types.
+
 ## Set up backend receiver
 
 1. Make sure to enable **otlp-traces** handler in OTLP receiver of `application.yml`.
diff --git a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHTTPHandler.java b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHTTPHandler.java
new file mode 100644
index 0000000000..9b2dab70a2
--- /dev/null
+++ b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHTTPHandler.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.receiver.otel.otlp;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+import com.linecorp.armeria.common.HttpData;
+import com.linecorp.armeria.common.HttpRequest;
+import com.linecorp.armeria.common.HttpResponse;
+import com.linecorp.armeria.common.HttpStatus;
+import com.linecorp.armeria.common.MediaType;
+import com.linecorp.armeria.server.annotation.Consumes;
+import com.linecorp.armeria.server.annotation.ConsumesJson;
+import com.linecorp.armeria.server.annotation.Post;
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * OTLP/HTTP handler for log data. Supports both protobuf and JSON encoding.
+ * Delegates processing to {@link OpenTelemetryLogHandler#processExport}.
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class OpenTelemetryLogHTTPHandler {
+    private static final byte[] EMPTY_RESPONSE =
+        ExportLogsServiceResponse.getDefaultInstance().toByteArray();
+
+    private final OpenTelemetryLogHandler logHandler;
+
+    @Post("/v1/logs")
+    @Consumes("application/x-protobuf")
+    public HttpResponse collectProtobuf(HttpRequest req) {
+        return HttpResponse.from(req.aggregate().thenApply(request -> {
+            try (final HttpData data = request.content()) {
+                final ExportLogsServiceRequest exportRequest =
+                    ExportLogsServiceRequest.parseFrom(data.byteBuf().nioBuffer());
+                logHandler.processExport(exportRequest);
+                return HttpResponse.of(HttpStatus.OK, MediaType.PROTOBUF, EMPTY_RESPONSE);
+            } catch (InvalidProtocolBufferException e) {
+                log.warn("Failed to parse OTLP/HTTP log request", e);
+                return HttpResponse.of(HttpStatus.BAD_REQUEST);
+            } catch (Exception e) {
+                log.error("Failed to process OTLP/HTTP log request", e);
+                return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        }));
+    }
+
+    @Post("/v1/logs")
+    @ConsumesJson
+    public HttpResponse collectJson(HttpRequest req) {
+        return HttpResponse.from(req.aggregate().thenApply(request -> {
+            try (final HttpData data = request.content()) {
+                final ExportLogsServiceRequest.Builder builder =
+                    ExportLogsServiceRequest.newBuilder();
+                JsonFormat.parser().merge(data.toStringUtf8(), builder);
+                logHandler.processExport(builder.build());
+                return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{}");
+            } catch (InvalidProtocolBufferException e) {
+                log.warn("Failed to parse OTLP/HTTP JSON log request", e);
+                return HttpResponse.of(HttpStatus.BAD_REQUEST);
+            } catch (Exception e) {
+                log.error("Failed to process OTLP/HTTP JSON log request", e);
+                return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        }));
+    }
+}
diff --git a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java
index 5f7983083d..4f7ae66b0e 100644
--- a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java
+++ b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryLogHandler.java
@@ -36,7 +36,10 @@ import org.apache.skywalking.apm.network.logging.v3.TextLog;
 import org.apache.skywalking.oap.server.core.source.LogMetadataUtils;
 import org.apache.skywalking.oap.log.analyzer.v2.module.LogAnalyzerModule;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.ILogAnalyzerService;
+import com.linecorp.armeria.common.HttpMethod;
+import java.util.Collections;
 import org.apache.skywalking.oap.server.core.server.GRPCHandlerRegister;
+import org.apache.skywalking.oap.server.core.server.HTTPHandlerRegister;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 import org.apache.skywalking.oap.server.receiver.otel.Handler;
@@ -88,10 +91,26 @@ public class OpenTelemetryLogHandler
                                                          .provider()
                                                          .getService(GRPCHandlerRegister.class);
         grpcHandlerRegister.addHandler(this);
+
+        HTTPHandlerRegister httpHandlerRegister = manager.find(SharingServerModule.NAME)
+                                                         .provider()
+                                                         .getService(HTTPHandlerRegister.class);
+        httpHandlerRegister.addHandler(
+            new OpenTelemetryLogHTTPHandler(this),
+            Collections.singletonList(HttpMethod.POST));
     }
 
     @Override
     public void export(ExportLogsServiceRequest request, StreamObserver<ExportLogsServiceResponse> responseObserver) {
+        processExport(request);
+        responseObserver.onNext(ExportLogsServiceResponse.getDefaultInstance());
+        responseObserver.onCompleted();
+    }
+
+    /**
+     * Process an OTLP log export request. Shared by both gRPC and HTTP handlers.
+     */
+    void processExport(ExportLogsServiceRequest request) {
         request.getResourceLogsList().forEach(resourceLogs -> {
             final var resource = resourceLogs.getResource();
             final var attributes = resource
@@ -122,8 +141,6 @@ public class OpenTelemetryLogHandler
                     }
                 });
         });
-        responseObserver.onNext(ExportLogsServiceResponse.getDefaultInstance());
-        responseObserver.onCompleted();
     }
 
     private void doAnalysisQuietly(String service, String layer, String serviceInstance,
diff --git a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryMetricHTTPHandler.java b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryMetricHTTPHandler.java
new file mode 100644
index 0000000000..4dbdb4e86e
--- /dev/null
+++ b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryMetricHTTPHandler.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.receiver.otel.otlp;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+import com.linecorp.armeria.common.HttpData;
+import com.linecorp.armeria.common.HttpRequest;
+import com.linecorp.armeria.common.HttpResponse;
+import com.linecorp.armeria.common.HttpStatus;
+import com.linecorp.armeria.common.MediaType;
+import com.linecorp.armeria.server.annotation.Consumes;
+import com.linecorp.armeria.server.annotation.ConsumesJson;
+import com.linecorp.armeria.server.annotation.Post;
+import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
+import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * OTLP/HTTP handler for metric data. Supports both protobuf and JSON encoding.
+ * Delegates processing to {@link OpenTelemetryMetricRequestProcessor#processMetricsRequest}.
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class OpenTelemetryMetricHTTPHandler {
+    private static final byte[] EMPTY_RESPONSE =
+        ExportMetricsServiceResponse.getDefaultInstance().toByteArray();
+
+    private final OpenTelemetryMetricRequestProcessor metricProcessor;
+
+    @Post("/v1/metrics")
+    @Consumes("application/x-protobuf")
+    public HttpResponse collectProtobuf(HttpRequest req) {
+        return HttpResponse.from(req.aggregate().thenApply(request -> {
+            try (final HttpData data = request.content()) {
+                final ExportMetricsServiceRequest exportRequest =
+                    ExportMetricsServiceRequest.parseFrom(data.byteBuf().nioBuffer());
+                metricProcessor.processMetricsRequest(exportRequest);
+                return HttpResponse.of(HttpStatus.OK, MediaType.PROTOBUF, EMPTY_RESPONSE);
+            } catch (InvalidProtocolBufferException e) {
+                log.warn("Failed to parse OTLP/HTTP metric request", e);
+                return HttpResponse.of(HttpStatus.BAD_REQUEST);
+            } catch (Exception e) {
+                log.error("Failed to process OTLP/HTTP metric request", e);
+                return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        }));
+    }
+
+    @Post("/v1/metrics")
+    @ConsumesJson
+    public HttpResponse collectJson(HttpRequest req) {
+        return HttpResponse.from(req.aggregate().thenApply(request -> {
+            try (final HttpData data = request.content()) {
+                final ExportMetricsServiceRequest.Builder builder =
+                    ExportMetricsServiceRequest.newBuilder();
+                JsonFormat.parser().merge(data.toStringUtf8(), builder);
+                metricProcessor.processMetricsRequest(builder.build());
+                return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{}");
+            } catch (InvalidProtocolBufferException e) {
+                log.warn("Failed to parse OTLP/HTTP JSON metric request", e);
+                return HttpResponse.of(HttpStatus.BAD_REQUEST);
+            } catch (Exception e) {
+                log.error("Failed to process OTLP/HTTP JSON metric request", e);
+                return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        }));
+    }
+}
diff --git a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryMetricHandler.java b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryMetricHandler.java
index 5c69711ee6..d1abb2bf87 100644
--- a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryMetricHandler.java
+++ b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryMetricHandler.java
@@ -23,7 +23,10 @@ import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
 import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
 import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc;
 import lombok.extern.slf4j.Slf4j;
+import com.linecorp.armeria.common.HttpMethod;
+import java.util.Collections;
 import org.apache.skywalking.oap.server.core.server.GRPCHandlerRegister;
+import org.apache.skywalking.oap.server.core.server.HTTPHandlerRegister;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 import org.apache.skywalking.oap.server.receiver.otel.Handler;
@@ -58,6 +61,13 @@ public class OpenTelemetryMetricHandler
                                                          .provider()
                                                          .getService(GRPCHandlerRegister.class);
         grpcHandlerRegister.addHandler(this);
+
+        HTTPHandlerRegister httpHandlerRegister = manager.find(SharingServerModule.NAME)
+                                                         .provider()
+                                                         .getService(HTTPHandlerRegister.class);
+        httpHandlerRegister.addHandler(
+            new OpenTelemetryMetricHTTPHandler(metricRequestProcessor),
+            Collections.singletonList(HttpMethod.POST));
     }
 
     @Override
diff --git a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryTraceHTTPHandler.java b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryTraceHTTPHandler.java
new file mode 100644
index 0000000000..cd1d016e6b
--- /dev/null
+++ b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryTraceHTTPHandler.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.receiver.otel.otlp;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+import com.linecorp.armeria.common.HttpData;
+import com.linecorp.armeria.common.HttpRequest;
+import com.linecorp.armeria.common.HttpResponse;
+import com.linecorp.armeria.common.HttpStatus;
+import com.linecorp.armeria.common.MediaType;
+import com.linecorp.armeria.server.annotation.Consumes;
+import com.linecorp.armeria.server.annotation.ConsumesJson;
+import com.linecorp.armeria.server.annotation.Post;
+import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
+import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * OTLP/HTTP handler for trace data. Supports both protobuf and JSON encoding.
+ * Delegates processing to {@link OpenTelemetryTraceHandler#processExport}.
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class OpenTelemetryTraceHTTPHandler {
+    private static final byte[] EMPTY_RESPONSE =
+        ExportTraceServiceResponse.getDefaultInstance().toByteArray();
+
+    private final OpenTelemetryTraceHandler traceHandler;
+
+    @Post("/v1/traces")
+    @Consumes("application/x-protobuf")
+    public HttpResponse collectProtobuf(HttpRequest req) {
+        return HttpResponse.from(req.aggregate().thenApply(request -> {
+            try (final HttpData data = request.content()) {
+                final ExportTraceServiceRequest exportRequest =
+                    ExportTraceServiceRequest.parseFrom(data.byteBuf().nioBuffer());
+                traceHandler.processExport(exportRequest);
+                return HttpResponse.of(HttpStatus.OK, MediaType.PROTOBUF, EMPTY_RESPONSE);
+            } catch (InvalidProtocolBufferException e) {
+                log.warn("Failed to parse OTLP/HTTP trace request", e);
+                return HttpResponse.of(HttpStatus.BAD_REQUEST);
+            } catch (Exception e) {
+                log.error("Failed to process OTLP/HTTP trace request", e);
+                return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        }));
+    }
+
+    @Post("/v1/traces")
+    @ConsumesJson
+    public HttpResponse collectJson(HttpRequest req) {
+        return HttpResponse.from(req.aggregate().thenApply(request -> {
+            try (final HttpData data = request.content()) {
+                final ExportTraceServiceRequest.Builder builder =
+                    ExportTraceServiceRequest.newBuilder();
+                JsonFormat.parser().merge(data.toStringUtf8(), builder);
+                traceHandler.processExport(builder.build());
+                return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{}");
+            } catch (InvalidProtocolBufferException e) {
+                log.warn("Failed to parse OTLP/HTTP JSON trace request", e);
+                return HttpResponse.of(HttpStatus.BAD_REQUEST);
+            } catch (Exception e) {
+                log.error("Failed to process OTLP/HTTP JSON trace request", e);
+                return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        }));
+    }
+}
diff --git a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryTraceHandler.java b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryTraceHandler.java
index 6e769eb288..a03e9a4045 100644
--- a/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryTraceHandler.java
+++ b/oap-server/server-receiver-plugin/otel-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/otel/otlp/OpenTelemetryTraceHandler.java
@@ -35,7 +35,10 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.skywalking.oap.server.core.CoreModule;
 import org.apache.skywalking.oap.server.core.trace.SpanListenerManager;
 import org.apache.skywalking.oap.server.core.trace.SpanListenerResult;
+import com.linecorp.armeria.common.HttpMethod;
+import java.util.Collections;
 import org.apache.skywalking.oap.server.core.server.GRPCHandlerRegister;
+import org.apache.skywalking.oap.server.core.server.HTTPHandlerRegister;
 import org.apache.skywalking.oap.server.library.module.ModuleManager;
 import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
@@ -107,10 +110,26 @@ public class OpenTelemetryTraceHandler
             .provider()
             .getService(GRPCHandlerRegister.class);
         grpcHandlerRegister.addHandler(this);
+
+        HTTPHandlerRegister httpHandlerRegister = manager.find(SharingServerModule.NAME)
+            .provider()
+            .getService(HTTPHandlerRegister.class);
+        httpHandlerRegister.addHandler(
+            new OpenTelemetryTraceHTTPHandler(this),
+            Collections.singletonList(HttpMethod.POST));
     }
 
     @Override
     public void export(ExportTraceServiceRequest request, StreamObserver<ExportTraceServiceResponse> responseObserver) {
+        processExport(request);
+        responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance());
+        responseObserver.onCompleted();
+    }
+
+    /**
+     * Process an OTLP trace export request. Shared by both gRPC and HTTP handlers.
+     */
+    void processExport(ExportTraceServiceRequest request) {
         final ArrayList<Span> result = new ArrayList<>();
 
         try (final var unused = getProcessHistogram().createTimer()) {
@@ -167,9 +186,6 @@ public class OpenTelemetryTraceHandler
                 getDroppedSpans().inc(result.size() - processedSpans.size());
             }
         }
-
-        responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance());
-        responseObserver.onCompleted();
     }
 
     private Span convertSpan(io.opentelemetry.proto.trace.v1.Span span, String serviceName, Map<String, String> resourceTags) {