From fb9563e3324dead8eaf3f4e288aba679eaae7142 Mon Sep 17 00:00:00 2001
From: Anonymous
Date: Sun, 5 Apr 2026 09:22:20 -0700
Subject: [PATCH] feat: add SkyWalking integration plugin and guide
- add magic-api-plugin-skywalking module and auto configuration
- add request interceptor and tests for SkyWalking tracing
- add integration guide and README entry for issue #122
Closes #122
---
README.md | 7 +-
SKYWALKING_INTEGRATION.md | 95 ++++++++++++++++
.../magic-api-plugin-skywalking/pom.xml | 45 ++++++++
.../MagicSkyWalkingConfiguration.java | 22 ++++
.../skywalking/SkyWalkingInterceptor.java | 104 ++++++++++++++++++
.../main/resources/META-INF/spring.factories | 1 +
.../skywalking/SkyWalkingInterceptorTest.java | 73 ++++++++++++
magic-api-plugins/pom.xml | 1 +
8 files changed, 347 insertions(+), 1 deletion(-)
create mode 100644 SKYWALKING_INTEGRATION.md
create mode 100644 magic-api-plugins/magic-api-plugin-skywalking/pom.xml
create mode 100644 magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/MagicSkyWalkingConfiguration.java
create mode 100644 magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptor.java
create mode 100644 magic-api-plugins/magic-api-plugin-skywalking/src/main/resources/META-INF/spring.factories
create mode 100644 magic-api-plugins/magic-api-plugin-skywalking/src/test/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptorTest.java
diff --git a/README.md b/README.md
index 913aba7f..7dfa1e14 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
-[特性](#特性) | [快速开始](#快速开始) | [文档/演示](#文档演示) | [示例项目](#示例项目) | 更新日志 | [项目截图](#项目截图) | [交流群](#交流群)
+[特性](#特性) | [快速开始](#快速开始) | [文档/演示](#文档演示) | [示例项目](#示例项目) | [SkyWalking集成](#skywalking集成) | 更新日志 | [项目截图](#项目截图) | [交流群](#交流群)
# 简介
@@ -76,6 +76,11 @@ magic-api.resource.location=/data/magic-api
- [magic-api-example](https://gitee.com/ssssssss-team/magic-api-example)
+# SkyWalking集成
+
+- 使用说明文档:[`SKYWALKING_INTEGRATION.md`](SKYWALKING_INTEGRATION.md)
+- 文档包含:启动参数、最小验证步骤、常见“不生效”排查清单、链路断点场景说明
+
# 项目截图
|  |  |
|---|---|
diff --git a/SKYWALKING_INTEGRATION.md b/SKYWALKING_INTEGRATION.md
new file mode 100644
index 00000000..22329089
--- /dev/null
+++ b/SKYWALKING_INTEGRATION.md
@@ -0,0 +1,95 @@
+# magic-api 与 SkyWalking 集成指南
+
+本文用于说明在 `magic-api` 场景下如何正确接入 SkyWalking,以及“启动时加了 agent 但看不到链路”时的常见排查步骤。
+
+## 1. 适用范围
+
+- 通过 `magic-api-spring-boot-starter` 启动的 Spring Boot 应用
+- 使用 SkyWalking Java Agent(`-javaagent`)采集链路
+
+## 2. 基础接入步骤
+
+### 2.1 准备 SkyWalking Agent
+
+从 SkyWalking 官方发布包中解压 `skywalking-agent` 目录,确保可访问:
+
+- `skywalking-agent/skywalking-agent.jar`
+- `skywalking-agent/config/agent.config`
+
+### 2.2 启动参数示例
+
+```bash
+java \
+ -javaagent:/opt/skywalking-agent/skywalking-agent.jar \
+ -Dskywalking.agent.service_name=magic-api-demo \
+ -Dskywalking.collector.backend_service=127.0.0.1:11800 \
+ -jar app.jar
+```
+
+也可使用环境变量方式配置(与 `agent.config` 一致):
+
+```bash
+export SW_AGENT_NAME=magic-api-demo
+export SW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800
+```
+
+## 3. 最小验证流程
+
+1. 启动应用后,检查启动日志里是否出现 `SkyWalking agent started`
+2. 访问一个实际 API(而不是仅访问 `magic-api` 的静态页面)
+3. 在 SkyWalking UI 中确认:
+ - 服务名已出现(如 `magic-api-demo`)
+ - `trace` 中可见请求链路
+ - 请求耗时、错误率指标有数据
+
+## 4. 常见“不生效”排查
+
+### 4.1 仅访问了静态页面
+
+如果只打开了编辑器页面(如 `/magic/web`),通常不会形成你期望的业务调用链。
+请触发真实的业务 API 请求后再观察 trace。
+
+### 4.2 Agent 实际未加载
+
+检查是否存在以下问题:
+
+- `-javaagent` 路径错误
+- 启动脚本被覆盖(例如容器入口脚本没有携带 `-javaagent`)
+- JDK 版本与 Agent 版本不兼容
+
+### 4.3 OAP 地址不可达
+
+确认 `skywalking.collector.backend_service` 对当前运行环境可达(网络、防火墙、端口映射均正常)。
+
+### 4.4 服务名配置不一致
+
+如果应用实例很多,建议显式配置服务名并保持唯一且稳定,避免在 UI 中误判“未上报”。
+
+### 4.5 异步线程场景链路断开
+
+在跨线程执行(例如线程池异步任务)时,可能出现上下文未自动透传导致链路断点。
+如果你的脚本或扩展逻辑包含异步执行,请在该部分增加上下文透传或手动埋点。
+
+## 5. 可选:业务扩展代码中手动埋点
+
+若需要对自定义模块/扩展逻辑进行更细粒度追踪,可在业务工程中引入 SkyWalking toolkit 后手动埋点(示例):
+
+```java
+import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
+import org.apache.skywalking.apm.toolkit.trace.Trace;
+
+public class DemoService {
+
+ @Trace
+ public void execute(String apiName) {
+ ActiveSpan.tag("magic.api.name", apiName);
+ }
+}
+```
+
+> 注意:手动埋点依赖请按你的 SkyWalking 版本选择对应 toolkit 组件版本。
+
+## 6. 建议
+
+- 先完成“基础接入 + 最小验证”,再逐步扩展到网关、异步调用、外部存储等复杂链路。
+- 遇到“无数据”时优先从“Agent 是否加载成功”和“OAP 是否可达”两项开始排查。
diff --git a/magic-api-plugins/magic-api-plugin-skywalking/pom.xml b/magic-api-plugins/magic-api-plugin-skywalking/pom.xml
new file mode 100644
index 00000000..6fb3bf51
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-skywalking/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ magic-api-plugins
+ org.ssssssss
+ ${revision}
+
+ 4.0.0
+
+ magic-api-plugin-skywalking
+
+
+
+ org.ssssssss
+ magic-api
+ ${project.version}
+ provided
+
+
+ org.apache.skywalking
+ apm-toolkit-trace
+ 9.2.0
+ provided
+
+
+ org.springframework
+ spring-context
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.9.3
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.5.0
+ test
+
+
+
diff --git a/magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/MagicSkyWalkingConfiguration.java b/magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/MagicSkyWalkingConfiguration.java
new file mode 100644
index 00000000..6d1d1cbc
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/MagicSkyWalkingConfiguration.java
@@ -0,0 +1,22 @@
+package org.ssssssss.magicapi.skywalking;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * SkyWalking 集成自动配置
+ *
+ * @author magic-api
+ */
+@Configuration
+@ConditionalOnClass(name = "org.apache.skywalking.apm.toolkit.trace.Tracer")
+public class MagicSkyWalkingConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SkyWalkingInterceptor skyWalkingInterceptor() {
+ return new SkyWalkingInterceptor();
+ }
+}
diff --git a/magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptor.java b/magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptor.java
new file mode 100644
index 00000000..096b731c
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-skywalking/src/main/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptor.java
@@ -0,0 +1,104 @@
+package org.ssssssss.magicapi.skywalking;
+
+import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
+import org.apache.skywalking.apm.toolkit.trace.SpanRef;
+import org.apache.skywalking.apm.toolkit.trace.TraceContext;
+import org.apache.skywalking.apm.toolkit.trace.Tracer;
+import org.ssssssss.magicapi.core.context.RequestEntity;
+import org.ssssssss.magicapi.core.interceptor.RequestInterceptor;
+import org.ssssssss.magicapi.core.model.ApiInfo;
+
+/**
+ * SkyWalking 集成拦截器
+ * 在 magic-api 请求执行过程中创建和管理 SkyWalking span
+ *
+ * @author magic-api
+ */
+public class SkyWalkingInterceptor implements RequestInterceptor {
+
+ private static final ThreadLocal SPAN_HOLDER = new ThreadLocal<>();
+ private static final String OPERATION_NAME_PREFIX = "magic-api:";
+
+ @Override
+ public Object preHandle(RequestEntity requestEntity) throws Exception {
+ ApiInfo apiInfo = requestEntity.getApiInfo();
+ if (apiInfo == null) {
+ return null;
+ }
+
+ String operationName = OPERATION_NAME_PREFIX + apiInfo.getPath();
+
+ // 创建 LocalSpan 用于追踪 magic-api 脚本执行
+ SpanRef span = Tracer.createLocalSpan(operationName);
+
+ // 设置标签
+ span.tag("api.id", apiInfo.getId());
+ span.tag("api.name", apiInfo.getName());
+ span.tag("api.path", apiInfo.getPath());
+ span.tag("api.method", requestEntity.getRequest().getMethod());
+
+ // 将 span 存储到 ThreadLocal 中
+ SPAN_HOLDER.set(span);
+
+ return null;
+ }
+
+ @Override
+ public Object postHandle(RequestEntity requestEntity, Object value) throws Exception {
+ // 后置处理,可以在此记录返回值信息
+ SpanRef span = SPAN_HOLDER.get();
+ if (span != null && value != null) {
+ span.tag("result.type", value.getClass().getSimpleName());
+ }
+ return null;
+ }
+
+ @Override
+ public void afterCompletion(RequestEntity requestEntity, Object returnValue, Throwable throwable) {
+ SpanRef span = SPAN_HOLDER.get();
+ if (span == null) {
+ return;
+ }
+
+ try {
+ // 如果有异常,标记为错误
+ if (throwable != null) {
+ ActiveSpan.error(throwable);
+ span.log(throwable);
+ }
+ } finally {
+ // 清理 ThreadLocal
+ SPAN_HOLDER.remove();
+ // 结束 span
+ Tracer.stopSpan();
+ }
+ }
+
+ /**
+ * 获取当前 traceId
+ */
+ public static String getTraceId() {
+ return TraceContext.traceId();
+ }
+
+ /**
+ * 获取当前 segmentId
+ */
+ public static String getSegmentId() {
+ return TraceContext.segmentId();
+ }
+
+ /**
+ * 获取当前 spanId
+ */
+ public static int getSpanId() {
+ return TraceContext.spanId();
+ }
+
+ /**
+ * 添加自定义标签到当前 span
+ */
+ public static void tag(String key, String value) {
+ ActiveSpan.tag(key, value);
+ }
+}
diff --git a/magic-api-plugins/magic-api-plugin-skywalking/src/main/resources/META-INF/spring.factories b/magic-api-plugins/magic-api-plugin-skywalking/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..04c946bb
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-skywalking/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.ssssssss.magicapi.skywalking.MagicSkyWalkingConfiguration
diff --git a/magic-api-plugins/magic-api-plugin-skywalking/src/test/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptorTest.java b/magic-api-plugins/magic-api-plugin-skywalking/src/test/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptorTest.java
new file mode 100644
index 00000000..7db7eefa
--- /dev/null
+++ b/magic-api-plugins/magic-api-plugin-skywalking/src/test/java/org/ssssssss/magicapi/skywalking/SkyWalkingInterceptorTest.java
@@ -0,0 +1,73 @@
+package org.ssssssss.magicapi.skywalking;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.ssssssss.magicapi.core.context.RequestEntity;
+import org.ssssssss.magicapi.core.model.ApiInfo;
+import org.ssssssss.magicapi.core.servlet.MagicHttpServletRequest;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * SkyWalking 拦截器测试
+ */
+@ExtendWith(MockitoExtension.class)
+class SkyWalkingInterceptorTest {
+
+ private SkyWalkingInterceptor interceptor;
+
+ @Mock
+ private RequestEntity requestEntity;
+
+ @Mock
+ private ApiInfo apiInfo;
+
+ @Mock
+ private MagicHttpServletRequest request;
+
+ @BeforeEach
+ void setUp() {
+ interceptor = new SkyWalkingInterceptor();
+ }
+
+ @Test
+ void testPreHandleWithNullApiInfo() throws Exception {
+ when(requestEntity.getApiInfo()).thenReturn(null);
+
+ Object result = interceptor.preHandle(requestEntity);
+
+ assertNull(result);
+ }
+
+ @Test
+ void testPreHandleWithApiInfo() throws Exception {
+ when(apiInfo.getId()).thenReturn("test-api-id");
+ when(apiInfo.getName()).thenReturn("test-api");
+ when(apiInfo.getPath()).thenReturn("/test/path");
+ when(requestEntity.getApiInfo()).thenReturn(apiInfo);
+ when(requestEntity.getRequest()).thenReturn(request);
+ when(request.getMethod()).thenReturn("GET");
+
+ // 注意:这个测试在没有 SkyWalking agent 的情况下会抛出异常或返回 null
+ // 实际测试需要在有 SkyWalking agent 的环境中运行
+ try {
+ Object result = interceptor.preHandle(requestEntity);
+ assertNull(result);
+ } catch (Exception e) {
+ // 预期在没有 SkyWalking agent 时可能会有异常
+ assertTrue(e.getMessage() != null);
+ }
+ }
+
+ @Test
+ void testAfterCompletion() {
+ // 测试清理逻辑不会抛出异常
+ assertDoesNotThrow(() -> {
+ interceptor.afterCompletion(requestEntity, null, null);
+ });
+ }
+}
diff --git a/magic-api-plugins/pom.xml b/magic-api-plugins/pom.xml
index 8ea1dd66..36535c83 100644
--- a/magic-api-plugins/pom.xml
+++ b/magic-api-plugins/pom.xml
@@ -24,6 +24,7 @@
magic-api-plugin-cluster
magic-api-plugin-git
magic-api-plugin-nebula
+ magic-api-plugin-skywalking