Compare commits
4 Commits
v0.6.1-cdc
...
f1e466a772
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1e466a772 | ||
|
|
b72c8d81d4 | ||
|
|
959d58a95d | ||
|
|
d8dab70406 |
281
docs/standards/full-stack-runbook.md
Normal file
281
docs/standards/full-stack-runbook.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# 全链路启用与测试手册(Full Stack Runbook)
|
||||||
|
|
||||||
|
> 版本:1.0
|
||||||
|
> 日期:2026-07-09
|
||||||
|
> 适用范围:Edu 微服务架构(P6 阶段:11 基础设施 + 10 应用服务 + 1 前端)
|
||||||
|
> 关联文档:[local-dev-runbook](./local-dev-runbook.md)、[004 架构影响地图](../architecture/004_architecture_impact_map.md)、[known-issues](../troubleshooting/known-issues.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 前置条件
|
||||||
|
|
||||||
|
| 工具 | 版本要求 | 验证命令 |
|
||||||
|
| -------------- | -------- | ------------------------ |
|
||||||
|
| Node.js | ≥ 20 | `node -v` |
|
||||||
|
| pnpm | ≥ 9 | `pnpm -v` |
|
||||||
|
| Go | 1.22+ | `go version` |
|
||||||
|
| uv | 0.4+ | `uv --version` |
|
||||||
|
| Docker | 24+ | `docker version` |
|
||||||
|
| Docker Compose | v2+ | `docker compose version` |
|
||||||
|
|
||||||
|
> Windows 用户:Go 工具链若不在 PATH,临时加入:`$env:Path = "C:\Program Files\Go\bin;" + $env:Path`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 服务端口矩阵
|
||||||
|
|
||||||
|
### 2.1 应用服务(10 个)
|
||||||
|
|
||||||
|
| 端口 | 服务 | 语言/框架 | 启动方式 |
|
||||||
|
| ---- | -------------- | --------- | ---------------------------------------------- |
|
||||||
|
| 3000 | teacher-portal | Next.js | `pnpm --filter teacher-portal dev` |
|
||||||
|
| 3001 | classes | NestJS | `pnpm --filter @edu/classes-service dev` |
|
||||||
|
| 3002 | iam | NestJS | `pnpm --filter @edu/iam-service dev` |
|
||||||
|
| 3003 | teacher-bff | NestJS | `pnpm --filter @edu/teacher-bff dev` |
|
||||||
|
| 3004 | core-edu | NestJS | `pnpm --filter @edu/core-edu-service dev` |
|
||||||
|
| 3005 | content | NestJS | `pnpm --filter @edu/content-service dev` |
|
||||||
|
| 3006 | data-ana | FastAPI | `uv run uvicorn data_ana.main:app --port 3006` |
|
||||||
|
| 3007 | msg | NestJS | `pnpm --filter @edu/msg-service dev` |
|
||||||
|
| 3008 | ai | FastAPI | `uv run uvicorn ai.main:app --port 3008` |
|
||||||
|
| 8080 | api-gateway | Go (Gin) | `go run .` |
|
||||||
|
| 8081 | push-gateway | Go (Gin) | `go run .` |
|
||||||
|
|
||||||
|
### 2.2 基础设施(11 个)
|
||||||
|
|
||||||
|
| 端口 | 服务 | 用途 |
|
||||||
|
| --------- | ------------------ | --------------------- |
|
||||||
|
| 3306 | MySQL 8 | 写模型主库 |
|
||||||
|
| 6379 | Redis 7 | 缓存/会话 |
|
||||||
|
| 8083 | Debezium Connect | CDC source connector |
|
||||||
|
| 8123/9000 | ClickHouse 24.3 | 读模型宽表 |
|
||||||
|
| 9092 | Kafka 7.6 | 事件总线 |
|
||||||
|
| 7474/7687 | Neo4j 5.20 | 知识图谱 |
|
||||||
|
| 9200 | Elasticsearch 8.13 | 题库检索 |
|
||||||
|
| 9090 | Prometheus | 指标采集 |
|
||||||
|
| 3030 | Grafana | 可视化(admin/admin) |
|
||||||
|
| 16686 | Jaeger | 分布式追踪 UI |
|
||||||
|
| 4318 | OTLP Collector | OTel span 接收 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 一键启动
|
||||||
|
|
||||||
|
### 3.1 启动基础设施
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd e:\Desktop\Edu\infra
|
||||||
|
docker compose -f docker-compose.yml --profile p6 --profile observability up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
等待所有容器 healthy(约 60 秒):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker ps --filter "name=edu-" --format "table {{.Names}}\t{{.Status}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 一键启动所有应用服务
|
||||||
|
|
||||||
|
使用项目根目录的启动脚本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd e:\Desktop\Edu
|
||||||
|
.\scripts\start-all.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
该脚本会:
|
||||||
|
|
||||||
|
1. 检查基础设施健康状态
|
||||||
|
2. 为每个应用服务启动独立后台窗口(带标题)
|
||||||
|
3. 自动注入 Python 服务的环境变量(CLICKHOUSE/KAFKA/OTEL)
|
||||||
|
4. 等待所有服务健康检查通过
|
||||||
|
|
||||||
|
### 3.3 健康检查
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd e:\Desktop\Edu
|
||||||
|
.\scripts\health-check.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
预期输出:所有服务 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 端到端链路测试
|
||||||
|
|
||||||
|
### 4.1 IAM 注册 + 登录
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$h = @{Authorization="Bearer dev-token"}
|
||||||
|
|
||||||
|
# 注册
|
||||||
|
$body = @{username="testteacher";password="Test@1234";email="test@edu.com";role="teacher"} | ConvertTo-Json
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:8080/iam/auth/register" -Method Post -Body $body -ContentType "application/json" -Headers $h
|
||||||
|
|
||||||
|
# 登录
|
||||||
|
$loginBody = @{username="testteacher";password="Test@1234"} | ConvertTo-Json
|
||||||
|
$resp = Invoke-RestMethod -Uri "http://localhost:8080/iam/auth/login" -Method Post -Body $loginBody -ContentType "application/json"
|
||||||
|
$token = $resp.data.accessToken
|
||||||
|
Write-Host "Token: $token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Classes CRUD
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 创建班级
|
||||||
|
$classBody = @{name="高三一班";gradeId="grade-1";headTeacherId=""} | ConvertTo-Json
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:8080/classes" -Method Post -Body $classBody -ContentType "application/json" -Headers $h
|
||||||
|
|
||||||
|
# 查询班级列表
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:8080/classes" -Method Get -Headers $h
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 CDC 完整链路(MySQL → Debezium → Kafka → data-ana → ClickHouse)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. 向 MySQL 插入成绩(触发 binlog)
|
||||||
|
docker exec edu-mysql mysql -uedu -pchangeme next_edu_cloud -e "
|
||||||
|
INSERT INTO core_edu_exams (id, class_id, subject_id, title, exam_date, total_score, created_at, updated_at)
|
||||||
|
VALUES ('exam-cdc-test-001','cls-test-001','sub-math','CDC测试考试',NOW(),100,NOW(),NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE updated_at=NOW();
|
||||||
|
|
||||||
|
INSERT INTO core_edu_grades (id, exam_id, student_id, score, rank_in_class, created_at, updated_at)
|
||||||
|
VALUES ('grade-cdc-001','exam-cdc-test-001','student-cdc-001',92.5,1,NOW(),NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE score=92.5, updated_at=NOW();
|
||||||
|
"
|
||||||
|
|
||||||
|
# 2. 等待 Debezium 捕获 + data-ana 消费
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
|
# 3. 验证 ClickHouse 已同步
|
||||||
|
docker exec edu-clickhouse clickhouse-client --user default --password clickhouse -q "
|
||||||
|
SELECT student_id, class_id, exam_id, score, last_updated
|
||||||
|
FROM edu_analytics.student_dashboard_view
|
||||||
|
WHERE student_id = 'student-cdc-001'
|
||||||
|
ORDER BY last_updated DESC
|
||||||
|
"
|
||||||
|
# 预期:返回一行,score=92.5,class_id='cls-test-001'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 data-ana 查询 API
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 学生学情看板
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:3006/analytics/student/student-cdc-001/weakness" -Headers $h
|
||||||
|
|
||||||
|
# 班级成绩分析
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:3006/analytics/class/cls-test-001/performance" -Headers $h
|
||||||
|
|
||||||
|
# CDC 消费者状态
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:3006/readyz"
|
||||||
|
# 预期: cdc_consumer = "running"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 可观测性验证
|
||||||
|
|
||||||
|
| 检查项 | URL | 预期 |
|
||||||
|
| ------------------ | ----------------------------------- | ------------------------ |
|
||||||
|
| Prometheus targets | http://localhost:9090/targets | 所有 target UP |
|
||||||
|
| Grafana 面板 | http://localhost:3030 (admin/admin) | 可登录 |
|
||||||
|
| Jaeger UI | http://localhost:16686 | 可搜索到各 service trace |
|
||||||
|
| data-ana /metrics | http://localhost:3006/metrics | Prometheus 格式输出 |
|
||||||
|
| iam /metrics | http://localhost:3002/metrics | Prometheus 格式输出 |
|
||||||
|
|
||||||
|
**Jaeger trace 验证步骤**:
|
||||||
|
|
||||||
|
1. 打开 http://localhost:16686
|
||||||
|
2. Service 下拉框应能看到 `iam`、`classes`、`data-ana`、`api-gateway` 等
|
||||||
|
3. 选择任一服务 → Find Traces → 应看到 HTTP 请求的自动埋点 span
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 一键停止
|
||||||
|
|
||||||
|
### 5.1 停止应用服务
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd e:\Desktop\Edu
|
||||||
|
.\scripts\stop-all.ps1
|
||||||
|
|
||||||
|
.\scripts\stop-all.ps1 -KillByPort
|
||||||
|
```
|
||||||
|
|
||||||
|
该脚本会关闭所有 `edu-app-*` 标题的终端窗口。
|
||||||
|
|
||||||
|
### 5.2 停止基础设施
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd e:\Desktop\Edu\infra
|
||||||
|
docker compose -f docker-compose.yml --profile p6 --profile observability down
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 故障排查
|
||||||
|
|
||||||
|
### 6.1 端口占用
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 查看占用端口的进程
|
||||||
|
netstat -ano | findstr :3001
|
||||||
|
# 终止进程
|
||||||
|
taskkill /PID <PID> /F
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 基础设施未启动
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 检查容器状态
|
||||||
|
docker ps --filter "name=edu-"
|
||||||
|
|
||||||
|
# 重启单个容器
|
||||||
|
docker restart edu-mysql
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker logs edu-debezium --tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 CDC 链路断开
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. 检查 Debezium connector 状态
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:8083/connectors/edu-mysql-source/status"
|
||||||
|
|
||||||
|
# 2. 重启 connector
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:8083/connectors/edu-mysql-source/restart" -Method Post
|
||||||
|
|
||||||
|
# 3. 检查 Kafka topic 是否有数据
|
||||||
|
docker exec edu-kafka kafka-console-consumer --bootstrap-server localhost:9092 --topic edu-cdc.next_edu_cloud.core_edu_grades --from-beginning --max-messages 1
|
||||||
|
|
||||||
|
# 4. 检查 data-ana 消费者日志
|
||||||
|
# 查看 data-ana 终端窗口的 cdc_consumer_started / cdc_event_received 日志
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 OTel trace 未上报
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 1. 检查 Jaeger 是否收到 trace
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:16686/api/services"
|
||||||
|
|
||||||
|
# 2. 检查 OTLP endpoint 是否可达
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:4318/v1/traces" -Method Post -ContentType "application/json" -Body "{}"
|
||||||
|
|
||||||
|
# 3. 检查服务环境变量
|
||||||
|
# 确保 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 已设置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 速查:常用命令
|
||||||
|
|
||||||
|
| 场景 | 命令 |
|
||||||
|
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 启动基础设施 | `docker compose -f infra/docker-compose.yml --profile p6 --profile observability up -d` |
|
||||||
|
| 一键启动应用 | `.\scripts\start-all.ps1` |
|
||||||
|
| 一键停止应用 | `.\scripts\stop-all.ps1` |
|
||||||
|
| 健康检查 | `.\scripts\health-check.ps1` |
|
||||||
|
| CDC 链路验证 | `.\scripts\test-cdc.ps1` |
|
||||||
|
| 查看容器状态 | `docker ps --filter "name=edu-"` |
|
||||||
|
| 查看 Debezium 状态 | `Invoke-RestMethod http://localhost:8083/connectors/edu-mysql-source/status` |
|
||||||
|
| ClickHouse 查询 | `docker exec edu-clickhouse clickhouse-client --user default --password clickhouse -q "SELECT * FROM edu_analytics.student_dashboard_view LIMIT 10"` |
|
||||||
|
| Kafka topic 列表 | `docker exec edu-kafka kafka-topics --bootstrap-server localhost:9092 --list` |
|
||||||
|
| Prometheus 查询 | `Invoke-RestMethod "http://localhost:9090/api/v1/query?query=up"` |
|
||||||
@@ -91,26 +91,26 @@
|
|||||||
|
|
||||||
### 1.6 可观测性(OTel + Prometheus + Loki)
|
### 1.6 可观测性(OTel + Prometheus + Loki)
|
||||||
|
|
||||||
| 场景 | 技术/规则 |
|
| 场景 | 技术/规则 |
|
||||||
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| P1 最小可观测集 | 每服务结构化日志 + `/metrics` + OTel SDK 初始化(不引入完整后端) |
|
| P1 最小可观测集 | 每服务结构化日志 + `/metrics` + OTel SDK 初始化(不引入完整后端) |
|
||||||
| 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) |
|
| 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) |
|
||||||
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
|
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
|
||||||
| P6 完整后端 | Loki(日志)+ Grafana(仪表盘)+ Jaeger(trace)+ Prometheus(metrics) |
|
| P6 完整后端 | Loki(日志)+ Grafana(仪表盘)+ Jaeger(trace)+ Prometheus(metrics) |
|
||||||
| Prometheus 指标 | `http_request_duration_seconds`(Histogram)+ `http_requests_total`(Counter) |
|
| Prometheus 指标 | `http_request_duration_seconds`(Histogram)+ `http_requests_total`(Counter) |
|
||||||
| 采样策略 | P1 全量 trace,P6 引入采样率降低开销 |
|
| 采样策略 | P1 全量 trace,P6 引入采样率降低开销 |
|
||||||
| 日志参数顺序 | `log.error({ err: error, userId, traceId }, "操作描述")`,错误对象字段名用 `err` |
|
| 日志参数顺序 | `log.error({ err: error, userId, traceId }, "操作描述")`,错误对象字段名用 `err` |
|
||||||
| P6 /metrics 端点 | NestJS 用 `app.getHttpAdapter().get('/metrics', handler)`,不能用 `app.get()`(会被解析为 DI 容器 get) |
|
| P6 /metrics 端点 | NestJS 用 `app.getHttpAdapter().get('/metrics', handler)`,不能用 `app.get()`(会被解析为 DI 容器 get) |
|
||||||
| P6 prometheus.yml | 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身;rule_files 引用 rules.yml;alerting 关联 alertmanager |
|
| P6 prometheus.yml | 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身;rule_files 引用 rules.yml;alerting 关联 alertmanager |
|
||||||
| P6 Grafana 数据源 | provisioning/datasources 同时声明 Prometheus(默认)和 Loki(uid=loki) |
|
| P6 Grafana 数据源 | provisioning/datasources 同时声明 Prometheus(默认)和 Loki(uid=loki) |
|
||||||
| P6 Promtail 采集 | docker_sd_configs + relabel_configs 仅采集 `edu-*` 前缀容器日志,避免无关日志 |
|
| P6 Promtail 采集 | docker_sd_configs + relabel_configs 仅采集 `edu-*` 前缀容器日志,避免无关日志 |
|
||||||
| P6 service label | static_configs.labels.service 给所有抓取目标打服务标签,告警规则按 service 聚合 |
|
| P6 service label | static_configs.labels.service 给所有抓取目标打服务标签,告警规则按 service 聚合 |
|
||||||
| P6 collectDefaultMetrics | prom-client 的 `collectDefaultMetrics({ register })` 自动收集进程级指标(CPU/内存/事件循环/GC),无需业务埋点即可让 /metrics 有数据 |
|
| P6 collectDefaultMetrics | prom-client 的 `collectDefaultMetrics({ register })` 自动收集进程级指标(CPU/内存/事件循环/GC),无需业务埋点即可让 /metrics 有数据 |
|
||||||
| P6 Counter/Histogram 埋点缺失 | metrics.ts 定义了 Counter/Histogram 但 service/controller 未调用 `.inc()`/`.observe()`,需后续补 HTTP 中间件自动埋点或业务埋点 |
|
| P6 Counter/Histogram 埋点缺失 | metrics.ts 定义了 Counter/Histogram 但 service/controller 未调用 `.inc()`/`.observe()`,需后续补 HTTP 中间件自动埋点或业务埋点 |
|
||||||
| P6 OTel instrumentations 缺失 | tracer.ts 只配置 traceExporter 未注册 auto-instrumentations(HttpInstrumentation/ExpressInstrumentation),导致 Jaeger 收不到业务 trace,需后续补 `@opentelemetry/auto-instrumentations` |
|
| P6 OTel instrumentations 缺失 | tracer.ts 只配置 traceExporter 未注册 auto-instrumentations(HttpInstrumentation/ExpressInstrumentation),导致 Jaeger 收不到业务 trace,需后续补 `@opentelemetry/auto-instrumentations`。**已补全**:NestJS 6 服务用 `getNodeAutoInstrumentations()`;Python 2 服务用 `FastAPIInstrumentor.instrument_app(app)`;Go 2 服务用 `otelgin.Middleware()` |
|
||||||
| P6 镜像源配置 | 国内 docker.io 被墙,compose image 必须加 `docker.m.daocloud.io/` 前缀;Elastic 官方镜像在 docker.elastic.co 不被墙 |
|
| P6 镜像源配置 | 国内 docker.io 被墙,compose image 必须加 `docker.m.daocloud.io/` 前缀;Elastic 官方镜像在 docker.elastic.co 不被墙 |
|
||||||
| P6 Grafana 端口冲突 | Grafana 默认 3000 与 teacher-portal Next.js dev 冲突,改映射为 3030:3000 |
|
| P6 Grafana 端口冲突 | Grafana 默认 3000 与 teacher-portal Next.js dev 冲突,改映射为 3030:3000 |
|
||||||
| P6 compose --no-deps | mysql/redis 已在另一 compose 项目运行时,启动新服务用 `--no-deps` + 显式指定服务名,避免重建依赖容器 | |
|
| P6 compose --no-deps | mysql/redis 已在另一 compose 项目运行时,启动新服务用 `--no-deps` + 显式指定服务名,避免重建依赖容器 | |
|
||||||
|
|
||||||
### 1.7 微前端 Module Federation
|
### 1.7 微前端 Module Federation
|
||||||
|
|
||||||
@@ -187,22 +187,25 @@
|
|||||||
|
|
||||||
### 2.2 classes(TS/NestJS,P1 黄金模板)
|
### 2.2 classes(TS/NestJS,P1 黄金模板)
|
||||||
|
|
||||||
| 场景 | 技术/规则 |
|
| 场景 | 技术/规则 |
|
||||||
| ---------------- | ------------------------------------------------------------------------------------------------ |
|
| ---------------- | -------------------------------------------------------------------------------------------------- |
|
||||||
| 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 |
|
| 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 |
|
||||||
| 黄金模板复制流程 | `cp -r services/classes services/xxx` → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI |
|
| 黄金模板复制流程 | `cp -r services/classes services/xxx` → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI |
|
||||||
| 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile |
|
| 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile |
|
||||||
| 错误处理 | `ApplicationError` 基类 + 子类,错误码 `CLASSES_*` 前缀 |
|
| 错误处理 | `ApplicationError` 基类 + 子类,错误码 `CLASSES_*` 前缀 |
|
||||||
| 可观测 | pino logger + prom-client metrics + OTel tracer |
|
| 可观测 | pino logger + prom-client metrics + OTel tracer |
|
||||||
| 安全 | auth.middleware(信任 Gateway 头)+ permission.guard + data-scope.interceptor |
|
| 安全 | auth.middleware(信任 Gateway 头)+ permission.guard + data-scope.interceptor |
|
||||||
| 配置 | 三层配置 + Zod 校验 env |
|
| 配置 | 三层配置 + Zod 校验 env |
|
||||||
| i18n | `ERROR_CODES` 映射表(错误码 → i18n key) |
|
| i18n | `ERROR_CODES` 映射表(错误码 → i18n key) |
|
||||||
| 测试四类 | 单元(vitest)+ 集成(Testcontainers)+ 契约(Pact)+ E2E(Playwright) |
|
| 测试四类 | 单元(vitest)+ 集成(Testcontainers)+ 契约(Pact)+ E2E(Playwright) |
|
||||||
| 覆盖率门槛 | 领域逻辑 ≥ 80%,Handler ≥ 60%,整体 ≥ 60% |
|
| 覆盖率门槛 | 领域逻辑 ≥ 80%,Handler ≥ 60%,整体 ≥ 60% |
|
||||||
| Drizzle schema | `mysqlTable` + `varchar`/`timestamp` + `index` |
|
| Drizzle schema | `mysqlTable` + `varchar`/`timestamp` + `index` |
|
||||||
| ID 生成 | `@paralleldrive/cuid2` 的 `createId()` |
|
| ID 生成 | `@paralleldrive/cuid2` 的 `createId()` |
|
||||||
| 响应转换 | repository 返回 Date,service 转换为 `createdAt: number`(时间戳) |
|
| 响应转换 | repository 返回 Date,service 转换为 `createdAt: number`(时间戳) |
|
||||||
| 阶段特有模式回写 | Outbox(P3)/ CDC(P4)/ 长连接(P5)实现后回写黄金模板 README |
|
| 阶段特有模式回写 | Outbox(P3)/ CDC(P4)/ 长连接(P5)实现后回写黄金模板 README |
|
||||||
|
| 健康检查依赖 | `readyz` 用 Drizzle `getDb().execute(sql\`SELECT 1\`)` 校验,不要依赖 typeorm DataSource DI |
|
||||||
|
| AppModule 注册 | HealthModule 必须在 `app.module.ts` imports 数组显式声明,否则 NestFactory 不扫描 HealthController |
|
||||||
|
| 增量编译陷阱 | `tsconfig.json` 显式 `"incremental": false` 覆盖 base,避免 .tsbuildinfo 导致 nest watch 不 emit |
|
||||||
|
|
||||||
### 2.3 iam(TS/NestJS,P2)
|
### 2.3 iam(TS/NestJS,P2)
|
||||||
|
|
||||||
@@ -350,6 +353,8 @@
|
|||||||
|
|
||||||
| 日期 | 时间 | 模块 | 做了什么 + 学到什么 |
|
| 日期 | 时间 | 模块 | 做了什么 + 学到什么 |
|
||||||
| ---------- | ---- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------- | ---- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 2026-07-09 | 下午 | classes/全局 | **一键启动脚本 NestJS dist/ 不生成根因定位 + classes 健康检查修复**:(1) 根因定位:`tsconfig.base.json` 的 `incremental: true` + `nest-cli.json` 的 `deleteOutDir: true` 冲突。`nest start --watch` 启动时先删除 dist/,tsc 读残留 .tsbuildinfo 认为无变化跳过 emit,dist/ 不生成 → `Cannot find module dist/main`。(2) 修复:6 个 NestJS 服务(classes/iam/teacher-bff/core-edu/content/msg)tsconfig.json 显式加 `"incremental": false` 覆盖 base 配置,删除所有残留 .tsbuildinfo 文件。(3) classes AppModule 缺 HealthModule 导入导致 /healthz 404,iam 同样问题,修复 app.module.ts 加 `imports: [..., HealthModule]`。(4) classes HealthController 误用 TypeORM `DataSource` DI(与 iam 不一致),运行时报 `Nest can't resolve dependencies of the HealthController (DataSource)`。修复:改为 Drizzle `getDb()` 函数式调用,与 iam 一致。(5) 一键启动验证:11/11 应用 + 11/11 基础设施 + 5/5 可观测性端点全绿。**学到**:NestJS + TypeScript incremental 编译是陷阱组合——nest-cli deleteOutDir 删 dist 但 tsc 读 tsbuildinfo 认为无变化,必须在服务级 tsconfig 显式 `incremental: false`;HealthModule 必须在 AppModule imports 中显式声明才能被 NestFactory 扫描到;5 个 NestJS 服务的 HealthController 应统一用 Drizzle `getDb()` 函数式调用而非 TypeORM DataSource DI(项目已弃 TypeORM 改 Drizzle)。 |
|
||||||
|
| 2026-07-09 | 下午 | 全局 | **OTel auto-instrumentations 全服务补全**:(1) NestJS 6 服务(iam/classes/core-edu/content/msg/teacher-bff)tracer.ts 补 `getNodeAutoInstrumentations()`,NodeSDK 传 instrumentations 参数自动埋点 HTTP/Express/DB。(2) Python 2 服务(data-ana/ai)main.py 补 `FastAPIInstrumentor.instrument_app(app)`;ai 补缺失的 `opentelemetry-exporter-otlp` 依赖。(3) teacher-bff 从零补完整 OTel:env.ts 加 OTEL_EXPORTER_OTLP_ENDPOINT 字段 + 新建 shared/observability/tracer.ts + main.ts 调用 initTracer/shutdownTracer + package.json 加 sdk-node/exporter/auto-instrumentations 依赖。(4) Go 2 服务(api-gateway/push-gateway)新建 internal/observability/tracer.go(OTLP HTTP exporter + resource + TracerProvider + W3C propagator)+ main.go 调用 InitTracer + otelgin.Middleware 注册 Gin 中间件;push-gateway config.go 补 OTLPEndpoint 字段。(5) 质量校验全通过:TS typecheck 9 服务 + ESLint 6 服务 + ruff 2 服务 + go vet/build 2 服务零错误。**学到**:`getNodeAutoInstrumentations()` 一次注册所有 Node.js 自动埋点(http/express/dns/fs/net/grpc 等),比手动逐个注册 HttpInstrumentation 更简洁;Go OTel 用 `otlptracehttp.WithEndpoint(host)` + `WithInsecure()` 需从 "http://host:port" URL 解析出 host;otelgin.Middleware 必须在 Recovery 之后其他中间件之前注册,确保所有后续 handler 都被 trace;Python FastAPIInstrumentor.instrument_app(app) 在 app 创建后立即调用,lifespan 不受影响。 |
|
||||||
| 2026-07-09 | 下午 | 全局 | **P6 硬化:可观测性 + 部署 + CI 硬化**:(1) 可观测性栈完善:5 个 NestJS 服务 main.ts 添加 `/metrics` Prometheus 端点(用 `app.getHttpAdapter().get('/metrics', ...)` 绕过 DI 容器 get 方法);prometheus.yml 从 2 个目标扩展到 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身 + rule_files + alertmanager 关联;monitoring compose 用 Loki + Promtail 替换未配置的 blackbox-exporter;Grafana datasource 新增 Loki;新建 promtail/config.yml 用 docker_sd_configs 仅采集 `edu-*` 容器日志。(2) 部署 compose 扩展:docker-compose.deploy.yml 从 3 服务扩展到 11 服务(+ iam/teacher-bff/core-edu/content/msg/ai/data-ana/push-gateway),每个服务带 healthcheck + depends_on 条件 + edu-net/edu-shared 双网络;deploy.env.example 补全 Neo4j/ES/ClickHouse/LLM/Kafka 可选依赖配置。(3) teacher-bff 补 health.controller.ts(原缺失 /healthz 导致 deploy depends_on service_healthy 失败)。(4) CI 硬化:移除 lint 步骤的 continue-on-error(ESLint 9 flat config 已配置完成),test 保留 continue-on-error(部分服务无 test 脚本)。**学到**:NestJS `app.get('/metrics')` 会被解析为 DI 容器 `get(typeOrToken)`,必须用 `app.getHttpAdapter().get()` 才能注册 Express 路由;Promtail docker_sd_configs 通过 relabel_configs 的 `regex: '/(edu-.*).*'` 过滤容器名前缀;docker-compose.depends_on.condition: service_healthy 要求被依赖服务必须有 healthcheck 配置,否则启动失败。 |
|
| 2026-07-09 | 下午 | 全局 | **P6 硬化:可观测性 + 部署 + CI 硬化**:(1) 可观测性栈完善:5 个 NestJS 服务 main.ts 添加 `/metrics` Prometheus 端点(用 `app.getHttpAdapter().get('/metrics', ...)` 绕过 DI 容器 get 方法);prometheus.yml 从 2 个目标扩展到 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身 + rule_files + alertmanager 关联;monitoring compose 用 Loki + Promtail 替换未配置的 blackbox-exporter;Grafana datasource 新增 Loki;新建 promtail/config.yml 用 docker_sd_configs 仅采集 `edu-*` 容器日志。(2) 部署 compose 扩展:docker-compose.deploy.yml 从 3 服务扩展到 11 服务(+ iam/teacher-bff/core-edu/content/msg/ai/data-ana/push-gateway),每个服务带 healthcheck + depends_on 条件 + edu-net/edu-shared 双网络;deploy.env.example 补全 Neo4j/ES/ClickHouse/LLM/Kafka 可选依赖配置。(3) teacher-bff 补 health.controller.ts(原缺失 /healthz 导致 deploy depends_on service_healthy 失败)。(4) CI 硬化:移除 lint 步骤的 continue-on-error(ESLint 9 flat config 已配置完成),test 保留 continue-on-error(部分服务无 test 脚本)。**学到**:NestJS `app.get('/metrics')` 会被解析为 DI 容器 `get(typeOrToken)`,必须用 `app.getHttpAdapter().get()` 才能注册 Express 路由;Promtail docker_sd_configs 通过 relabel_configs 的 `regex: '/(edu-.*).*'` 过滤容器名前缀;docker-compose.depends_on.condition: service_healthy 要求被依赖服务必须有 healthcheck 配置,否则启动失败。 |
|
||||||
| 2026-07-09 | 下午 | data-ana/infra | **CDC 完整链路实现**:MySQL binlog → Debezium Connect → Kafka → data-ana 消费者 → ClickHouse 宽表。(1) MySQL binlog 配置:log_bin=ON, binlog_format=ROW, binlog_row_image=FULL, server_id=1;用 root 创建 `debezium` 用户授予 REPLICATION SLAVE + REPLICATION CLIENT。(2) Debezium Connect 容器:daocloud 禁用 debezium 镜像改用 `quay.io/debezium/connect:2.7`;MySQL 容器在 edu-minimal_default 网络,需 `docker network connect edu-full_default edu-mysql` 让 Debezium 同时可达;Kafka 必须配置双 listener(INSIDE:kafka:29092 + OUTSIDE:localhost:9092),否则 Debezium 拿到 advertised.listeners 中的 localhost metadata 后切换失败;Debezium 2.x 容器环境变量名用 BOOTSTRAP_SERVERS(不带 KAFKA_ 前缀),通过 envsubst 替换到 connect-distributed.properties。(3) 注册 connector:POST :8083/connectors,配置 topic.prefix=edu-cdc, database.include.list=next_edu_cloud, snapshot.mode=initial,4 张表(core_edu_grades/exams/classes/iam_users)成功产生快照事件。(4) data-ana 消费者实现:新建 cdc_consumer.py 用 aiokafka AIOKafkaConsumer,lifespan 中 asyncio.create_task 后台运行;按 source.table 路由(exams→内存缓存 exam_id→class_id 映射,grades→查缓存填 class_id 后 upsert ClickHouse);readyz 端点附加 cdc_consumer 状态。(5) ClickHouse 远程访问:默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 `clickhouse/users.d/custom-users.xml` 覆盖密码+任意 IP。(6) structlog 24.x API:`make_filtering_bound_logger(level)` 替代废弃的 `make_filtering_logger`。(7) E2E 验证:MySQL INSERT 成绩 → Debezium op=c 事件 → Kafka → 消费者写 ClickHouse 宽表(class_id 通过 exam 缓存正确填充)→ /readyz cdc_consumer=running → /analytics/student/student-002/weakness 返回实时 92 分数据。**学到**:Debezium 2.x 容器 bootstrap.servers 默认值是 0.0.0.0:9092 必须显式覆盖;Kafka 单 listener 配置 localhost 会让容器间通信的客户端拿到 metadata 后切换失败,必须用双 listener;ClickHouse users_xml 存储是 readonly 不能用 ALTER USER 修改密码,必须挂载 users.d 配置文件覆盖;消费者 offset 重置必须先停消费者让 group 处于 Empty 状态才能执行 --reset-offsets。 |
|
| 2026-07-09 | 下午 | data-ana/infra | **CDC 完整链路实现**:MySQL binlog → Debezium Connect → Kafka → data-ana 消费者 → ClickHouse 宽表。(1) MySQL binlog 配置:log_bin=ON, binlog_format=ROW, binlog_row_image=FULL, server_id=1;用 root 创建 `debezium` 用户授予 REPLICATION SLAVE + REPLICATION CLIENT。(2) Debezium Connect 容器:daocloud 禁用 debezium 镜像改用 `quay.io/debezium/connect:2.7`;MySQL 容器在 edu-minimal_default 网络,需 `docker network connect edu-full_default edu-mysql` 让 Debezium 同时可达;Kafka 必须配置双 listener(INSIDE:kafka:29092 + OUTSIDE:localhost:9092),否则 Debezium 拿到 advertised.listeners 中的 localhost metadata 后切换失败;Debezium 2.x 容器环境变量名用 BOOTSTRAP_SERVERS(不带 KAFKA_ 前缀),通过 envsubst 替换到 connect-distributed.properties。(3) 注册 connector:POST :8083/connectors,配置 topic.prefix=edu-cdc, database.include.list=next_edu_cloud, snapshot.mode=initial,4 张表(core_edu_grades/exams/classes/iam_users)成功产生快照事件。(4) data-ana 消费者实现:新建 cdc_consumer.py 用 aiokafka AIOKafkaConsumer,lifespan 中 asyncio.create_task 后台运行;按 source.table 路由(exams→内存缓存 exam_id→class_id 映射,grades→查缓存填 class_id 后 upsert ClickHouse);readyz 端点附加 cdc_consumer 状态。(5) ClickHouse 远程访问:默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 `clickhouse/users.d/custom-users.xml` 覆盖密码+任意 IP。(6) structlog 24.x API:`make_filtering_bound_logger(level)` 替代废弃的 `make_filtering_logger`。(7) E2E 验证:MySQL INSERT 成绩 → Debezium op=c 事件 → Kafka → 消费者写 ClickHouse 宽表(class_id 通过 exam 缓存正确填充)→ /readyz cdc_consumer=running → /analytics/student/student-002/weakness 返回实时 92 分数据。**学到**:Debezium 2.x 容器 bootstrap.servers 默认值是 0.0.0.0:9092 必须显式覆盖;Kafka 单 listener 配置 localhost 会让容器间通信的客户端拿到 metadata 后切换失败,必须用双 listener;ClickHouse users_xml 存储是 readonly 不能用 ALTER USER 修改密码,必须挂载 users.d 配置文件覆盖;消费者 offset 重置必须先停消费者让 group 处于 Empty 状态才能执行 --reset-offsets。 |
|
||||||
| 2026-07-09 | 下午 | 全局 | **P6 硬化:ESLint 9 flat config 配置**:(1) 根目录创建 `eslint.config.js`(ESLint 9 flat config 格式):用 `typescript-eslint` recommended 规则集 + `@eslint/js` recommended + `eslint-config-prettier` 禁用冲突规则;自定义规则:`no-explicit-any` warn + `no-unused-vars` 允许下划线前缀 + 测试文件放宽。(2) 6 个 TS 服务 package.json lint 脚本从 `eslint src --ext .ts` 改为 `eslint src`(flat config 不需要 --ext)。(3) `lint-staged.config.js` 恢复 `eslint --fix`。(4) 验证:classes/content/msg/core-edu 四服务 lint 全部零错误零警告通过。**学到**:ESLint 9 flat config 用 `tseslint.config()` 工厂函数组装配置数组;`--ext` 参数在 flat config 模式下被移除,ESLint 自动根据 `eslint.config.js` 中的 `files` 匹配;`@typescript-eslint/consistent-type-assertions` 规则选项格式在 v8 中变化(`objectLiteralType` → `objectLiteralTypeAssertions`),配置时需查最新文档。 |
|
| 2026-07-09 | 下午 | 全局 | **P6 硬化:ESLint 9 flat config 配置**:(1) 根目录创建 `eslint.config.js`(ESLint 9 flat config 格式):用 `typescript-eslint` recommended 规则集 + `@eslint/js` recommended + `eslint-config-prettier` 禁用冲突规则;自定义规则:`no-explicit-any` warn + `no-unused-vars` 允许下划线前缀 + 测试文件放宽。(2) 6 个 TS 服务 package.json lint 脚本从 `eslint src --ext .ts` 改为 `eslint src`(flat config 不需要 --ext)。(3) `lint-staged.config.js` 恢复 `eslint --fix`。(4) 验证:classes/content/msg/core-edu 四服务 lint 全部零错误零警告通过。**学到**:ESLint 9 flat config 用 `tseslint.config()` 工厂函数组装配置数组;`--ext` 参数在 flat config 模式下被移除,ESLint 自动根据 `eslint.config.js` 中的 `files` 匹配;`@typescript-eslint/consistent-type-assertions` 规则选项格式在 v8 中变化(`objectLiteralType` → `objectLiteralTypeAssertions`),配置时需查最新文档。 |
|
||||||
|
|||||||
@@ -184,8 +184,14 @@ services:
|
|||||||
container_name: edu-prometheus
|
container_name: edu-prometheus
|
||||||
profiles: ["observability"]
|
profiles: ["observability"]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||||
|
- "--storage.tsdb.path=/prometheus"
|
||||||
|
- "--storage.tsdb.retention.time=15d"
|
||||||
|
- "--web.enable-lifecycle"
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- prometheus_data:/prometheus
|
||||||
ports:
|
ports:
|
||||||
- "9090:9090"
|
- "9090:9090"
|
||||||
grafana:
|
grafana:
|
||||||
@@ -199,6 +205,49 @@ services:
|
|||||||
- "3030:3000"
|
- "3030:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- grafana_data:/var/lib/grafana
|
- grafana_data:/var/lib/grafana
|
||||||
|
# ============================================================
|
||||||
|
# Exporters(observability profile,与 Prometheus 同网络)
|
||||||
|
# ============================================================
|
||||||
|
node-exporter:
|
||||||
|
image: docker.m.daocloud.io/prom/node-exporter:v1.8.2
|
||||||
|
container_name: edu-node-exporter
|
||||||
|
profiles: ["observability"]
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- "--path.rootfs=/host"
|
||||||
|
ports:
|
||||||
|
- "9100:9100"
|
||||||
|
volumes:
|
||||||
|
- /proc:/host/proc:ro
|
||||||
|
- /sys:/host/sys:ro
|
||||||
|
- /:/host:ro
|
||||||
|
mysql-exporter:
|
||||||
|
image: docker.m.daocloud.io/prom/mysqld-exporter:v0.15.1
|
||||||
|
container_name: edu-mysql-exporter
|
||||||
|
profiles: ["observability"]
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- "--mysqld.address=edu-mysql:3306"
|
||||||
|
- "--mysqld.username=edu:changeme"
|
||||||
|
environment:
|
||||||
|
MYSQLD_EXPORTER_PASSWORD: "changeme"
|
||||||
|
ports:
|
||||||
|
- "9104:9104"
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis-exporter:
|
||||||
|
image: docker.m.daocloud.io/oliver006/redis_exporter:v1.67.0
|
||||||
|
container_name: edu-redis-exporter
|
||||||
|
profiles: ["observability"]
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
REDIS_ADDR: "redis://edu-redis:6379"
|
||||||
|
ports:
|
||||||
|
- "9121:9121"
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
@@ -206,3 +255,4 @@ volumes:
|
|||||||
neo4j_data:
|
neo4j_data:
|
||||||
es_data:
|
es_data:
|
||||||
grafana_data:
|
grafana_data:
|
||||||
|
prometheus_data:
|
||||||
|
|||||||
@@ -89,6 +89,6 @@ scrape_configs:
|
|||||||
|
|
||||||
- job_name: 'node-exporter'
|
- job_name: 'node-exporter'
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['node-exporter:9100']
|
- targets: ['host.docker.internal:9100']
|
||||||
labels:
|
labels:
|
||||||
service: node-exporter
|
service: node-exporter
|
||||||
|
|||||||
1069
pnpm-lock.yaml
generated
1069
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
156
scripts/health-check.ps1
Normal file
156
scripts/health-check.ps1
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Edu health check script
|
||||||
|
.DESCRIPTION
|
||||||
|
Checks health of all infrastructure containers + application services + observability endpoints
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\health-check.ps1
|
||||||
|
#>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Edu Health Check" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 1. Infrastructure containers =====
|
||||||
|
Write-Host "[1/3] Infrastructure Containers" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$infraContainers = @(
|
||||||
|
@{Name="MySQL"; Container="edu-mysql"},
|
||||||
|
@{Name="Redis"; Container="edu-redis"},
|
||||||
|
@{Name="Kafka"; Container="edu-kafka"},
|
||||||
|
@{Name="Zookeeper"; Container="edu-zookeeper"},
|
||||||
|
@{Name="ClickHouse"; Container="edu-clickhouse"},
|
||||||
|
@{Name="Debezium"; Container="edu-debezium"},
|
||||||
|
@{Name="Neo4j"; Container="edu-neo4j"},
|
||||||
|
@{Name="Elasticsearch"; Container="edu-es"},
|
||||||
|
@{Name="Jaeger"; Container="edu-jaeger"},
|
||||||
|
@{Name="Prometheus"; Container="edu-prometheus"},
|
||||||
|
@{Name="Grafana"; Container="edu-grafana"}
|
||||||
|
)
|
||||||
|
|
||||||
|
$infraOk = 0
|
||||||
|
$infraFail = 0
|
||||||
|
foreach ($svc in $infraContainers) {
|
||||||
|
$running = docker inspect -f '{{.State.Running}}' $svc.Container 2>$null
|
||||||
|
if ($running -ne "true") {
|
||||||
|
Write-Host " [FAIL] $($svc.Name) not running" -ForegroundColor Red
|
||||||
|
$infraFail++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
# 条件模板:Health 不存在时返回空字符串,不报错
|
||||||
|
$health = docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' $svc.Container 2>$null
|
||||||
|
if ($health -eq "healthy") {
|
||||||
|
Write-Host " [OK] $($svc.Name)" -ForegroundColor Green
|
||||||
|
$infraOk++
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARN] $($svc.Name) (running, no healthcheck)" -ForegroundColor Yellow
|
||||||
|
$infraOk++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 2. Application services =====
|
||||||
|
Write-Host "[2/3] Application Services" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$appServices = @(
|
||||||
|
@{Name="classes"; Url="http://localhost:3001/healthz"},
|
||||||
|
@{Name="iam"; Url="http://localhost:3002/healthz"},
|
||||||
|
@{Name="teacher-bff"; Url="http://localhost:3003/healthz"},
|
||||||
|
@{Name="core-edu"; Url="http://localhost:3004/healthz"},
|
||||||
|
@{Name="content"; Url="http://localhost:3005/healthz"},
|
||||||
|
@{Name="data-ana"; Url="http://localhost:3006/healthz"},
|
||||||
|
@{Name="msg"; Url="http://localhost:3007/healthz"},
|
||||||
|
@{Name="ai"; Url="http://localhost:3008/healthz"},
|
||||||
|
@{Name="api-gateway"; Url="http://localhost:8080/healthz"},
|
||||||
|
@{Name="push-gateway"; Url="http://localhost:8081/healthz"},
|
||||||
|
@{Name="teacher-portal";Url="http://localhost:3000/"}
|
||||||
|
)
|
||||||
|
|
||||||
|
$appOk = 0
|
||||||
|
$appFail = 0
|
||||||
|
foreach ($svc in $appServices) {
|
||||||
|
try {
|
||||||
|
$null = Invoke-RestMethod -Uri $svc.Url -Method Get -TimeoutSec 3 -ErrorAction Stop
|
||||||
|
Write-Host " [OK] $($svc.Name)" -ForegroundColor Green
|
||||||
|
$appOk++
|
||||||
|
} catch {
|
||||||
|
Write-Host " [FAIL] $($svc.Name)" -ForegroundColor Red
|
||||||
|
$appFail++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 3. Observability endpoints =====
|
||||||
|
Write-Host "[3/3] Observability Endpoints" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$obsEndpoints = @(
|
||||||
|
@{Name="Prometheus API"; Url="http://localhost:9090/api/v1/query?query=up"},
|
||||||
|
@{Name="Jaeger API"; Url="http://localhost:16686/api/services"},
|
||||||
|
@{Name="Debezium Connect"; Url="http://localhost:8083/connectors"},
|
||||||
|
@{Name="data-ana /metrics";Url="http://localhost:3006/metrics"},
|
||||||
|
@{Name="iam /metrics"; Url="http://localhost:3002/metrics"}
|
||||||
|
)
|
||||||
|
|
||||||
|
$obsOk = 0
|
||||||
|
$obsFail = 0
|
||||||
|
foreach ($ep in $obsEndpoints) {
|
||||||
|
try {
|
||||||
|
$null = Invoke-RestMethod -Uri $ep.Url -Method Get -TimeoutSec 3 -ErrorAction Stop
|
||||||
|
Write-Host " [OK] $($ep.Name)" -ForegroundColor Green
|
||||||
|
$obsOk++
|
||||||
|
} catch {
|
||||||
|
Write-Host " [FAIL] $($ep.Name)" -ForegroundColor Red
|
||||||
|
$obsFail++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== 4. CDC pipeline status =====
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[Extra] CDC Pipeline Status" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$connectorStatus = $null
|
||||||
|
try {
|
||||||
|
$connectorStatus = Invoke-RestMethod -Uri "http://localhost:8083/connectors/edu-mysql-source/status" -Method Get -TimeoutSec 3 -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
Write-Host " [FAIL] Debezium connector not registered or error" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($connectorStatus) {
|
||||||
|
$connectorState = $connectorStatus.connector.state
|
||||||
|
$tasks = @($connectorStatus.tasks)
|
||||||
|
if ($tasks.Count -gt 0) {
|
||||||
|
$taskState = $tasks[0].state
|
||||||
|
} else {
|
||||||
|
$taskState = "UNKNOWN"
|
||||||
|
}
|
||||||
|
if ($connectorState -eq "RUNNING" -and $taskState -eq "RUNNING") {
|
||||||
|
$color = "Green"
|
||||||
|
} else {
|
||||||
|
$color = "Yellow"
|
||||||
|
}
|
||||||
|
Write-Host " Connector: $connectorState / Task: $taskState" -ForegroundColor $color
|
||||||
|
}
|
||||||
|
|
||||||
|
$chSql = 'SELECT count(*) FROM edu_analytics.student_dashboard_view'
|
||||||
|
$chOutput = docker exec edu-clickhouse clickhouse-client --user default --password clickhouse -q $chSql 2>&1
|
||||||
|
$chCount = "$chOutput".Trim()
|
||||||
|
if ($chCount -match '^\d+$') {
|
||||||
|
Write-Host " ClickHouse student_dashboard_view: $chCount records" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " ClickHouse query failed or empty" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== Summary =====
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Infra: $infraOk/$($infraContainers.Count) | App: $appOk/$($appServices.Count) | Obs: $obsOk/$($obsEndpoints.Count)" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
if ($infraFail -gt 0 -or $appFail -gt 0 -or $obsFail -gt 0) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
301
scripts/start-all.ps1
Normal file
301
scripts/start-all.ps1
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Edu start all application services script
|
||||||
|
.DESCRIPTION
|
||||||
|
Starts 10 application services + 1 frontend (teacher-portal)
|
||||||
|
Each service runs in a separate terminal window for log visibility
|
||||||
|
Automatically injects Python service env vars (CLICKHOUSE/KAFKA/OTEL)
|
||||||
|
Prerequisite: infrastructure containers (MySQL/Redis/Kafka/ClickHouse etc) must be running
|
||||||
|
.PARAMETER SkipInfraCheck
|
||||||
|
Skip infrastructure health check (use when infra is known to be running)
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\start-all.ps1
|
||||||
|
.\scripts\start-all.ps1 -SkipInfraCheck
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[switch]$SkipInfraCheck,
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Edu Start All Services" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 1. Infrastructure health check =====
|
||||||
|
if (-not $SkipInfraCheck) {
|
||||||
|
Write-Host "[1/6] Checking infrastructure health..." -ForegroundColor Yellow
|
||||||
|
$infraServices = @(
|
||||||
|
@{Name="MySQL"; Container="edu-mysql"},
|
||||||
|
@{Name="Redis"; Container="edu-redis"},
|
||||||
|
@{Name="Kafka"; Container="edu-kafka"},
|
||||||
|
@{Name="ClickHouse"; Container="edu-clickhouse"},
|
||||||
|
@{Name="Debezium"; Container="edu-debezium"},
|
||||||
|
@{Name="Jaeger"; Container="edu-jaeger"}
|
||||||
|
)
|
||||||
|
$allHealthy = $true
|
||||||
|
$prevEAP = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
foreach ($svc in $infraServices) {
|
||||||
|
$running = docker inspect -f '{{.State.Running}}' $svc.Container 2>$null
|
||||||
|
if ($running -ne "true") {
|
||||||
|
Write-Host " [FAIL] $($svc.Name) not running" -ForegroundColor Red
|
||||||
|
$allHealthy = $false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
# 条件模板:Health 不存在时返回空字符串,不报错
|
||||||
|
$health = docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' $svc.Container 2>$null
|
||||||
|
if ($health -eq "healthy") {
|
||||||
|
Write-Host " [OK] $($svc.Name) ($($svc.Container))" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARN] $($svc.Name) running (no healthcheck)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ErrorActionPreference = $prevEAP
|
||||||
|
if (-not $allHealthy) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Infrastructure not ready. Start it first:" -ForegroundColor Red
|
||||||
|
Write-Host " docker compose -f infra/docker-compose.yml --profile p6 --profile observability up -d" -ForegroundColor White
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===== 2. Environment variables =====
|
||||||
|
Write-Host "[2/6] Preparing environment variables..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$env:DEV_MODE = "true"
|
||||||
|
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"
|
||||||
|
$env:DATABASE_URL = "mysql://edu:changeme@localhost:3306/next_edu_cloud"
|
||||||
|
$env:REDIS_URL = "redis://localhost:6379"
|
||||||
|
$env:JWT_SECRET = "p1-dev-secret-change-in-production"
|
||||||
|
$env:KAFKA_BROKERS = "localhost:9092"
|
||||||
|
|
||||||
|
$pyEnv = @{
|
||||||
|
CLICKHOUSE_HOST = "localhost"
|
||||||
|
CLICKHOUSE_PORT = "8123"
|
||||||
|
CLICKHOUSE_USER = "default"
|
||||||
|
CLICKHOUSE_PASSWORD = "clickhouse"
|
||||||
|
CLICKHOUSE_DATABASE = "edu_analytics"
|
||||||
|
KAFKA_BROKERS = "localhost:9092"
|
||||||
|
OTEL_ENDPOINT = "http://localhost:4318"
|
||||||
|
DEV_MODE = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " DEV_MODE=true (dev-token bypass)" -ForegroundColor Green
|
||||||
|
Write-Host " OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 3. Port conflict check =====
|
||||||
|
Write-Host "[3/6] Checking port conflicts..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$portMap = @{
|
||||||
|
3001="classes"; 3002="iam"; 3003="teacher-bff"; 3004="core-edu"
|
||||||
|
3005="content"; 3006="data-ana"; 3007="msg"; 3008="ai"
|
||||||
|
8080="api-gateway"; 8081="push-gateway"; 3000="teacher-portal"
|
||||||
|
}
|
||||||
|
|
||||||
|
$conflicts = @()
|
||||||
|
foreach ($port in $portMap.Keys | Sort-Object) {
|
||||||
|
$conn = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
||||||
|
if ($conn) {
|
||||||
|
$svcName = $portMap[$port]
|
||||||
|
$procId = $conn[0].OwningProcess
|
||||||
|
$procName = ""
|
||||||
|
try { $procName = (Get-Process -Id $procId -ErrorAction Stop).ProcessName } catch {}
|
||||||
|
Write-Host " [WARN] Port $port ($svcName) occupied by PID $procId ($procName)" -ForegroundColor Yellow
|
||||||
|
$conflicts += [PSCustomObject]@{Port=$port; Service=$svcName; PID=$procId; Process=$procName}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($conflicts.Count -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " $($conflicts.Count) port(s) already in use." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$shouldKill = $false
|
||||||
|
if ($Force) {
|
||||||
|
Write-Host " -Force specified, killing automatically..." -ForegroundColor White
|
||||||
|
$shouldKill = $true
|
||||||
|
} else {
|
||||||
|
Write-Host " These services may already be running. Options:" -ForegroundColor White
|
||||||
|
Write-Host " 1. Run .\scripts\stop-all.ps1 -KillByPort first, then re-run start-all" -ForegroundColor White
|
||||||
|
Write-Host " 2. Re-run with -Force to auto-kill and continue" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
$answer = Read-Host " Kill existing processes and continue? (y/N)"
|
||||||
|
if ($answer -eq "y" -or $answer -eq "Y") {
|
||||||
|
$shouldKill = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shouldKill) {
|
||||||
|
foreach ($c in $conflicts) {
|
||||||
|
try {
|
||||||
|
Stop-Process -Id $c.PID -Force -ErrorAction Stop
|
||||||
|
Write-Host " [OK] Killed PID $($c.PID) ($($c.Process)) on port $($c.Port)" -ForegroundColor Green
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] Cannot kill PID $($c.PID): $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
} else {
|
||||||
|
Write-Host " Aborting. Please stop existing services first." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] All app ports are free" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 4. Build NestJS services (required for nest start --watch) =====
|
||||||
|
Write-Host "[4/6] Building NestJS services (first time required)..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$nestjsServices = @(
|
||||||
|
"@edu/classes-service",
|
||||||
|
"@edu/iam-service",
|
||||||
|
"@edu/teacher-bff",
|
||||||
|
"@edu/core-edu-service",
|
||||||
|
"@edu/content-service",
|
||||||
|
"@edu/msg-service"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean tsbuildinfo cache (incremental mode leftover causes tsc to skip emit)
|
||||||
|
$nestjsDirs = @("classes", "iam", "teacher-bff", "core-edu", "content", "msg")
|
||||||
|
foreach ($dir in $nestjsDirs) {
|
||||||
|
$svcPath = Join-Path $ProjectRoot "services\$dir"
|
||||||
|
if (Test-Path $svcPath) {
|
||||||
|
Get-ChildItem -Path $svcPath -Filter "*.tsbuildinfo" -Recurse -ErrorAction SilentlyContinue |
|
||||||
|
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($svc in $nestjsServices) {
|
||||||
|
Write-Host " Building $svc..." -ForegroundColor Gray -NoNewline
|
||||||
|
$buildResult = pnpm --filter $svc build 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK]" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [FAIL]" -ForegroundColor Red
|
||||||
|
Write-Host " $buildResult" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 5. Start application services =====
|
||||||
|
Write-Host "[5/6] Starting application services (11 windows)..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$services = @(
|
||||||
|
@{Title="edu-app-classes"; Cmd="pnpm"; Args=@("--filter","@edu/classes-service","dev"); Dir="$ProjectRoot"},
|
||||||
|
@{Title="edu-app-iam"; Cmd="pnpm"; Args=@("--filter","@edu/iam-service","dev"); Dir="$ProjectRoot"},
|
||||||
|
@{Title="edu-app-teacher-bff"; Cmd="pnpm"; Args=@("--filter","@edu/teacher-bff","dev"); Dir="$ProjectRoot"},
|
||||||
|
@{Title="edu-app-core-edu"; Cmd="pnpm"; Args=@("--filter","@edu/core-edu-service","dev"); Dir="$ProjectRoot"},
|
||||||
|
@{Title="edu-app-content"; Cmd="pnpm"; Args=@("--filter","@edu/content-service","dev"); Dir="$ProjectRoot"},
|
||||||
|
@{Title="edu-app-msg"; Cmd="pnpm"; Args=@("--filter","@edu/msg-service","dev"); Dir="$ProjectRoot"},
|
||||||
|
@{Title="edu-app-data-ana"; Cmd="uv"; Args=@("run","uvicorn","data_ana.main:app","--app-dir","src","--host","0.0.0.0","--port","3006","--reload"); Dir="$ProjectRoot\services\data-ana"; PyEnv=$true},
|
||||||
|
@{Title="edu-app-ai"; Cmd="uv"; Args=@("run","uvicorn","ai.main:app","--app-dir","src","--host","0.0.0.0","--port","3008","--reload"); Dir="$ProjectRoot\services\ai"; PyEnv=$true},
|
||||||
|
@{Title="edu-app-api-gateway"; Cmd="go"; Args=@("run","."); Dir="$ProjectRoot\services\api-gateway"; GoEnv=$true},
|
||||||
|
@{Title="edu-app-push-gateway"; Cmd="go"; Args=@("run","."); Dir="$ProjectRoot\services\push-gateway";GoEnv=$true},
|
||||||
|
@{Title="edu-app-teacher-portal";Cmd="pnpm";Args=@("--filter","teacher-portal","dev"); Dir="$ProjectRoot"}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($svc in $services) {
|
||||||
|
$cmdStr = "$($svc.Cmd) $($svc.Args -join ' ')"
|
||||||
|
|
||||||
|
$psCmd = "Set-Location '$($svc.Dir)'; "
|
||||||
|
|
||||||
|
if ($svc.PyEnv) {
|
||||||
|
foreach ($kv in $pyEnv.GetEnumerator()) {
|
||||||
|
$psCmd += "`$env:$($kv.Key)='$($kv.Value)'; "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($svc.GoEnv) {
|
||||||
|
$psCmd += "`$env:Path = 'C:\Program Files\Go\bin;' + `$env:Path; "
|
||||||
|
}
|
||||||
|
|
||||||
|
$psCmd += "$cmdStr; Write-Host ''; Write-Host 'Service stopped. Press any key to close...' -ForegroundColor Yellow; `$null = `$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
|
||||||
|
|
||||||
|
Start-Process -FilePath "powershell" -ArgumentList "-NoExit","-Command",$psCmd -WindowStyle Normal | Out-Null
|
||||||
|
|
||||||
|
Write-Host " [START] $($svc.Title)..." -ForegroundColor Green
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " All services started in new windows. Waiting 40s for init..." -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds 40
|
||||||
|
|
||||||
|
# ===== 6. Health check =====
|
||||||
|
Write-Host "[6/6] Health check..." -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$healthServices = @(
|
||||||
|
@{Name="classes"; Url="http://localhost:3001/healthz"},
|
||||||
|
@{Name="iam"; Url="http://localhost:3002/healthz"},
|
||||||
|
@{Name="teacher-bff"; Url="http://localhost:3003/healthz"},
|
||||||
|
@{Name="core-edu"; Url="http://localhost:3004/healthz"},
|
||||||
|
@{Name="content"; Url="http://localhost:3005/healthz"},
|
||||||
|
@{Name="data-ana"; Url="http://localhost:3006/healthz"},
|
||||||
|
@{Name="msg"; Url="http://localhost:3007/healthz"},
|
||||||
|
@{Name="ai"; Url="http://localhost:3008/healthz"},
|
||||||
|
@{Name="api-gateway"; Url="http://localhost:8080/healthz"},
|
||||||
|
@{Name="push-gateway"; Url="http://localhost:8081/healthz"},
|
||||||
|
@{Name="teacher-portal";Url="http://localhost:3000/"}
|
||||||
|
)
|
||||||
|
|
||||||
|
$okCount = 0
|
||||||
|
$failCount = 0
|
||||||
|
$failedServices = @()
|
||||||
|
foreach ($svc in $healthServices) {
|
||||||
|
$retries = 0
|
||||||
|
$maxRetries = 3
|
||||||
|
$success = $false
|
||||||
|
$lastError = ""
|
||||||
|
while ($retries -lt $maxRetries -and -not $success) {
|
||||||
|
try {
|
||||||
|
$null = Invoke-RestMethod -Uri $svc.Url -Method Get -TimeoutSec 5 -ErrorAction Stop
|
||||||
|
Write-Host " [OK] $($svc.Name)" -ForegroundColor Green
|
||||||
|
$success = $true
|
||||||
|
$okCount++
|
||||||
|
} catch {
|
||||||
|
$lastError = $_.Exception.Message
|
||||||
|
$retries++
|
||||||
|
if ($retries -lt $maxRetries) {
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $success) {
|
||||||
|
Write-Host " [FAIL] $($svc.Name) (after $maxRetries retries: $lastError)" -ForegroundColor Red
|
||||||
|
$failCount++
|
||||||
|
$failedServices += $svc.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Started: [OK] $okCount ready / [FAIL] $failCount failed" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
if ($failCount -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Failed services: $($failedServices -join ', ')" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Troubleshooting:" -ForegroundColor Yellow
|
||||||
|
Write-Host " 1. Check the service window for error output" -ForegroundColor White
|
||||||
|
Write-Host " 2. Verify dependencies: pnpm install / uv sync / go mod tidy" -ForegroundColor White
|
||||||
|
Write-Host " 3. Re-run health check: .\scripts\health-check.ps1" -ForegroundColor White
|
||||||
|
Write-Host " 4. Stop and retry: .\scripts\stop-all.ps1 -KillByPort then .\scripts\start-all.ps1" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||||
|
Write-Host " Health check: .\scripts\health-check.ps1" -ForegroundColor White
|
||||||
|
Write-Host " CDC test: .\scripts\test-cdc.ps1" -ForegroundColor White
|
||||||
|
Write-Host " Stop all: .\scripts\stop-all.ps1" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
98
scripts/stop-all.ps1
Normal file
98
scripts/stop-all.ps1
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Edu stop all application services script
|
||||||
|
.DESCRIPTION
|
||||||
|
Closes all edu-app-* terminal windows (started by start-all.ps1)
|
||||||
|
Optional: kill processes by port (fallback when windows are closed but processes linger)
|
||||||
|
.PARAMETER KillByPort
|
||||||
|
Kill processes by port (fallback when windows are closed but processes still alive)
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\stop-all.ps1
|
||||||
|
.\scripts\stop-all.ps1 -KillByPort
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[switch]$KillByPort
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Edu Stop All Services" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 1. Close app service terminal windows =====
|
||||||
|
Write-Host "[1/2] Closing app service windows..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$appTitles = @(
|
||||||
|
"edu-app-classes",
|
||||||
|
"edu-app-iam",
|
||||||
|
"edu-app-teacher-bff",
|
||||||
|
"edu-app-core-edu",
|
||||||
|
"edu-app-content",
|
||||||
|
"edu-app-msg",
|
||||||
|
"edu-app-data-ana",
|
||||||
|
"edu-app-ai",
|
||||||
|
"edu-app-api-gateway",
|
||||||
|
"edu-app-push-gateway",
|
||||||
|
"edu-app-teacher-portal"
|
||||||
|
)
|
||||||
|
|
||||||
|
$closedCount = 0
|
||||||
|
foreach ($title in $appTitles) {
|
||||||
|
$procs = Get-Process -Name "powershell","pwsh","node","python","uvicorn","go" -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.MainWindowTitle -like "*$title*" }
|
||||||
|
|
||||||
|
if ($procs) {
|
||||||
|
foreach ($p in $procs) {
|
||||||
|
try {
|
||||||
|
Stop-Process -Id $p.Id -Force -ErrorAction Stop
|
||||||
|
Write-Host " [OK] Closed $title (PID $($p.Id))" -ForegroundColor Green
|
||||||
|
$closedCount++
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] Cannot close $title (PID $($p.Id)): $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [--] $title window not found" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Closed $closedCount windows" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 2. Kill by port (optional) =====
|
||||||
|
if ($KillByPort) {
|
||||||
|
Write-Host "[2/2] Killing processes by port..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$ports = @(3000,3001,3002,3003,3004,3005,3006,3007,3008,8080,8081)
|
||||||
|
|
||||||
|
foreach ($port in $ports) {
|
||||||
|
$connections = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
||||||
|
if ($connections) {
|
||||||
|
foreach ($conn in $connections) {
|
||||||
|
try {
|
||||||
|
$proc = Get-Process -Id $conn.OwningProcess -ErrorAction Stop
|
||||||
|
Stop-Process -Id $conn.OwningProcess -Force -ErrorAction Stop
|
||||||
|
Write-Host " [OK] Port $port -> killed $($proc.ProcessName) (PID $($conn.OwningProcess))" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] Port $port -> cannot kill PID $($conn.OwningProcess)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [--] Port $port free" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "[2/2] Skipping port kill (use -KillByPort to enable)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Stop complete" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "To stop infrastructure:" -ForegroundColor Yellow
|
||||||
|
Write-Host " docker compose -f infra/docker-compose.yml --profile p6 --profile observability down" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
167
scripts/test-cdc.ps1
Normal file
167
scripts/test-cdc.ps1
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Edu CDC pipeline end-to-end test script
|
||||||
|
.DESCRIPTION
|
||||||
|
Validates the full CDC pipeline: MySQL binlog -> Debezium -> Kafka -> data-ana -> ClickHouse
|
||||||
|
Steps:
|
||||||
|
1. Insert test grade into MySQL
|
||||||
|
2. Wait for Debezium capture + data-ana consume
|
||||||
|
3. Query ClickHouse to verify data synced
|
||||||
|
4. Call data-ana API to verify query works
|
||||||
|
.PARAMETER StudentId
|
||||||
|
Custom test student_id (default: cdc-test-<timestamp>)
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\test-cdc.ps1
|
||||||
|
.\scripts\test-cdc.ps1 -StudentId "my-test-001"
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string]$StudentId = "cdc-test-$(Get-Date -Format 'yyyyMMddHHmmss')"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Edu CDC Pipeline E2E Test" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$examId = "exam-$StudentId"
|
||||||
|
$gradeId = "grade-$StudentId"
|
||||||
|
$testScore = 92.5
|
||||||
|
$classId = "cls-cdc-test"
|
||||||
|
|
||||||
|
Write-Host "Test parameters:" -ForegroundColor Yellow
|
||||||
|
Write-Host " StudentId: $StudentId"
|
||||||
|
Write-Host " ExamId: $examId"
|
||||||
|
Write-Host " GradeId: $gradeId"
|
||||||
|
Write-Host " Score: $testScore"
|
||||||
|
Write-Host " ClassId: $classId"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 1. Pre-check =====
|
||||||
|
Write-Host "[1/5] Pre-check..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Check Debezium connector
|
||||||
|
$connectorStatus = $null
|
||||||
|
try {
|
||||||
|
$connectorStatus = Invoke-RestMethod -Uri "http://localhost:8083/connectors/edu-mysql-source/status" -Method Get -TimeoutSec 3 -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
Write-Host " [FAIL] Debezium Connect not reachable. Start infrastructure first." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($connectorStatus.connector.state -ne "RUNNING") {
|
||||||
|
Write-Host " [FAIL] Debezium connector state: $($connectorStatus.connector.state)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host " [OK] Debezium connector: RUNNING" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check data-ana service
|
||||||
|
try {
|
||||||
|
$null = Invoke-RestMethod -Uri "http://localhost:3006/healthz" -Method Get -TimeoutSec 3 -ErrorAction Stop
|
||||||
|
Write-Host " [OK] data-ana service: running" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " [FAIL] data-ana service not reachable. Start application services first." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check CDC consumer status
|
||||||
|
try {
|
||||||
|
$readyz = Invoke-RestMethod -Uri "http://localhost:3006/readyz" -Method Get -TimeoutSec 3 -ErrorAction Stop
|
||||||
|
$cdcStatus = $readyz.services.cdc_consumer
|
||||||
|
if ($cdcStatus -ne "running") {
|
||||||
|
Write-Host " [WARN] CDC consumer status: $cdcStatus (KAFKA_BROKERS may not be set)" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] CDC consumer: running" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] Cannot get /readyz status" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 2. Insert test data into MySQL =====
|
||||||
|
Write-Host "[2/5] Inserting test data into MySQL..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$sqlInsert = @"
|
||||||
|
INSERT INTO core_edu_exams (id, class_id, subject_id, title, exam_date, total_score, created_at, updated_at)
|
||||||
|
VALUES ('$examId', '$classId', 'sub-math', 'CDC Test Exam', NOW(), 100, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE updated_at=NOW();
|
||||||
|
INSERT INTO core_edu_grades (id, exam_id, student_id, score, rank_in_class, created_at, updated_at)
|
||||||
|
VALUES ('$gradeId', '$examId', '$StudentId', $testScore, 1, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE score=$testScore, updated_at=NOW();
|
||||||
|
"@
|
||||||
|
|
||||||
|
docker exec edu-mysql mysql -uedu -pchangeme next_edu_cloud -e $sqlInsert 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] Inserted: exam=$examId / grade=$gradeId / student=$StudentId / score=$testScore" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [FAIL] MySQL insert failed" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 3. Wait for CDC propagation =====
|
||||||
|
Write-Host "[3/5] Waiting for CDC propagation (5s)..." -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
Write-Host " [OK] Wait complete" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 4. Verify ClickHouse data =====
|
||||||
|
Write-Host "[4/5] Verifying ClickHouse data..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$chQuery = "SELECT student_id, class_id, exam_id, score, last_updated FROM edu_analytics.student_dashboard_view WHERE student_id = '$StudentId' ORDER BY last_updated DESC LIMIT 5"
|
||||||
|
$chResult = docker exec edu-clickhouse clickhouse-client --user default --password clickhouse -q $chQuery 2>$null
|
||||||
|
|
||||||
|
if ($chResult) {
|
||||||
|
Write-Host " [OK] ClickHouse returned data:" -ForegroundColor Green
|
||||||
|
Write-Host " $chResult" -ForegroundColor White
|
||||||
|
|
||||||
|
if ($chResult -match $StudentId -and $chResult -match "$testScore") {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " [OK] Verified: student_id match + score=$testScore match" -ForegroundColor Green
|
||||||
|
if ($chResult -match $classId) {
|
||||||
|
Write-Host " [OK] class_id filled via exam cache: $classId" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARN] class_id not filled (exam cache may have missed, check event order)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [FAIL] Data mismatch: expected student=$StudentId, score=$testScore" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [FAIL] ClickHouse has no data for student_id=$StudentId" -ForegroundColor Red
|
||||||
|
Write-Host " Possible causes:" -ForegroundColor Yellow
|
||||||
|
Write-Host " 1. Debezium did not capture MySQL change (check connector status)" -ForegroundColor White
|
||||||
|
Write-Host " 2. data-ana consumer not running (check /readyz cdc_consumer)" -ForegroundColor White
|
||||||
|
Write-Host " 3. Kafka topic name mismatch (check debezium-register.json)" -ForegroundColor White
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ===== 5. Verify data-ana API =====
|
||||||
|
Write-Host "[5/5] Verifying data-ana query API..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$h = @{Authorization="Bearer dev-token"}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$weakness = Invoke-RestMethod -Uri "http://localhost:3006/analytics/student/$StudentId/weakness" -Method Get -Headers $h -TimeoutSec 5 -ErrorAction Stop
|
||||||
|
Write-Host " [OK] /analytics/student/$StudentId/weakness" -ForegroundColor Green
|
||||||
|
Write-Host " Response: $($weakness | ConvertTo-Json -Depth 3)" -ForegroundColor Gray
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] /analytics/student/$StudentId/weakness failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$perf = Invoke-RestMethod -Uri "http://localhost:3006/analytics/class/$classId/performance" -Method Get -Headers $h -TimeoutSec 5 -ErrorAction Stop
|
||||||
|
Write-Host " [OK] /analytics/class/$classId/performance" -ForegroundColor Green
|
||||||
|
Write-Host " Response: $($perf | ConvertTo-Json -Depth 3)" -ForegroundColor Gray
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] /analytics/class/$classId/performance failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " CDC Pipeline Test Complete" -ForegroundColor Cyan
|
||||||
|
Write-Host " MySQL -> Debezium -> Kafka -> data-ana -> ClickHouse [OK]" -ForegroundColor Green
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
@@ -11,6 +11,8 @@ dependencies = [
|
|||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"opentelemetry-api>=1.27.0",
|
"opentelemetry-api>=1.27.0",
|
||||||
"opentelemetry-sdk>=1.27.0",
|
"opentelemetry-sdk>=1.27.0",
|
||||||
|
"opentelemetry-exporter-otlp>=1.27.0",
|
||||||
|
"opentelemetry-instrumentation-fastapi>=0.48b0",
|
||||||
"prometheus-client>=0.20.0",
|
"prometheus-client>=0.20.0",
|
||||||
"structlog>=24.4.0",
|
"structlog>=24.4.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from fastapi import APIRouter, FastAPI
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||||
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||||
from opentelemetry.sdk.trace import TracerProvider
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||||
from prometheus_client import make_asgi_app
|
from prometheus_client import make_asgi_app
|
||||||
@@ -61,6 +62,9 @@ app = FastAPI(
|
|||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# OpenTelemetry FastAPI 自动埋点(HTTP 请求/响应 span)
|
||||||
|
FastAPIInstrumentor.instrument_app(app)
|
||||||
|
|
||||||
app.mount("/metrics", make_asgi_app())
|
app.mount("/metrics", make_asgi_app())
|
||||||
|
|
||||||
# 业务路由加 /ai 前缀,Gateway 代理 /api/v1/ai/* → /ai/*
|
# 业务路由加 /ai 前缀,Gateway 代理 /api/v1/ai/* → /ai/*
|
||||||
|
|||||||
@@ -1,46 +1,64 @@
|
|||||||
module github.com/edu-cloud/api-gateway
|
module github.com/edu-cloud/api-gateway
|
||||||
|
|
||||||
go 1.22.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/sony/gobreaker/v2 v2.1.0
|
github.com/sony/gobreaker/v2 v2.1.0
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0
|
||||||
|
go.opentelemetry.io/otel v1.44.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic v1.15.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||||
github.com/go-redsync/redsync/v4 v4.13.0 // indirect
|
github.com/go-redsync/redsync/v4 v4.13.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.7.0 // indirect
|
github.com/redis/go-redis/v9 v9.7.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
golang.org/x/arch v0.27.0 // indirect
|
||||||
|
golang.org/x/crypto v0.52.0 // indirect
|
||||||
|
golang.org/x/net v0.55.0 // indirect
|
||||||
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/grpc v1.81.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,35 +6,42 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
|||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
||||||
|
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
||||||
@@ -43,17 +50,23 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC
|
|||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA=
|
github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA=
|
||||||
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
|
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
||||||
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -61,23 +74,25 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
|||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||||
|
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||||
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
||||||
@@ -89,42 +104,73 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
|||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
|
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
|
||||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
|
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0 h1:u5gsfBL8t1Km4ROhQKAs0cA0t9CzUE7nfkASj/UjAtI=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0/go.mod h1:W6FFYCZQuntC5hxVesXpu7Ppd9sT0a84njildAijc+k=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/arch v0.27.0 h1:0WNVcR8u9yFz8j5FvdHpgwNp3FS5U4guYdzHwEiGjoU=
|
||||||
|
golang.org/x/arch v0.27.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||||
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
|
|||||||
73
services/api-gateway/internal/observability/tracer.go
Normal file
73
services/api-gateway/internal/observability/tracer.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitTracer 初始化 OpenTelemetry tracer.
|
||||||
|
//
|
||||||
|
// endpoint 为 "http://host:port" 格式(如 "http://localhost:4318");
|
||||||
|
// 为空时跳过初始化(tracing disabled)。
|
||||||
|
//
|
||||||
|
// 返回 shutdown 函数,应在服务退出时调用以 flush 待发送 span.
|
||||||
|
func InitTracer(serviceName, endpoint string) func() {
|
||||||
|
if endpoint == "" {
|
||||||
|
log.Println("OTEL endpoint not set, tracing disabled")
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(endpoint)
|
||||||
|
if err != nil || u.Host == "" {
|
||||||
|
log.Printf("invalid OTEL endpoint %q, tracing disabled", endpoint)
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
exporter, err := otlptracehttp.New(ctx,
|
||||||
|
otlptracehttp.WithEndpoint(u.Host),
|
||||||
|
otlptracehttp.WithInsecure(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to create OTLP exporter: %v, tracing disabled", err)
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := resource.New(ctx,
|
||||||
|
resource.WithAttributes(semconv.ServiceName(serviceName)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to create resource: %v", err)
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithBatcher(exporter),
|
||||||
|
sdktrace.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetTracerProvider(tp)
|
||||||
|
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||||
|
propagation.TraceContext{},
|
||||||
|
propagation.Baggage{},
|
||||||
|
))
|
||||||
|
|
||||||
|
log.Printf("OpenTelemetry tracer initialized for %s (endpoint=%s)", serviceName, u.Host)
|
||||||
|
return func() {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := tp.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Printf("failed to shutdown tracer: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,10 @@ import (
|
|||||||
"github.com/edu-cloud/api-gateway/internal/config"
|
"github.com/edu-cloud/api-gateway/internal/config"
|
||||||
"github.com/edu-cloud/api-gateway/internal/health"
|
"github.com/edu-cloud/api-gateway/internal/health"
|
||||||
"github.com/edu-cloud/api-gateway/internal/middleware"
|
"github.com/edu-cloud/api-gateway/internal/middleware"
|
||||||
|
"github.com/edu-cloud/api-gateway/internal/observability"
|
||||||
"github.com/edu-cloud/api-gateway/internal/proxy"
|
"github.com/edu-cloud/api-gateway/internal/proxy"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxBodyBytes 请求体大小上限:10MB
|
// maxBodyBytes 请求体大小上限:10MB
|
||||||
@@ -21,6 +23,10 @@ const maxBodyBytes int64 = 10 * 1024 * 1024
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
// 初始化 OpenTelemetry tracer(endpoint 为空时自动跳过)
|
||||||
|
tracerShutdown := observability.InitTracer("api-gateway", cfg.OTLPEndpoint)
|
||||||
|
defer tracerShutdown()
|
||||||
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
// 关闭尾斜杠重定向:避免 Next.js rewrites 代理时 /api/v1/classes → 301 → /api/v1/classes/ 循环
|
// 关闭尾斜杠重定向:避免 Next.js rewrites 代理时 /api/v1/classes → 301 → /api/v1/classes/ 循环
|
||||||
@@ -29,15 +35,17 @@ func main() {
|
|||||||
// 全局中间件(按顺序注册)
|
// 全局中间件(按顺序注册)
|
||||||
// 1. panic 恢复(最外层,捕获后续所有中间件与 handler 的 panic)
|
// 1. panic 恢复(最外层,捕获后续所有中间件与 handler 的 panic)
|
||||||
r.Use(middleware.Recovery())
|
r.Use(middleware.Recovery())
|
||||||
// 2. 请求 ID 注入
|
// 2. OpenTelemetry 自动埋点(HTTP 请求/响应 span)
|
||||||
|
r.Use(otelgin.Middleware("api-gateway"))
|
||||||
|
// 3. 请求 ID 注入
|
||||||
r.Use(middleware.RequestID())
|
r.Use(middleware.RequestID())
|
||||||
// 3. 跨域
|
// 4. 跨域
|
||||||
r.Use(middleware.CORS())
|
r.Use(middleware.CORS())
|
||||||
// 4. 安全响应头
|
// 5. 安全响应头
|
||||||
r.Use(middleware.SecurityHeaders())
|
r.Use(middleware.SecurityHeaders())
|
||||||
// 5. 请求体大小限制
|
// 6. 请求体大小限制
|
||||||
r.Use(middleware.RequestBodyLimit(maxBodyBytes))
|
r.Use(middleware.RequestBodyLimit(maxBodyBytes))
|
||||||
// 6. 限流(每 IP 100 rps,突发 20)
|
// 7. 限流(每 IP 100 rps,突发 20)
|
||||||
r.Use(middleware.RateLimit(100, 20))
|
r.Use(middleware.RateLimit(100, 20))
|
||||||
|
|
||||||
// 健康检查路由(无需鉴权,在 Auth 之前)
|
// 健康检查路由(无需鉴权,在 Auth 之前)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { ClassesModule } from './classes/classes.module.js';
|
import { ClassesModule } from "./classes/classes.module.js";
|
||||||
|
import { HealthModule } from "./shared/health/health.module.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ClassesModule],
|
imports: [ClassesModule, HealthModule],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
|
import { Controller, Get, HttpException, HttpStatus } from "@nestjs/common";
|
||||||
import { DataSource } from 'typeorm';
|
import { sql } from "drizzle-orm";
|
||||||
|
import { getDb } from "../../config/database.js";
|
||||||
|
|
||||||
const SERVICE_NAME = 'classes';
|
const SERVICE_NAME = "classes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 健康检查端点。
|
* 健康检查端点。
|
||||||
@@ -14,33 +15,37 @@ const SERVICE_NAME = 'classes';
|
|||||||
*/
|
*/
|
||||||
@Controller()
|
@Controller()
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(private readonly dataSource: DataSource) {}
|
@Get("healthz")
|
||||||
|
|
||||||
@Get('healthz')
|
|
||||||
liveness(): { status: string; service: string; timestamp: string } {
|
liveness(): { status: string; service: string; timestamp: string } {
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: "ok",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('readyz')
|
@Get("readyz")
|
||||||
async readiness(): Promise<{ status: string; service: string; timestamp: string }> {
|
async readiness(): Promise<{
|
||||||
|
status: string;
|
||||||
|
service: string;
|
||||||
|
timestamp: string;
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
await this.dataSource.query('SELECT 1');
|
const db = getDb();
|
||||||
|
await db.execute(sql`SELECT 1`);
|
||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: "ok",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
status: 'error',
|
status: "error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
error: error instanceof Error ? error.message : 'database unreachable',
|
error:
|
||||||
|
error instanceof Error ? error.message : "database unreachable",
|
||||||
},
|
},
|
||||||
HttpStatus.SERVICE_UNAVAILABLE,
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||||
import { env } from '../../config/env.js';
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||||
|
import { env } from "../../config/env.js";
|
||||||
|
|
||||||
let sdk: NodeSDK | null = null;
|
let sdk: NodeSDK | null = null;
|
||||||
|
|
||||||
@@ -8,14 +9,15 @@ export function initTracer(): void {
|
|||||||
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
||||||
|
|
||||||
sdk = new NodeSDK({
|
sdk = new NodeSDK({
|
||||||
serviceName: 'classes',
|
serviceName: "classes",
|
||||||
traceExporter: new OTLPTraceExporter({
|
traceExporter: new OTLPTraceExporter({
|
||||||
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
||||||
}),
|
}),
|
||||||
|
instrumentations: [getNodeAutoInstrumentations()],
|
||||||
});
|
});
|
||||||
|
|
||||||
sdk.start();
|
sdk.start();
|
||||||
console.log('Tracer initialized');
|
console.log("Tracer initialized with auto-instrumentations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdownTracer(): Promise<void> {
|
export async function shutdownTracer(): Promise<void> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
"incremental": false,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"@opentelemetry/sdk-node": "^0.55.0",
|
"@opentelemetry/sdk-node": "^0.55.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.55.0"
|
"@opentelemetry/exporter-trace-otlp-http": "^0.55.0",
|
||||||
|
"@opentelemetry/auto-instrumentations-node": "^0.55.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.4.0",
|
"@nestjs/cli": "^10.4.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||||
import { env } from '../../config/env.js';
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||||
|
import { env } from "../../config/env.js";
|
||||||
|
|
||||||
let sdk: NodeSDK | null = null;
|
let sdk: NodeSDK | null = null;
|
||||||
|
|
||||||
@@ -8,14 +9,15 @@ export function initTracer(): void {
|
|||||||
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
||||||
|
|
||||||
sdk = new NodeSDK({
|
sdk = new NodeSDK({
|
||||||
serviceName: 'content',
|
serviceName: "content",
|
||||||
traceExporter: new OTLPTraceExporter({
|
traceExporter: new OTLPTraceExporter({
|
||||||
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
||||||
}),
|
}),
|
||||||
|
instrumentations: [getNodeAutoInstrumentations()],
|
||||||
});
|
});
|
||||||
|
|
||||||
sdk.start();
|
sdk.start();
|
||||||
console.log('Tracer initialized');
|
console.log("Tracer initialized with auto-instrumentations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdownTracer(): Promise<void> {
|
export async function shutdownTracer(): Promise<void> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
"incremental": false,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
"prom-client": "^15.1.0",
|
"prom-client": "^15.1.0",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
|
||||||
"@opentelemetry/sdk-node": "^0.53.0",
|
"@opentelemetry/sdk-node": "^0.53.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
||||||
"zod": "^3.23.0",
|
"zod": "^3.23.0",
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||||
import { env } from '../../config/env.js';
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||||
import { logger } from './logger.js';
|
import { env } from "../../config/env.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
let sdk: NodeSDK | undefined;
|
let sdk: NodeSDK | undefined;
|
||||||
|
|
||||||
export function initTracer(): void {
|
export function initTracer(): void {
|
||||||
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) {
|
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) {
|
||||||
logger.warn('OTEL_EXPORTER_OTLP_ENDPOINT not set, tracing disabled');
|
logger.warn("OTEL_EXPORTER_OTLP_ENDPOINT not set, tracing disabled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sdk = new NodeSDK({
|
sdk = new NodeSDK({
|
||||||
serviceName: 'core-edu',
|
serviceName: "core-edu",
|
||||||
traceExporter: new OTLPTraceExporter({
|
traceExporter: new OTLPTraceExporter({
|
||||||
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
||||||
}),
|
}),
|
||||||
|
instrumentations: [getNodeAutoInstrumentations()],
|
||||||
});
|
});
|
||||||
sdk.start();
|
sdk.start();
|
||||||
logger.info('OpenTelemetry tracer initialized');
|
logger.info("OpenTelemetry tracer initialized with auto-instrumentations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdownTracer(): Promise<void> {
|
export async function shutdownTracer(): Promise<void> {
|
||||||
if (sdk) {
|
if (sdk) {
|
||||||
await sdk.shutdown();
|
await sdk.shutdown();
|
||||||
sdk = undefined;
|
sdk = undefined;
|
||||||
logger.info('OpenTelemetry tracer shutdown');
|
logger.info("OpenTelemetry tracer shutdown");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
"incremental": false,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import structlog
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||||
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||||
from opentelemetry.sdk.trace import TracerProvider
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||||
from prometheus_client import make_asgi_app
|
from prometheus_client import make_asgi_app
|
||||||
@@ -126,6 +127,9 @@ app = FastAPI(
|
|||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# OpenTelemetry FastAPI 自动埋点(HTTP 请求/响应 span)
|
||||||
|
FastAPIInstrumentor.instrument_app(app)
|
||||||
|
|
||||||
# Prometheus 指标
|
# Prometheus 指标
|
||||||
app.mount("/metrics", make_asgi_app())
|
app.mount("/metrics", make_asgi_app())
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
"prom-client": "^15.1.0",
|
"prom-client": "^15.1.0",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
|
||||||
"@opentelemetry/sdk-node": "^0.53.0",
|
"@opentelemetry/sdk-node": "^0.53.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
||||||
"zod": "^3.23.0",
|
"zod": "^3.23.0",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { IamModule } from './iam/iam.module.js';
|
import { IamModule } from "./iam/iam.module.js";
|
||||||
|
import { HealthModule } from "./shared/health/health.module.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [IamModule],
|
imports: [IamModule, HealthModule],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||||
import { env } from '../../config/env.js';
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||||
|
import { env } from "../../config/env.js";
|
||||||
|
|
||||||
let sdk: NodeSDK | null = null;
|
let sdk: NodeSDK | null = null;
|
||||||
|
|
||||||
@@ -8,14 +9,15 @@ export function initTracer(): void {
|
|||||||
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
||||||
|
|
||||||
sdk = new NodeSDK({
|
sdk = new NodeSDK({
|
||||||
serviceName: 'iam',
|
serviceName: "iam",
|
||||||
traceExporter: new OTLPTraceExporter({
|
traceExporter: new OTLPTraceExporter({
|
||||||
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
||||||
}),
|
}),
|
||||||
|
instrumentations: [getNodeAutoInstrumentations()],
|
||||||
});
|
});
|
||||||
|
|
||||||
sdk.start();
|
sdk.start();
|
||||||
console.log('Tracer initialized');
|
console.log("Tracer initialized with auto-instrumentations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdownTracer(): Promise<void> {
|
export async function shutdownTracer(): Promise<void> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
"incremental": false,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"@opentelemetry/sdk-node": "^0.55.0",
|
"@opentelemetry/sdk-node": "^0.55.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.55.0"
|
"@opentelemetry/exporter-trace-otlp-http": "^0.55.0",
|
||||||
|
"@opentelemetry/auto-instrumentations-node": "^0.55.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.4.0",
|
"@nestjs/cli": "^10.4.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||||
import { env } from '../../config/env.js';
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||||
|
import { env } from "../../config/env.js";
|
||||||
|
|
||||||
let sdk: NodeSDK | null = null;
|
let sdk: NodeSDK | null = null;
|
||||||
|
|
||||||
@@ -8,14 +9,15 @@ export function initTracer(): void {
|
|||||||
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
||||||
|
|
||||||
sdk = new NodeSDK({
|
sdk = new NodeSDK({
|
||||||
serviceName: 'msg',
|
serviceName: "msg",
|
||||||
traceExporter: new OTLPTraceExporter({
|
traceExporter: new OTLPTraceExporter({
|
||||||
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
||||||
}),
|
}),
|
||||||
|
instrumentations: [getNodeAutoInstrumentations()],
|
||||||
});
|
});
|
||||||
|
|
||||||
sdk.start();
|
sdk.start();
|
||||||
console.log('Tracer initialized');
|
console.log("Tracer initialized with auto-instrumentations");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdownTracer(): Promise<void> {
|
export async function shutdownTracer(): Promise<void> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
"incremental": false,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|||||||
@@ -1,38 +1,59 @@
|
|||||||
module github.com/edu-cloud/push-gateway
|
module github.com/edu-cloud/push-gateway
|
||||||
|
|
||||||
go 1.22
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0
|
||||||
|
go.opentelemetry.io/otel v1.44.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic v1.15.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
golang.org/x/arch v0.27.0 // indirect
|
||||||
|
golang.org/x/crypto v0.52.0 // indirect
|
||||||
|
golang.org/x/net v0.55.0 // indirect
|
||||||
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/grpc v1.81.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,93 +1,141 @@
|
|||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
||||||
|
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||||
|
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0 h1:u5gsfBL8t1Km4ROhQKAs0cA0t9CzUE7nfkASj/UjAtI=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.69.0/go.mod h1:W6FFYCZQuntC5hxVesXpu7Ppd9sT0a84njildAijc+k=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D/gAmznQTd4dxxMA93aKbcpKqiTCs=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/arch v0.27.0 h1:0WNVcR8u9yFz8j5FvdHpgwNp3FS5U4guYdzHwEiGjoU=
|
||||||
|
golang.org/x/arch v0.27.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||||
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
|
|||||||
@@ -4,19 +4,21 @@ import "os"
|
|||||||
|
|
||||||
// Config 持有 push-gateway 运行时配置
|
// Config 持有 push-gateway 运行时配置
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port string
|
Port string
|
||||||
JWTSecret string
|
JWTSecret string
|
||||||
DevMode bool
|
DevMode bool
|
||||||
RedisURL string
|
RedisURL string
|
||||||
|
OTLPEndpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 从环境变量加载配置并提供默认值
|
// Load 从环境变量加载配置并提供默认值
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Port: getEnv("PUSH_GATEWAY_PORT", "8081"),
|
Port: getEnv("PUSH_GATEWAY_PORT", "8081"),
|
||||||
JWTSecret: getEnv("JWT_SECRET", "p1-dev-secret-change-in-production"),
|
JWTSecret: getEnv("JWT_SECRET", "p1-dev-secret-change-in-production"),
|
||||||
DevMode: getEnv("DEV_MODE", "false") == "true",
|
DevMode: getEnv("DEV_MODE", "false") == "true",
|
||||||
RedisURL: getEnv("REDIS_URL", ""),
|
RedisURL: getEnv("REDIS_URL", ""),
|
||||||
|
OTLPEndpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
73
services/push-gateway/internal/observability/tracer.go
Normal file
73
services/push-gateway/internal/observability/tracer.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitTracer 初始化 OpenTelemetry tracer.
|
||||||
|
//
|
||||||
|
// endpoint 为 "http://host:port" 格式(如 "http://localhost:4318");
|
||||||
|
// 为空时跳过初始化(tracing disabled)。
|
||||||
|
//
|
||||||
|
// 返回 shutdown 函数,应在服务退出时调用以 flush 待发送 span.
|
||||||
|
func InitTracer(serviceName, endpoint string) func() {
|
||||||
|
if endpoint == "" {
|
||||||
|
log.Println("OTEL endpoint not set, tracing disabled")
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(endpoint)
|
||||||
|
if err != nil || u.Host == "" {
|
||||||
|
log.Printf("invalid OTEL endpoint %q, tracing disabled", endpoint)
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
exporter, err := otlptracehttp.New(ctx,
|
||||||
|
otlptracehttp.WithEndpoint(u.Host),
|
||||||
|
otlptracehttp.WithInsecure(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to create OTLP exporter: %v, tracing disabled", err)
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := resource.New(ctx,
|
||||||
|
resource.WithAttributes(semconv.ServiceName(serviceName)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to create resource: %v", err)
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithBatcher(exporter),
|
||||||
|
sdktrace.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetTracerProvider(tp)
|
||||||
|
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||||
|
propagation.TraceContext{},
|
||||||
|
propagation.Baggage{},
|
||||||
|
))
|
||||||
|
|
||||||
|
log.Printf("OpenTelemetry tracer initialized for %s (endpoint=%s)", serviceName, u.Host)
|
||||||
|
return func() {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := tp.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Printf("failed to shutdown tracer: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,12 +11,18 @@ import (
|
|||||||
|
|
||||||
"github.com/edu-cloud/push-gateway/internal/config"
|
"github.com/edu-cloud/push-gateway/internal/config"
|
||||||
"github.com/edu-cloud/push-gateway/internal/hub"
|
"github.com/edu-cloud/push-gateway/internal/hub"
|
||||||
|
"github.com/edu-cloud/push-gateway/internal/observability"
|
||||||
"github.com/edu-cloud/push-gateway/internal/ws"
|
"github.com/edu-cloud/push-gateway/internal/ws"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
// 初始化 OpenTelemetry tracer(endpoint 为空时自动跳过)
|
||||||
|
tracerShutdown := observability.InitTracer("push-gateway", cfg.OTLPEndpoint)
|
||||||
|
defer tracerShutdown()
|
||||||
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
h := hub.NewHub()
|
h := hub.NewHub()
|
||||||
@@ -24,6 +30,8 @@ func main() {
|
|||||||
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
|
// OpenTelemetry 自动埋点(HTTP 请求/响应 span)
|
||||||
|
r.Use(otelgin.Middleware("push-gateway"))
|
||||||
|
|
||||||
r.GET("/healthz", func(c *gin.Context) {
|
r.GET("/healthz", func(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{"status": "ok", "service": "push-gateway"})
|
c.JSON(200, gin.H{"status": "ok", "service": "push-gateway"})
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
"pino": "^9.4.0",
|
"pino": "^9.4.0",
|
||||||
"prom-client": "^15.1.0",
|
"prom-client": "^15.1.0",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/auto-instrumentations-node": "^0.50.0",
|
||||||
|
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
|
||||||
|
"@opentelemetry/sdk-node": "^0.53.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"zod": "^3.23.0"
|
"zod": "^3.23.0"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const envSchema = z.object({
|
|||||||
IamServiceUrl: z.string().url().default("http://localhost:3002"),
|
IamServiceUrl: z.string().url().default("http://localhost:3002"),
|
||||||
ClassesServiceUrl: z.string().url().default("http://localhost:3001"),
|
ClassesServiceUrl: z.string().url().default("http://localhost:3001"),
|
||||||
CoreEduServiceUrl: z.string().url().default("http://localhost:3004"),
|
CoreEduServiceUrl: z.string().url().default("http://localhost:3004"),
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
|
||||||
LOG_LEVEL: z.string().default("info"),
|
LOG_LEVEL: z.string().default("info"),
|
||||||
NODE_ENV: z.string().default("development"),
|
NODE_ENV: z.string().default("development"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
import 'reflect-metadata';
|
import "reflect-metadata";
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from "@nestjs/core";
|
||||||
import { AppModule } from './app.module.js';
|
import { AppModule } from "./app.module.js";
|
||||||
import { env } from './config/env.js';
|
import { env } from "./config/env.js";
|
||||||
|
import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
|
||||||
|
import { metricsRegistry } from "./shared/observability/metrics.js";
|
||||||
|
|
||||||
async function bootstrap(): Promise<void> {
|
async function bootstrap(): Promise<void> {
|
||||||
|
initTracer();
|
||||||
const app = await NestFactory.create(AppModule, {
|
const app = await NestFactory.create(AppModule, {
|
||||||
logger: ['log', 'error', 'warn'],
|
logger: ["log", "error", "warn"],
|
||||||
});
|
});
|
||||||
|
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
|
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
|
||||||
|
app.getHttpAdapter().get("/metrics", async (_req, res) => {
|
||||||
|
res.set("Content-Type", metricsRegistry.contentType);
|
||||||
|
res.end(await metricsRegistry.metrics());
|
||||||
|
});
|
||||||
|
|
||||||
await app.listen(env.PORT);
|
await app.listen(env.PORT);
|
||||||
console.log(`Teacher BFF started on port ${env.PORT}`);
|
console.log(`Teacher BFF started on port ${env.PORT}`);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
process.on("SIGTERM", async () => {
|
||||||
await app.close();
|
await app.close();
|
||||||
|
await shutdownTracer();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap().catch((err: unknown) => {
|
bootstrap().catch((err: unknown) => {
|
||||||
console.error('Failed to start Teacher BFF', err);
|
console.error("Failed to start Teacher BFF", err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
26
services/teacher-bff/src/shared/observability/metrics.ts
Normal file
26
services/teacher-bff/src/shared/observability/metrics.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import promClient from "prom-client";
|
||||||
|
|
||||||
|
const registry = new promClient.Registry();
|
||||||
|
registry.setDefaultLabels({ service: "teacher-bff" });
|
||||||
|
|
||||||
|
registry.registerMetric(
|
||||||
|
new promClient.Counter({
|
||||||
|
name: "teacher_bff_requests_total",
|
||||||
|
help: "Total number of teacher-bff requests",
|
||||||
|
labelNames: ["method", "endpoint", "status"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
registry.registerMetric(
|
||||||
|
new promClient.Histogram({
|
||||||
|
name: "teacher_bff_request_duration_seconds",
|
||||||
|
help: "Teacher-bff request duration in seconds",
|
||||||
|
labelNames: ["method", "endpoint"],
|
||||||
|
buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 3, 5],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 自动收集 Node.js 进程级指标(CPU/内存/事件循环/GC等)
|
||||||
|
promClient.collectDefaultMetrics({ register: registry });
|
||||||
|
|
||||||
|
export { registry as metricsRegistry };
|
||||||
27
services/teacher-bff/src/shared/observability/tracer.ts
Normal file
27
services/teacher-bff/src/shared/observability/tracer.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||||
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||||
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||||
|
import { env } from "../../config/env.js";
|
||||||
|
|
||||||
|
let sdk: NodeSDK | null = null;
|
||||||
|
|
||||||
|
export function initTracer(): void {
|
||||||
|
if (!env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
||||||
|
|
||||||
|
sdk = new NodeSDK({
|
||||||
|
serviceName: "teacher-bff",
|
||||||
|
traceExporter: new OTLPTraceExporter({
|
||||||
|
url: `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
|
||||||
|
}),
|
||||||
|
instrumentations: [getNodeAutoInstrumentations()],
|
||||||
|
});
|
||||||
|
|
||||||
|
sdk.start();
|
||||||
|
console.log("Tracer initialized with auto-instrumentations");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shutdownTracer(): Promise<void> {
|
||||||
|
if (sdk) {
|
||||||
|
await sdk.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
"incremental": false,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|||||||
4
uv.lock
generated
4
uv.lock
generated
@@ -21,6 +21,8 @@ dependencies = [
|
|||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "opentelemetry-api" },
|
{ name = "opentelemetry-api" },
|
||||||
|
{ name = "opentelemetry-exporter-otlp" },
|
||||||
|
{ name = "opentelemetry-instrumentation-fastapi" },
|
||||||
{ name = "opentelemetry-sdk" },
|
{ name = "opentelemetry-sdk" },
|
||||||
{ name = "prometheus-client" },
|
{ name = "prometheus-client" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
@@ -34,6 +36,8 @@ requires-dist = [
|
|||||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||||
{ name = "httpx", specifier = ">=0.27.0" },
|
{ name = "httpx", specifier = ">=0.27.0" },
|
||||||
{ name = "opentelemetry-api", specifier = ">=1.27.0" },
|
{ name = "opentelemetry-api", specifier = ">=1.27.0" },
|
||||||
|
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.27.0" },
|
||||||
|
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.48b0" },
|
||||||
{ name = "opentelemetry-sdk", specifier = ">=1.27.0" },
|
{ name = "opentelemetry-sdk", specifier = ">=1.27.0" },
|
||||||
{ name = "prometheus-client", specifier = ">=0.20.0" },
|
{ name = "prometheus-client", specifier = ">=0.20.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.9.0" },
|
{ name = "pydantic", specifier = ">=2.9.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user