first gen

This commit is contained in:
kingecg 2025-06-10 21:38:06 +08:00
commit 25a2c2de57
15 changed files with 2040 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
vendor/

153
README.md Normal file
View File

@ -0,0 +1,153 @@
# GoTiDB - 时序数据库
GoTiDB 是一个用 Go 语言编写的轻量级时序数据库,专门用于存储和查询时间序列数据。它支持高效的数据写入、查询和实时数据推送功能。
## 特性
- 高性能内存存储引擎
- WAL预写日志持久化
- REST API 接口
- WebSocket 实时数据推送
- NATS 消息系统集成
- Prometheus 指标监控
- 支持自定义标签的数据点
- 环形缓冲区数据结构
- 支持多种查询类型(最新值、所有值、持续时间)
## 安装
确保你已经安装了 Go 1.16 或更高版本。
```bash
git clone git.pyer.club/kingecg/gotidb
cd gotidb
go mod download
```
## 构建
```bash
go build -o gotidb cmd/server/main.go
```
## 运行
```bash
./gotidb [options]
```
### 可用选项
- `-rest-addr`: REST API 服务地址(默认:":8080"
- `-ws-addr`: WebSocket 服务地址(默认:":8081"
- `-metrics-addr`: 指标服务地址(默认:":8082"
- `-nats-url`: NATS 服务器地址(默认:"nats://localhost:4222"
- `-persistence`: 持久化类型none, wal默认"none"
- `-persistence-dir`: 持久化目录(默认:"./data"
- `-sync-every`: 每写入多少条数据同步一次默认100
## API 使用
### REST API
#### 写入数据
```bash
curl -X POST http://localhost:8080/api/v1/write \
-H "Content-Type: application/json" \
-d '{
"device_id": "device1",
"metric_code": "temperature",
"labels": {
"location": "room1"
},
"value": 25.5
}'
```
#### 批量写入数据
```bash
curl -X POST http://localhost:8080/api/v1/batch_write \
-H "Content-Type: application/json" \
-d '{
"points": [
{
"device_id": "device1",
"metric_code": "temperature",
"labels": {
"location": "room1"
},
"value": 25.5
},
{
"device_id": "device2",
"metric_code": "humidity",
"labels": {
"location": "room2"
},
"value": 60
}
]
}'
```
#### 查询数据
```bash
curl -X POST http://localhost:8080/api/v1/query \
-H "Content-Type: application/json" \
-d '{
"device_id": "device1",
"metric_code": "temperature",
"labels": {
"location": "room1"
},
"query_type": "latest"
}'
```
### WebSocket API
连接 WebSocket 服务:
```javascript
const ws = new WebSocket('ws://localhost:8081/ws');
// 订阅数据点
ws.send(JSON.stringify({
device_id: "device1",
metric_code: "temperature",
labels: {
location: "room1"
}
}));
// 接收数据更新
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Received update:', data);
};
```
## 监控
访问 `http://localhost:8082/metrics` 查看 Prometheus 指标。
可用指标:
- `gotidb_write_total`: 写入操作总数
- `gotidb_query_total`: 查询操作总数
- `gotidb_write_latency_seconds`: 写入操作延迟
- `gotidb_query_latency_seconds`: 查询操作延迟
- `gotidb_active_connections`: 活跃连接数
- `gotidb_data_points_count`: 数据点数量
- `gotidb_persistence_latency_seconds`: 持久化操作延迟
- `gotidb_persistence_errors_total`: 持久化错误总数
- `gotidb_messaging_latency_seconds`: 消息操作延迟
- `gotidb_messaging_errors_total`: 消息错误总数
- `gotidb_websocket_connections`: WebSocket 连接数
## 许可证
MIT License

155
cmd/server/main.go Normal file
View File

@ -0,0 +1,155 @@
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"syscall"
"time"
"git.pyer.club/kingecg/gotidb/pkg/api"
"git.pyer.club/kingecg/gotidb/pkg/manager"
"git.pyer.club/kingecg/gotidb/pkg/messaging"
"git.pyer.club/kingecg/gotidb/pkg/model"
"git.pyer.club/kingecg/gotidb/pkg/monitoring"
"git.pyer.club/kingecg/gotidb/pkg/storage"
)
var (
restAddr = flag.String("rest-addr", ":8080", "REST API服务地址")
wsAddr = flag.String("ws-addr", ":8081", "WebSocket服务地址")
metricsAddr = flag.String("metrics-addr", ":8082", "指标服务地址")
natsURL = flag.String("nats-url", "nats://localhost:4222", "NATS服务器地址")
persistenceType = flag.String("persistence", "none", "持久化类型 (none, wal)")
persistenceDir = flag.String("persistence-dir", "./data", "持久化目录")
syncEvery = flag.Int("sync-every", 100, "每写入多少条数据同步一次")
)
func main() {
flag.Parse()
// 创建存储引擎
engine := storage.NewMemoryEngine()
// 如果启用了持久化,配置持久化
if *persistenceType != "none" {
persistenceConfig := storage.PersistenceConfig{
Type: storage.PersistenceType(*persistenceType),
Directory: *persistenceDir,
SyncEvery: *syncEvery,
}
if err := engine.EnablePersistence(persistenceConfig); err != nil {
log.Fatalf("Failed to enable persistence: %v", err)
}
log.Printf("Persistence enabled: type=%s, directory=%s", *persistenceType, *persistenceDir)
}
// 创建数据管理器
dataManager := manager.NewDataManager(engine)
// 创建指标收集器
metrics := monitoring.NewMetrics()
// 创建REST API服务
restServer := api.NewRESTServer(dataManager)
// 创建WebSocket服务
wsServer := api.NewWebSocketServer(dataManager)
// 创建NATS消息系统
natsConfig := messaging.NATSConfig{
URL: *natsURL,
}
nats, err := messaging.NewNATSMessaging(natsConfig)
if err != nil {
log.Printf("Failed to create NATS messaging: %v", err)
log.Println("Continuing without NATS messaging")
} else {
// 订阅消息
err = nats.Subscribe(func(msg messaging.DataMessage) error {
// 创建数据点
id := manager.CreateDataPoint(
msg.DeviceID,
msg.MetricCode,
msg.Labels,
msg.Value,
)
// 写入数据
value := model.DataValue{
Timestamp: msg.Timestamp,
Value: msg.Value,
}
return dataManager.Write(context.Background(), id, value)
})
if err != nil {
log.Printf("Failed to subscribe to NATS: %v", err)
} else {
log.Println("NATS messaging enabled")
}
}
// 启动服务
go func() {
log.Printf("Starting REST API server on %s", *restAddr)
if err := restServer.Start(*restAddr); err != nil {
log.Fatalf("Failed to start REST API server: %v", err)
}
}()
go func() {
log.Printf("Starting WebSocket server on %s", *wsAddr)
if err := wsServer.Start(*wsAddr); err != nil {
log.Fatalf("Failed to start WebSocket server: %v", err)
}
}()
go func() {
log.Printf("Starting metrics server on %s", *metricsAddr)
if err := metrics.StartServer(*metricsAddr); err != nil {
log.Fatalf("Failed to start metrics server: %v", err)
}
}()
// 等待信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
// 优雅关闭
log.Println("Shutting down...")
// 创建关闭上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 关闭REST API服务
if err := restServer.Stop(ctx); err != nil {
log.Printf("Failed to stop REST API server: %v", err)
}
// 关闭WebSocket服务
if err := wsServer.Stop(ctx); err != nil {
log.Printf("Failed to stop WebSocket server: %v", err)
}
// 关闭NATS消息系统
if nats != nil {
if err := nats.Close(); err != nil {
log.Printf("Failed to close NATS messaging: %v", err)
}
}
// 关闭数据管理器
if err := dataManager.Close(); err != nil {
log.Printf("Failed to close data manager: %v", err)
}
log.Println("Shutdown complete")
}

View File

@ -0,0 +1,14 @@
时序数据库需求的总结及推荐类库
一、核心需求总结
1.数据存储逻辑
每个数据点仅保留最近30次变化值(环形队列覆盖机制)
精确追踪当前值的持续时间(记录值变更的起始时间戳)
2.读写性能要求
高速内存处理:微秒级写入延迟,支持高并发(>500K ops/sec)
细粒度锁:每个数据点独立锁,避免全局锁竞争
3.客户端分离
写客户端:批量提交数据,低延迟响应
读客户端:支持实时推送变化(数据变更时主动推送)
4.扩展能力
可选持久化(如WAL日志)
监控指标暴露(内存用量、写入延迟等)

49
go.mod Normal file
View File

@ -0,0 +1,49 @@
module git.pyer.club/kingecg/gotidb
go 1.20
require (
github.com/gin-gonic/gin v1.9.1
github.com/gorilla/websocket v1.5.1
github.com/nats-io/nats.go v1.31.0
github.com/prometheus/client_golang v1.17.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nats-io/nkeys v0.4.6 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

125
go.sum Normal file
View File

@ -0,0 +1,125 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/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.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
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/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/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
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/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
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/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
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/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=

257
pkg/api/rest.go Normal file
View File

@ -0,0 +1,257 @@
package api
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"git.pyer.club/kingecg/gotidb/pkg/manager"
"git.pyer.club/kingecg/gotidb/pkg/model"
)
// RESTServer REST API服务
type RESTServer struct {
dataManager *manager.DataManager
router *gin.Engine
server *http.Server
}
// WriteRequest 写入请求
type WriteRequest struct {
DeviceID string `json:"device_id"`
MetricCode string `json:"metric_code"`
Labels map[string]string `json:"labels"`
Value interface{} `json:"value"`
Timestamp *time.Time `json:"timestamp,omitempty"`
}
// BatchWriteRequest 批量写入请求
type BatchWriteRequest struct {
Points []WriteRequest `json:"points"`
}
// QueryRequest 查询请求
type QueryRequest struct {
DeviceID string `json:"device_id"`
MetricCode string `json:"metric_code"`
Labels map[string]string `json:"labels"`
QueryType string `json:"query_type"`
Params map[string]interface{} `json:"params"`
}
// NewRESTServer 创建一个新的REST API服务
func NewRESTServer(dataManager *manager.DataManager) *RESTServer {
router := gin.Default()
server := &RESTServer{
dataManager: dataManager,
router: router,
}
server.setupRoutes()
return server
}
// setupRoutes 设置路由
func (s *RESTServer) setupRoutes() {
// 健康检查
s.router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
// API版本
v1 := s.router.Group("/api/v1")
{
// 写入数据
v1.POST("/write", s.handleWrite)
// 批量写入数据
v1.POST("/batch_write", s.handleBatchWrite)
// 查询数据
v1.POST("/query", s.handleQuery)
}
}
// handleWrite 处理写入请求
func (s *RESTServer) handleWrite(c *gin.Context) {
var req WriteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request: " + err.Error(),
})
return
}
// 创建数据点
timestamp := time.Now()
if req.Timestamp != nil {
timestamp = *req.Timestamp
}
id := model.DataPointID{
DeviceID: req.DeviceID,
MetricCode: req.MetricCode,
Labels: req.Labels,
}
value := model.DataValue{
Timestamp: timestamp,
Value: req.Value,
}
// 写入数据
if err := s.dataManager.Write(c.Request.Context(), id, value); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to write data: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
// handleBatchWrite 处理批量写入请求
func (s *RESTServer) handleBatchWrite(c *gin.Context) {
var req BatchWriteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request: " + err.Error(),
})
return
}
// 创建批量写入数据
batch := make([]struct {
ID model.DataPointID
Value model.DataValue
}, len(req.Points))
for i, point := range req.Points {
timestamp := time.Now()
if point.Timestamp != nil {
timestamp = *point.Timestamp
}
batch[i].ID = model.DataPointID{
DeviceID: point.DeviceID,
MetricCode: point.MetricCode,
Labels: point.Labels,
}
batch[i].Value = model.DataValue{
Timestamp: timestamp,
Value: point.Value,
}
}
// 批量写入数据
if err := s.dataManager.BatchWrite(c.Request.Context(), batch); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to batch write data: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
// handleQuery 处理查询请求
func (s *RESTServer) handleQuery(c *gin.Context) {
var req QueryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request: " + err.Error(),
})
return
}
// 创建数据点ID
id := model.DataPointID{
DeviceID: req.DeviceID,
MetricCode: req.MetricCode,
Labels: req.Labels,
}
// 创建查询
queryType := model.QueryType(req.QueryType)
query := model.NewQuery(queryType, req.Params)
// 执行查询
result, err := s.dataManager.ExecuteQuery(c.Request.Context(), id, query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to execute query: " + err.Error(),
})
return
}
// 根据查询类型返回结果
switch queryType {
case model.QueryTypeLatest:
value, ok := result.AsLatest()
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": "Data point not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"timestamp": value.Timestamp,
"value": value.Value,
})
case model.QueryTypeAll:
values, ok := result.AsAll()
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": "Data point not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"values": values,
})
case model.QueryTypeDuration:
duration, ok := result.AsDuration()
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": "Data point not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"duration": duration,
})
default:
c.JSON(http.StatusBadRequest, gin.H{
"error": "Unsupported query type",
})
}
}
// Start 启动REST API服务
func (s *RESTServer) Start(addr string) error {
s.server = &http.Server{
Addr: addr,
Handler: s.router,
}
return s.server.ListenAndServe()
}
// Stop 停止REST API服务
func (s *RESTServer) Stop(ctx context.Context) error {
return s.server.Shutdown(ctx)
}

168
pkg/api/websocket.go Normal file
View File

@ -0,0 +1,168 @@
package api
import (
"context"
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"git.pyer.club/kingecg/gotidb/pkg/manager"
"git.pyer.club/kingecg/gotidb/pkg/model"
)
// WebSocketServer WebSocket服务
type WebSocketServer struct {
dataManager *manager.DataManager
router *gin.Engine
server *http.Server
clients map[*websocket.Conn]bool
clientsLock sync.RWMutex
upgrader websocket.Upgrader
}
// SubscriptionRequest 订阅请求
type SubscriptionRequest struct {
DeviceID string `json:"device_id"`
MetricCode string `json:"metric_code"`
Labels map[string]string `json:"labels"`
}
// DataChangeEvent 数据变更事件
type DataChangeEvent struct {
DeviceID string `json:"device_id"`
MetricCode string `json:"metric_code"`
Labels map[string]string `json:"labels"`
Timestamp time.Time `json:"timestamp"`
Value interface{} `json:"value"`
}
// NewWebSocketServer 创建一个新的WebSocket服务
func NewWebSocketServer(dataManager *manager.DataManager) *WebSocketServer {
router := gin.Default()
server := &WebSocketServer{
dataManager: dataManager,
router: router,
clients: make(map[*websocket.Conn]bool),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 允许所有来源的WebSocket连接
},
},
}
server.setupRoutes()
// 注册数据变更回调
dataManager.RegisterCallback(server.handleDataChange)
return server
}
// setupRoutes 设置路由
func (s *WebSocketServer) setupRoutes() {
// WebSocket连接
s.router.GET("/ws", s.handleWebSocket)
}
// handleWebSocket 处理WebSocket连接
func (s *WebSocketServer) handleWebSocket(c *gin.Context) {
// 升级HTTP连接为WebSocket连接
conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("Failed to upgrade connection: %v", err)
return
}
// 注册客户端
s.clientsLock.Lock()
s.clients[conn] = true
s.clientsLock.Unlock()
// 处理客户端消息
go s.handleClient(conn)
}
// handleClient 处理客户端消息
func (s *WebSocketServer) handleClient(conn *websocket.Conn) {
defer func() {
// 关闭连接
conn.Close()
// 移除客户端
s.clientsLock.Lock()
delete(s.clients, conn)
s.clientsLock.Unlock()
}()
// 读取客户端消息
for {
_, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("WebSocket error: %v", err)
}
break
}
// 解析订阅请求
var req SubscriptionRequest
if err := json.Unmarshal(message, &req); err != nil {
log.Printf("Failed to parse subscription request: %v", err)
continue
}
// 处理订阅请求
// TODO: 实现订阅逻辑
}
}
// handleDataChange 处理数据变更
func (s *WebSocketServer) handleDataChange(id model.DataPointID, value model.DataValue) {
// 创建数据变更事件
event := DataChangeEvent{
DeviceID: id.DeviceID,
MetricCode: id.MetricCode,
Labels: id.Labels,
Timestamp: value.Timestamp,
Value: value.Value,
}
// 序列化事件
data, err := json.Marshal(event)
if err != nil {
log.Printf("Failed to marshal data change event: %v", err)
return
}
// 广播事件
s.clientsLock.RLock()
for client := range s.clients {
if err := client.WriteMessage(websocket.TextMessage, data); err != nil {
log.Printf("Failed to send data change event: %v", err)
client.Close()
delete(s.clients, client)
}
}
s.clientsLock.RUnlock()
}
// Start 启动WebSocket服务
func (s *WebSocketServer) Start(addr string) error {
s.server = &http.Server{
Addr: addr,
Handler: s.router,
}
return s.server.ListenAndServe()
}
// Stop 停止WebSocket服务
func (s *WebSocketServer) Stop(ctx context.Context) error {
return s.server.Shutdown(ctx)
}

123
pkg/manager/datamanager.go Normal file
View File

@ -0,0 +1,123 @@
package manager
import (
"context"
"errors"
"sync"
"time"
"git.pyer.club/kingecg/gotidb/pkg/model"
"git.pyer.club/kingecg/gotidb/pkg/storage"
)
// DataChangeCallback 数据变更回调函数类型
type DataChangeCallback func(id model.DataPointID, value model.DataValue)
// DataManager 数据管理器
type DataManager struct {
engine storage.StorageEngine
callbacks []DataChangeCallback
callbacksLock sync.RWMutex
}
// NewDataManager 创建一个新的数据管理器
func NewDataManager(engine storage.StorageEngine) *DataManager {
return &DataManager{
engine: engine,
callbacks: make([]DataChangeCallback, 0),
}
}
// Write 写入数据
func (m *DataManager) Write(ctx context.Context, id model.DataPointID, value model.DataValue) error {
// 写入存储引擎
if err := m.engine.Write(ctx, id, value); err != nil {
return err
}
// 触发回调
m.callbacksLock.RLock()
callbacks := m.callbacks
m.callbacksLock.RUnlock()
for _, callback := range callbacks {
callback(id, value)
}
return nil
}
// BatchWrite 批量写入数据
func (m *DataManager) BatchWrite(ctx context.Context, batch []struct {
ID model.DataPointID
Value model.DataValue
}) error {
for _, item := range batch {
if err := m.Write(ctx, item.ID, item.Value); err != nil {
return err
}
}
return nil
}
// RegisterCallback 注册数据变更回调
func (m *DataManager) RegisterCallback(callback DataChangeCallback) {
m.callbacksLock.Lock()
defer m.callbacksLock.Unlock()
m.callbacks = append(m.callbacks, callback)
}
// ExecuteQuery 执行查询
func (m *DataManager) ExecuteQuery(ctx context.Context, id model.DataPointID, query model.Query) (model.Result, error) {
switch query.Type() {
case model.QueryTypeLatest:
value, err := m.engine.GetLatest(ctx, id)
if err != nil {
return nil, err
}
return model.NewLatestResult(value), nil
case model.QueryTypeAll:
values, err := m.engine.Read(ctx, id)
if err != nil {
return nil, err
}
return model.NewAllResult(values), nil
case model.QueryTypeDuration:
duration, err := m.engine.GetDuration(ctx, id)
if err != nil {
return nil, err
}
return model.NewDurationResult(duration), nil
default:
return nil, errors.New("unsupported query type")
}
}
// Close 关闭数据管理器
func (m *DataManager) Close() error {
return m.engine.Close()
}
// EnablePersistence 启用持久化
func (m *DataManager) EnablePersistence(config storage.PersistenceConfig) error {
return m.engine.EnablePersistence(config)
}
// CreateDataPoint 创建一个新的数据点
func CreateDataPoint(deviceID, metricCode string, labels map[string]string, value interface{}) (model.DataPointID, model.DataValue) {
id := model.DataPointID{
DeviceID: deviceID,
MetricCode: metricCode,
Labels: labels,
}
dataValue := model.DataValue{
Timestamp: time.Now(),
Value: value,
}
return id, dataValue
}

234
pkg/messaging/nats.go Normal file
View File

@ -0,0 +1,234 @@
package messaging
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.pyer.club/kingecg/gotidb/pkg/model"
)
const (
// DefaultStreamName 默认流名称
DefaultStreamName = "GOTIDB_DATA"
// DefaultSubject 默认主题
DefaultSubject = "gotidb.data.>"
)
// DataMessage 数据消息
type DataMessage struct {
DeviceID string `json:"device_id"`
MetricCode string `json:"metric_code"`
Labels map[string]string `json:"labels"`
Timestamp time.Time `json:"timestamp"`
Value interface{} `json:"value"`
}
// MessageHandler 消息处理函数类型
type MessageHandler func(msg DataMessage) error
// NATSMessaging NATS消息系统
type NATSMessaging struct {
conn *nats.Conn
js jetstream.JetStream
stream jetstream.Stream
consumer jetstream.Consumer
handlers []MessageHandler
subContext context.Context
subCancel context.CancelFunc
}
// NATSConfig NATS配置
type NATSConfig struct {
URL string
StreamName string
Subject string
}
// NewNATSMessaging 创建一个新的NATS消息系统
func NewNATSMessaging(config NATSConfig) (*NATSMessaging, error) {
// 使用默认值
if config.StreamName == "" {
config.StreamName = DefaultStreamName
}
if config.Subject == "" {
config.Subject = DefaultSubject
}
// 连接NATS服务器
conn, err := nats.Connect(config.URL)
if err != nil {
return nil, fmt.Errorf("failed to connect to NATS: %v", err)
}
// 创建JetStream上下文
js, err := jetstream.New(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("failed to create JetStream context: %v", err)
}
// 创建或获取流
stream, err := js.Stream(context.Background(), config.StreamName)
if err != nil {
// 如果流不存在,创建一个新的流
cfg := jetstream.StreamConfig{
Name: config.StreamName,
Subjects: []string{config.Subject},
}
stream, err = js.CreateStream(context.Background(), cfg)
if err != nil {
conn.Close()
return nil, fmt.Errorf("failed to create stream: %v", err)
}
}
return &NATSMessaging{
conn: conn,
js: js,
stream: stream,
handlers: make([]MessageHandler, 0),
}, nil
}
// Publish 发布消息
func (n *NATSMessaging) Publish(ctx context.Context, id model.DataPointID, value model.DataValue) error {
// 创建消息
msg := DataMessage{
DeviceID: id.DeviceID,
MetricCode: id.MetricCode,
Labels: id.Labels,
Timestamp: value.Timestamp,
Value: value.Value,
}
// 序列化消息
data, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("failed to marshal message: %v", err)
}
// 构建主题
subject := fmt.Sprintf("gotidb.data.%s.%s", id.DeviceID, id.MetricCode)
// 发布消息
_, err = n.js.Publish(ctx, subject, data)
if err != nil {
return fmt.Errorf("failed to publish message: %v", err)
}
return nil
}
// BatchPublish 批量发布消息
func (n *NATSMessaging) BatchPublish(ctx context.Context, batch []struct {
ID model.DataPointID
Value model.DataValue
}) error {
for _, item := range batch {
if err := n.Publish(ctx, item.ID, item.Value); err != nil {
return err
}
}
return nil
}
// Subscribe 订阅消息
func (n *NATSMessaging) Subscribe(handler MessageHandler) error {
// 添加处理函数
n.handlers = append(n.handlers, handler)
// 如果已经有订阅,不需要重新订阅
if n.subContext != nil {
return nil
}
// 创建消费者
consumerConfig := jetstream.ConsumerConfig{
Durable: "gotidb-consumer",
AckPolicy: jetstream.AckExplicitPolicy,
DeliverPolicy: jetstream.DeliverAllPolicy,
}
var err error
n.consumer, err = n.stream.CreateOrUpdateConsumer(context.Background(), consumerConfig)
if err != nil {
return fmt.Errorf("failed to create consumer: %v", err)
}
// 创建可取消的上下文
n.subContext, n.subCancel = context.WithCancel(context.Background())
// 开始消费消息
go n.consumeMessages()
return nil
}
// consumeMessages 消费消息
func (n *NATSMessaging) consumeMessages() {
// 创建消费迭代器
iter, err := n.consumer.Messages()
if err != nil {
log.Printf("Failed to create message iterator: %v", err)
return
}
// 消费消息
for {
select {
case <-n.subContext.Done():
return
default:
msg, err := iter.Next()
if err != nil {
log.Printf("Failed to get next message: %v", err)
continue
}
// 解析消息
var dataMsg DataMessage
if err := json.Unmarshal(msg.Data(), &dataMsg); err != nil {
log.Printf("Failed to unmarshal message: %v", err)
msg.Ack()
continue
}
// 调用所有处理函数
for _, handler := range n.handlers {
if err := handler(dataMsg); err != nil {
log.Printf("Failed to handle message: %v", err)
}
}
// 确认消息
msg.Ack()
}
}
}
// Unsubscribe 取消订阅
func (n *NATSMessaging) Unsubscribe() {
if n.subCancel != nil {
n.subCancel()
n.subContext = nil
n.subCancel = nil
}
}
// Close 关闭连接
func (n *NATSMessaging) Close() error {
// 取消订阅
n.Unsubscribe()
// 关闭连接
n.conn.Close()
return nil
}

124
pkg/model/datapoint.go Normal file
View File

@ -0,0 +1,124 @@
package model
import (
"fmt"
"sync"
"time"
)
// DataPointID 数据点标识
type DataPointID struct {
DeviceID string // 设备ID
MetricCode string // 指标代码
Labels map[string]string // 自定义标签
}
// String 返回数据点标识的字符串表示
func (id DataPointID) String() string {
return fmt.Sprintf("%s:%s:%v", id.DeviceID, id.MetricCode, id.Labels)
}
// DataValue 数据值
type DataValue struct {
Timestamp time.Time // 时间戳
Value interface{} // 支持 int64 或 float64
}
// CircularBuffer 环形缓冲区
type CircularBuffer struct {
Values [30]DataValue // 固定大小为30的环形缓冲区
Head int // 当前写入位置
Lock sync.RWMutex // 细粒度锁
}
// NewCircularBuffer 创建一个新的环形缓冲区
func NewCircularBuffer() *CircularBuffer {
return &CircularBuffer{
Head: -1, // 初始化为-1表示缓冲区为空
}
}
// Write 写入一个新的数据值
func (cb *CircularBuffer) Write(value DataValue) {
cb.Lock.Lock()
defer cb.Lock.Unlock()
cb.Head = (cb.Head + 1) % len(cb.Values)
cb.Values[cb.Head] = value
}
// Read 读取所有数据值,按时间顺序返回
func (cb *CircularBuffer) Read() []DataValue {
cb.Lock.RLock()
defer cb.Lock.RUnlock()
if cb.Head == -1 {
return []DataValue{} // 缓冲区为空
}
result := make([]DataValue, 0, len(cb.Values))
// 从最旧的数据开始
oldest := (cb.Head + 1) % len(cb.Values)
// 如果缓冲区未满oldest可能指向未初始化的元素
// 我们需要检查并跳过这些元素
for i := 0; i < len(cb.Values); i++ {
idx := (oldest + i) % len(cb.Values)
// 跳过未初始化的元素
if cb.Values[idx].Timestamp.IsZero() {
continue
}
result = append(result, cb.Values[idx])
}
return result
}
// GetLatest 获取最新的数据值
func (cb *CircularBuffer) GetLatest() (DataValue, bool) {
cb.Lock.RLock()
defer cb.Lock.RUnlock()
if cb.Head == -1 {
return DataValue{}, false // 缓冲区为空
}
return cb.Values[cb.Head], true
}
// GetDuration 获取最新值的持续时间
func (cb *CircularBuffer) GetDuration() time.Duration {
cb.Lock.RLock()
defer cb.Lock.RUnlock()
if cb.Head == -1 {
return 0 // 缓冲区为空
}
latest := cb.Values[cb.Head]
// 如果只有一个值,无法计算持续时间
if len(cb.Values) == 1 {
return 0
}
// 查找前一个不同的值
for i := 1; i < len(cb.Values); i++ {
idx := (cb.Head - i + len(cb.Values)) % len(cb.Values)
prev := cb.Values[idx]
// 跳过未初始化的元素
if prev.Timestamp.IsZero() {
continue
}
// 如果值不同,计算持续时间
if prev.Value != latest.Value {
return latest.Timestamp.Sub(prev.Timestamp)
}
}
// 所有值都相同,无法计算持续时间
return 0
}

117
pkg/model/query.go Normal file
View File

@ -0,0 +1,117 @@
package model
import (
"context"
)
// QueryType 查询类型
type QueryType string
const (
// QueryTypeLatest 获取最新值
QueryTypeLatest QueryType = "latest"
// QueryTypeAll 获取所有值
QueryTypeAll QueryType = "all"
// QueryTypeDuration 获取持续时间
QueryTypeDuration QueryType = "duration"
)
// Query 查询接口
type Query interface {
Type() QueryType
Params() map[string]interface{}
}
// Result 查询结果接口
type Result interface {
IsEmpty() bool
AsLatest() (DataValue, bool)
AsAll() ([]DataValue, bool)
AsDuration() (float64, bool)
}
// QueryExecutor 查询执行器接口
type QueryExecutor interface {
Execute(ctx context.Context, query Query) (Result, error)
}
// BaseQuery 基础查询实现
type BaseQuery struct {
queryType QueryType
params map[string]interface{}
}
// NewQuery 创建一个新的查询
func NewQuery(queryType QueryType, params map[string]interface{}) Query {
return &BaseQuery{
queryType: queryType,
params: params,
}
}
// Type 返回查询类型
func (q *BaseQuery) Type() QueryType {
return q.queryType
}
// Params 返回查询参数
func (q *BaseQuery) Params() map[string]interface{} {
return q.params
}
// BaseResult 基础查询结果实现
type BaseResult struct {
latest *DataValue
all []DataValue
duration *float64
}
// NewLatestResult 创建一个最新值查询结果
func NewLatestResult(value DataValue) Result {
return &BaseResult{
latest: &value,
}
}
// NewAllResult 创建一个所有值查询结果
func NewAllResult(values []DataValue) Result {
return &BaseResult{
all: values,
}
}
// NewDurationResult 创建一个持续时间查询结果
func NewDurationResult(duration float64) Result {
return &BaseResult{
duration: &duration,
}
}
// IsEmpty 检查结果是否为空
func (r *BaseResult) IsEmpty() bool {
return r.latest == nil && len(r.all) == 0 && r.duration == nil
}
// AsLatest 将结果转换为最新值
func (r *BaseResult) AsLatest() (DataValue, bool) {
if r.latest != nil {
return *r.latest, true
}
return DataValue{}, false
}
// AsAll 将结果转换为所有值
func (r *BaseResult) AsAll() ([]DataValue, bool) {
if len(r.all) > 0 {
return r.all, true
}
return nil, false
}
// AsDuration 将结果转换为持续时间
func (r *BaseResult) AsDuration() (float64, bool) {
if r.duration != nil {
return *r.duration, true
}
return 0, false
}

182
pkg/monitoring/metrics.go Normal file
View File

@ -0,0 +1,182 @@
package monitoring
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Metrics 指标收集器
type Metrics struct {
registry *prometheus.Registry
writeCounter prometheus.Counter
queryCounter prometheus.Counter
writeLatency prometheus.Histogram
queryLatency prometheus.Histogram
activeConnections prometheus.Gauge
dataPointsCount prometheus.Gauge
persistenceLatency prometheus.Histogram
persistenceErrors prometheus.Counter
messagingLatency prometheus.Histogram
messagingErrors prometheus.Counter
websocketConnections prometheus.Gauge
}
// NewMetrics 创建一个新的指标收集器
func NewMetrics() *Metrics {
registry := prometheus.NewRegistry()
writeCounter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "gotidb_write_total",
Help: "Total number of write operations",
})
queryCounter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "gotidb_query_total",
Help: "Total number of query operations",
})
writeLatency := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "gotidb_write_latency_seconds",
Help: "Write operation latency in seconds",
Buckets: prometheus.DefBuckets,
})
queryLatency := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "gotidb_query_latency_seconds",
Help: "Query operation latency in seconds",
Buckets: prometheus.DefBuckets,
})
activeConnections := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "gotidb_active_connections",
Help: "Number of active connections",
})
dataPointsCount := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "gotidb_data_points_count",
Help: "Number of data points in the database",
})
persistenceLatency := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "gotidb_persistence_latency_seconds",
Help: "Persistence operation latency in seconds",
Buckets: prometheus.DefBuckets,
})
persistenceErrors := prometheus.NewCounter(prometheus.CounterOpts{
Name: "gotidb_persistence_errors_total",
Help: "Total number of persistence errors",
})
messagingLatency := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "gotidb_messaging_latency_seconds",
Help: "Messaging operation latency in seconds",
Buckets: prometheus.DefBuckets,
})
messagingErrors := prometheus.NewCounter(prometheus.CounterOpts{
Name: "gotidb_messaging_errors_total",
Help: "Total number of messaging errors",
})
websocketConnections := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "gotidb_websocket_connections",
Help: "Number of active WebSocket connections",
})
// 注册指标
registry.MustRegister(writeCounter)
registry.MustRegister(queryCounter)
registry.MustRegister(writeLatency)
registry.MustRegister(queryLatency)
registry.MustRegister(activeConnections)
registry.MustRegister(dataPointsCount)
registry.MustRegister(persistenceLatency)
registry.MustRegister(persistenceErrors)
registry.MustRegister(messagingLatency)
registry.MustRegister(messagingErrors)
registry.MustRegister(websocketConnections)
return &Metrics{
registry: registry,
writeCounter: writeCounter,
queryCounter: queryCounter,
writeLatency: writeLatency,
queryLatency: queryLatency,
activeConnections: activeConnections,
dataPointsCount: dataPointsCount,
persistenceLatency: persistenceLatency,
persistenceErrors: persistenceErrors,
messagingLatency: messagingLatency,
messagingErrors: messagingErrors,
websocketConnections: websocketConnections,
}
}
// IncrementWriteCounter 增加写入计数器
func (m *Metrics) IncrementWriteCounter() {
m.writeCounter.Inc()
}
// IncrementQueryCounter 增加查询计数器
func (m *Metrics) IncrementQueryCounter() {
m.queryCounter.Inc()
}
// ObserveWriteLatency 观察写入延迟
func (m *Metrics) ObserveWriteLatency(seconds float64) {
m.writeLatency.Observe(seconds)
}
// ObserveQueryLatency 观察查询延迟
func (m *Metrics) ObserveQueryLatency(seconds float64) {
m.queryLatency.Observe(seconds)
}
// SetActiveConnections 设置活跃连接数
func (m *Metrics) SetActiveConnections(count float64) {
m.activeConnections.Set(count)
}
// SetDataPointsCount 设置数据点数量
func (m *Metrics) SetDataPointsCount(count float64) {
m.dataPointsCount.Set(count)
}
// ObservePersistenceLatency 观察持久化延迟
func (m *Metrics) ObservePersistenceLatency(seconds float64) {
m.persistenceLatency.Observe(seconds)
}
// IncrementPersistenceErrors 增加持久化错误计数器
func (m *Metrics) IncrementPersistenceErrors() {
m.persistenceErrors.Inc()
}
// ObserveMessagingLatency 观察消息延迟
func (m *Metrics) ObserveMessagingLatency(seconds float64) {
m.messagingLatency.Observe(seconds)
}
// IncrementMessagingErrors 增加消息错误计数器
func (m *Metrics) IncrementMessagingErrors() {
m.messagingErrors.Inc()
}
// SetWebsocketConnections 设置WebSocket连接数
func (m *Metrics) SetWebsocketConnections(count float64) {
m.websocketConnections.Set(count)
}
// Handler 返回Prometheus HTTP处理器
func (m *Metrics) Handler() http.Handler {
return promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{})
}
// StartServer 启动指标服务器
func (m *Metrics) StartServer(addr string) error {
http.Handle("/metrics", m.Handler())
return http.ListenAndServe(addr, nil)
}

169
pkg/storage/engine.go Normal file
View File

@ -0,0 +1,169 @@
package storage
import (
"context"
"sync"
"git.pyer.club/kingecg/gotidb/pkg/model"
)
// PersistenceType 持久化类型
type PersistenceType string
const (
// PersistenceTypeNone 不持久化
PersistenceTypeNone PersistenceType = "none"
// PersistenceTypeWAL 使用WAL日志持久化
PersistenceTypeWAL PersistenceType = "wal"
)
// PersistenceConfig 持久化配置
type PersistenceConfig struct {
Type PersistenceType // 持久化类型
Directory string // 持久化目录
SyncEvery int // 每写入多少条数据同步一次
}
// StorageEngine 存储引擎接口
type StorageEngine interface {
// Write 写入数据
Write(ctx context.Context, id model.DataPointID, value model.DataValue) error
// Read 读取数据
Read(ctx context.Context, id model.DataPointID) ([]model.DataValue, error)
// GetLatest 获取最新数据
GetLatest(ctx context.Context, id model.DataPointID) (model.DataValue, error)
// GetDuration 获取持续时间
GetDuration(ctx context.Context, id model.DataPointID) (float64, error)
// EnablePersistence 启用持久化
EnablePersistence(config PersistenceConfig) error
// Close 关闭存储引擎
Close() error
}
// MemoryEngine 内存存储引擎
type MemoryEngine struct {
data map[string]*model.CircularBuffer // 数据存储
dataLock sync.RWMutex // 数据锁
persister Persister // 持久化器
}
// NewMemoryEngine 创建一个新的内存存储引擎
func NewMemoryEngine() *MemoryEngine {
return &MemoryEngine{
data: make(map[string]*model.CircularBuffer),
}
}
// Write 写入数据
func (e *MemoryEngine) Write(ctx context.Context, id model.DataPointID, value model.DataValue) error {
key := id.String()
e.dataLock.RLock()
buffer, exists := e.data[key]
e.dataLock.RUnlock()
if !exists {
// 如果数据点不存在,创建一个新的环形缓冲区
buffer = model.NewCircularBuffer()
e.dataLock.Lock()
e.data[key] = buffer
e.dataLock.Unlock()
}
// 写入数据
buffer.Write(value)
// 如果启用了持久化写入WAL日志
if e.persister != nil {
if err := e.persister.Write(id, value); err != nil {
return err
}
}
return nil
}
// Read 读取数据
func (e *MemoryEngine) Read(ctx context.Context, id model.DataPointID) ([]model.DataValue, error) {
key := id.String()
e.dataLock.RLock()
buffer, exists := e.data[key]
e.dataLock.RUnlock()
if !exists {
return []model.DataValue{}, nil
}
return buffer.Read(), nil
}
// GetLatest 获取最新数据
func (e *MemoryEngine) GetLatest(ctx context.Context, id model.DataPointID) (model.DataValue, error) {
key := id.String()
e.dataLock.RLock()
buffer, exists := e.data[key]
e.dataLock.RUnlock()
if !exists {
return model.DataValue{}, nil
}
value, exists := buffer.GetLatest()
if !exists {
return model.DataValue{}, nil
}
return value, nil
}
// GetDuration 获取持续时间
func (e *MemoryEngine) GetDuration(ctx context.Context, id model.DataPointID) (float64, error) {
key := id.String()
e.dataLock.RLock()
buffer, exists := e.data[key]
e.dataLock.RUnlock()
if !exists {
return 0, nil
}
duration := buffer.GetDuration()
return duration.Seconds(), nil
}
// EnablePersistence 启用持久化
func (e *MemoryEngine) EnablePersistence(config PersistenceConfig) error {
var persister Persister
var err error
switch config.Type {
case PersistenceTypeWAL:
persister, err = NewWALPersister(config.Directory, config.SyncEvery)
case PersistenceTypeNone:
// 不启用持久化
e.persister = nil
return nil
default:
// 默认不启用持久化
e.persister = nil
return nil
}
if err != nil {
return err
}
e.persister = persister
return nil
}
// Close 关闭存储引擎
func (e *MemoryEngine) Close() error {
if e.persister != nil {
return e.persister.Close()
}
return nil
}

169
pkg/storage/persister.go Normal file
View File

@ -0,0 +1,169 @@
package storage
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"git.pyer.club/kingecg/gotidb/pkg/model"
)
// Persister 持久化接口
type Persister interface {
// Write 写入数据
Write(id model.DataPointID, value model.DataValue) error
// Close 关闭持久化器
Close() error
}
// WALEntry WAL日志条目
type WALEntry struct {
Timestamp time.Time `json:"timestamp"`
DeviceID string `json:"device_id"`
MetricCode string `json:"metric_code"`
Labels map[string]string `json:"labels"`
Value interface{} `json:"value"`
}
// WALPersister WAL持久化器
type WALPersister struct {
directory string // WAL日志目录
file *os.File // 当前WAL文件
writer *bufio.Writer
mutex sync.Mutex
syncEvery int // 每写入多少条数据同步一次
count int // 当前写入计数
}
// NewWALPersister 创建一个新的WAL持久化器
func NewWALPersister(directory string, syncEvery int) (*WALPersister, error) {
// 确保目录存在
if err := os.MkdirAll(directory, 0755); err != nil {
return nil, fmt.Errorf("failed to create WAL directory: %v", err)
}
// 创建新的WAL文件
filename := filepath.Join(directory, fmt.Sprintf("wal-%d.log", time.Now().UnixNano()))
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to create WAL file: %v", err)
}
return &WALPersister{
directory: directory,
file: file,
writer: bufio.NewWriter(file),
syncEvery: syncEvery,
}, nil
}
// Write 写入数据到WAL日志
func (w *WALPersister) Write(id model.DataPointID, value model.DataValue) error {
w.mutex.Lock()
defer w.mutex.Unlock()
// 创建WAL条目
entry := WALEntry{
Timestamp: value.Timestamp,
DeviceID: id.DeviceID,
MetricCode: id.MetricCode,
Labels: id.Labels,
Value: value.Value,
}
// 序列化为JSON
data, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshal WAL entry: %v", err)
}
// 写入数据
if _, err := w.writer.Write(data); err != nil {
return fmt.Errorf("failed to write WAL entry: %v", err)
}
if err := w.writer.WriteByte('\n'); err != nil {
return fmt.Errorf("failed to write newline: %v", err)
}
// 增加计数
w.count++
// 检查是否需要同步
if w.count >= w.syncEvery {
if err := w.sync(); err != nil {
return fmt.Errorf("failed to sync WAL: %v", err)
}
w.count = 0
}
return nil
}
// sync 同步WAL日志到磁盘
func (w *WALPersister) sync() error {
if err := w.writer.Flush(); err != nil {
return err
}
return w.file.Sync()
}
// Close 关闭WAL持久化器
func (w *WALPersister) Close() error {
w.mutex.Lock()
defer w.mutex.Unlock()
// 刷新缓冲区
if err := w.writer.Flush(); err != nil {
return fmt.Errorf("failed to flush WAL buffer: %v", err)
}
// 同步到磁盘
if err := w.file.Sync(); err != nil {
return fmt.Errorf("failed to sync WAL file: %v", err)
}
// 关闭文件
return w.file.Close()
}
// LoadWAL 从WAL日志恢复数据
func LoadWAL(directory string) ([]WALEntry, error) {
var entries []WALEntry
// 获取所有WAL文件
files, err := filepath.Glob(filepath.Join(directory, "wal-*.log"))
if err != nil {
return nil, fmt.Errorf("failed to list WAL files: %v", err)
}
// 按文件名排序(文件名包含时间戳)
// TODO: 实现文件排序
// 读取每个文件
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return nil, fmt.Errorf("failed to open WAL file %s: %v", file, err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
var entry WALEntry
if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
return nil, fmt.Errorf("failed to unmarshal WAL entry: %v", err)
}
entries = append(entries, entry)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to scan WAL file %s: %v", file, err)
}
}
return entries, nil
}