summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRikki <i@rikki.moe>2025-05-12 15:50:39 +0800
committerRikki <i@rikki.moe>2025-05-12 15:50:39 +0800
commitce8dbb7686bd1d9729b0396d7557db8126fe5cae (patch)
treed57872feae02e8b4e2c798580b2b015414b7f419
parent01d8c1c87f545eb1c14221f7a3e5820fc5055496 (diff)
use influx db insteadHEADmaster
-rw-r--r--cmd/v2stat/daemon.go45
-rw-r--r--cmd/v2stat/main.go192
-rw-r--r--def.go66
-rw-r--r--go.mod34
-rw-r--r--go.sum95
-rwxr-xr-xv2stat.amd64bin0 -> 19730756 bytes
-rw-r--r--v2stat.go234
7 files changed, 119 insertions, 547 deletions
diff --git a/cmd/v2stat/daemon.go b/cmd/v2stat/daemon.go
deleted file mode 100644
index 8c5e91f..0000000
--- a/cmd/v2stat/daemon.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package main
-
-import (
- "context"
- "os"
- "os/signal"
- "syscall"
- "time"
-
- "go.rikki.moe/v2stat"
-)
-
-func runDaemon(v2s *v2stat.V2Stat) {
- if err := v2s.InitDB(); err != nil {
- logger.Fatalf("Failed to initialize database: %v", err)
- }
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- sigCh := make(chan os.Signal, 1)
- signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
- defer signal.Stop(sigCh)
-
- ticker := time.NewTicker(time.Duration(*flagInterval) * time.Second)
- defer ticker.Stop()
-
- for {
- logger.Info("Recording stats...")
- if err := v2s.RecordNow(ctx); err != nil {
- logger.Errorf("Failed to record stats: %v", err)
- }
-
- select {
- case <-ticker.C:
- continue
- case <-sigCh:
- logger.Info("Received shutdown signal, exiting.")
- return
- case <-ctx.Done():
- logger.Info("Context canceled, exiting.")
- return
- }
- }
-}
diff --git a/cmd/v2stat/main.go b/cmd/v2stat/main.go
index ce0c3ed..962f54c 100644
--- a/cmd/v2stat/main.go
+++ b/cmd/v2stat/main.go
@@ -1,159 +1,113 @@
package main
import (
- "database/sql"
+ "context"
"flag"
- "fmt"
"os"
+ "os/signal"
+ "syscall"
+ "time"
- "github.com/jedib0t/go-pretty/table"
- "github.com/jedib0t/go-pretty/text"
- _ "github.com/mattn/go-sqlite3"
+ "github.com/influxdata/influxdb-client-go/v2"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
- "go.rikki.moe/v2stat"
+ "go.rikki.moe/v2stat/command"
)
var (
- flagInterval = flag.Int("interval", 300, "Interval in seconds to record stats")
- flagDatabase = flag.String("db", "", "Path to SQLite database")
- flagServer = flag.String("server", "127.0.0.1:8080", "V2Ray API server address")
- flagLogLevel = flag.String("log-level", "info", "Log level (debug, info, warn, error, fatal, panic)")
+ flagServerName = flag.String("name", "", "Name of the server")
+ flagInterval = flag.Int("interval", 300, "Interval in seconds to record stats")
+ flagInflux = flag.String("influx", "", "URL to InfluxDB database")
+ flagInfluxToken = flag.String("token", "", "InfluxDB token")
+ flagOrg = flag.String("org", "", "InfluxDB organization")
+ flagBucket = flag.String("bucket", "", "InfluxDB bucket")
+ flagServer = flag.String("server", "127.0.0.1:8080", "V2Ray API server address")
+ flagLogLevel = flag.String("log-level", "info", "Log level (debug, info, warn, error, fatal, panic)")
)
-var DefaultDBPaths = []string{
- "v2stat.db",
- "traffic.db",
- "/var/lib/v2stat/traffic.db",
- "/usr/local/share/v2stat/traffic.db",
- "/opt/apps/v2stat/traffic.db",
-}
-
var logger *logrus.Logger
func main() {
flag.Parse()
- args := flag.Args()
-
- if len(args) < 1 {
- fmt.Println("Usage: v2stat <command> [args]")
- fmt.Println("Available commands: daemon, query")
- os.Exit(1)
- }
logger = setupLogger(*flagLogLevel)
- db := setupDatabase(logger, *flagDatabase)
-
- v2s := v2stat.NewV2Stat(logger, db, nil)
- defer v2s.Close()
- switch args[0] {
- case "daemon":
- conn, err := grpc.NewClient(*flagServer, grpc.WithInsecure())
+ // Use hostname as default server name if not provided
+ servername := *flagServerName
+ if servername == "" {
+ hostname, err := os.Hostname()
if err != nil {
- logger.Fatalf("Failed to dial gRPC server: %v", err)
+ logger.Fatalf("Failed to get hostname: %v", err)
}
- v2s.SetConn(conn)
- runDaemon(v2s)
-
- case "query":
- handleQuery(v2s, args[1:])
-
- default:
- fmt.Println("Unknown command:", args[0])
- fmt.Println("Available commands: daemon, query")
- os.Exit(1)
+ servername = hostname
}
-}
+ logger.Infof("Using server name: %s", servername)
-func setupLogger(levelStr string) *logrus.Logger {
- level, err := logrus.ParseLevel(levelStr)
+ // Set up gRPC connection to V2Ray API server
+ conn, err := grpc.NewClient(*flagServer, grpc.WithInsecure())
if err != nil {
- logrus.Fatalf("Invalid log level: %v", err)
+ logger.Fatalf("Failed to connect to V2Ray API server: %v", err)
}
- logger := logrus.New()
- logger.SetLevel(level)
- return logger
-}
-
-func setupDatabase(logger *logrus.Logger, dbpath string) *sql.DB {
- if dbpath == "" {
- for _, path := range DefaultDBPaths {
- if _, err := os.Stat(path); err == nil {
- dbpath = path
- break
- }
- }
- if dbpath == "" {
- logger.Fatal("No database path provided and no default database found.")
- }
+ defer conn.Close()
+ client := command.NewStatsServiceClient(conn)
+ if client == nil {
+ logger.Fatalf("Failed to create V2Ray API client")
}
- logger.Infof("Using database: %s", dbpath)
- db, err := sql.Open("sqlite3", dbpath)
- if err != nil {
- logger.Fatalf("Failed to open database: %v", err)
- }
- return db
-}
+ // Set up InfluxDB client
+ influxClient := influxdb2.NewClient(*flagInflux, *flagInfluxToken)
+ defer influxClient.Close()
-func handleQuery(v2s *v2stat.V2Stat, args []string) {
- if len(args) == 0 {
- fmt.Println("Usage: v2stat query <connection_name>")
- fmt.Println("Available connections:")
- conns, err := v2s.QueryConn()
- if err != nil {
- logger.Fatalf("Failed to query connection: %v", err)
- }
- for _, c := range conns {
- fmt.Printf("\t%s\n", c.String())
- }
- return
- }
+ // Create a new point batch
+ bp := influxClient.WriteAPIBlocking(*flagOrg, *flagBucket)
- connStr := args[0]
- conn, ok := v2stat.ParseConnInfo(connStr)
- if !ok {
- logger.Fatalf("Invalid connection format: %s", connStr)
- }
+ ticker := time.NewTicker(time.Duration(*flagInterval) * time.Second)
+ defer ticker.Stop()
+ killsig := make(chan os.Signal, 1)
+ signal.Notify(killsig, syscall.SIGINT, syscall.SIGTERM)
- stats, err := v2s.QueryStatsHourly(&conn)
- if err != nil {
- logger.Fatalf("Failed to query stats: %v", err)
- }
-
- printStatsTable(stats)
-}
-
-func printStatsTable(stats []v2stat.TrafficStat) {
- tb := table.NewWriter()
- tb.SetOutputMirror(os.Stdout)
- tb.AppendHeader(table.Row{"Time", "Downlink", "Uplink"})
+ for {
+ now := time.Now()
+ stats, err := client.QueryStats(context.Background(), &command.QueryStatsRequest{
+ Reset_: true,
+ })
+ if err != nil {
+ logger.Errorf("Failed to get stats: %v", err)
+ goto LOOP_FINAL
+ }
- var totalDown, totalUp int64
+ // Write stats to InfluxDB
+ for _, stat := range stats.Stat {
+ point := influxdb2.NewPoint(
+ "v2ray_stats",
+ map[string]string{"server": servername, "stat": stat.Name},
+ map[string]interface{}{"value": stat.Value},
+ now,
+ )
+ if err := bp.WritePoint(context.Background(), point); err != nil {
+ logger.Errorf("Failed to write point to InfluxDB: %v", err)
+ }
+ }
- for _, stat := range stats {
- totalDown += stat.Downlink
- totalUp += stat.Uplink
- tb.AppendRow(table.Row{stat.Time, sizeToHuman(stat.Downlink), sizeToHuman(stat.Uplink)})
+LOOP_FINAL:
+ select {
+ case <-ticker.C:
+ continue
+ case sig := <-killsig:
+ logger.Infof("Received signal: %s", sig)
+ return
+ }
}
- tb.AppendFooter(table.Row{"Total", sizeToHuman(totalDown), sizeToHuman(totalUp)})
- style := table.StyleLight
- style.Format.Footer = text.FormatDefault
- tb.SetStyle(style)
- tb.Render()
}
-func sizeToHuman(size int64) string {
- units := []string{"B", "KiB", "MiB", "GiB", "TiB"}
- value := float64(size)
- for _, unit := range units {
- if value < 1024 {
- return fmt.Sprintf("%7.2f %s", value, unit)
- }
- value /= 1024
+func setupLogger(levelStr string) *logrus.Logger {
+ level, err := logrus.ParseLevel(levelStr)
+ if err != nil {
+ logrus.Fatalf("Invalid log level: %v", err)
}
- return fmt.Sprintf("%7.2f %s", value, "PiB")
+ logger := logrus.New()
+ logger.SetLevel(level)
+ return logger
}
diff --git a/def.go b/def.go
deleted file mode 100644
index e9951ad..0000000
--- a/def.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package v2stat
-
-import (
- "strings"
-)
-
-type TrafficDirection int
-
-const (
- DirectionDownlink TrafficDirection = iota
- DirectionUplink
-)
-
-type ConnectionType int
-
-const (
- ConnTypeUser ConnectionType = iota
- ConnTypeInbound
- ConnTypeOutbound
-)
-
-type ConnInfo struct {
- Type ConnectionType `json:"type"`
- Name string `json:"name"`
-}
-
-type TrafficStat struct {
- Time string `json:"time"`
- Downlink int64 `json:"downlink"`
- Uplink int64 `json:"uplink"`
-}
-
-func (ci *ConnInfo) String() string {
- switch ci.Type {
- case ConnTypeUser:
- return "user:" + ci.Name
- case ConnTypeInbound:
- return "inbound:" + ci.Name
- case ConnTypeOutbound:
- return "outbound:" + ci.Name
- default:
- return "unknown:" + ci.Name
- }
-}
-
-func ParseConnInfo(s string) (ConnInfo, bool) {
- parts := strings.Split(s, ":")
- if len(parts) != 2 {
- return ConnInfo{}, false
- }
- var connType ConnectionType
- switch parts[0] {
- case "user":
- connType = ConnTypeUser
- case "inbound":
- connType = ConnTypeInbound
- case "outbound":
- connType = ConnTypeOutbound
- default:
- return ConnInfo{}, false
- }
- return ConnInfo{
- Type: connType,
- Name: parts[1],
- }, true
-} \ No newline at end of file
diff --git a/go.mod b/go.mod
index 5b82f17..f85e6f4 100644
--- a/go.mod
+++ b/go.mod
@@ -2,42 +2,26 @@ module go.rikki.moe/v2stat
go 1.24.2
-require google.golang.org/grpc v1.71.1
+require (
+ github.com/influxdata/influxdb-client-go/v2 v2.14.0
+ google.golang.org/grpc v1.71.1
+)
require (
github.com/adrg/xdg v0.5.3 // indirect
- github.com/andybalholm/brotli v1.1.0 // indirect
- github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
- github.com/go-openapi/errors v0.22.0 // indirect
- github.com/go-openapi/strfmt v0.23.0 // indirect
- github.com/gofiber/fiber/v2 v2.52.6 // indirect
- github.com/gofiber/template v1.8.3 // indirect
- github.com/gofiber/template/html/v2 v2.1.3 // indirect
- github.com/gofiber/utils v1.1.0 // indirect
+ github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
- github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect
- github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
- github.com/klauspost/compress v1.17.9 // indirect
- github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-runewidth v0.0.16 // indirect
- github.com/mattn/go-sqlite3 v1.14.27 // indirect
- github.com/mitchellh/mapstructure v1.5.0 // indirect
- github.com/oklog/ulid v1.3.1 // indirect
- github.com/rivo/uniseg v0.4.7 // indirect
- github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasthttp v1.51.0 // indirect
- github.com/valyala/tcplisten v1.0.0 // indirect
- go.mongodb.org/mongo-driver v1.14.0 // indirect
+ github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
+ github.com/oapi-codegen/runtime v1.0.0 // indirect
)
require (
- github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/sirupsen/logrus v1.9.3
github.com/v2fly/v2ray-core/v5 v5.30.0
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
- google.golang.org/protobuf v1.36.5 // indirect
+ google.golang.org/protobuf v1.36.5
)
diff --git a/go.sum b/go.sum
index 0714350..62e6852 100644
--- a/go.sum
+++ b/go.sum
@@ -1,88 +1,67 @@
+github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
-github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
-github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
-github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
-github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
+github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
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/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
-github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
-github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
-github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
-github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
-github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
-github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
-github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
-github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
-github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
-github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
-github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
-github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
-github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
-github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
-github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
-github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
+github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
+github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
+github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
+github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
+github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
+github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
+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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
-github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/v2fly/v2ray-core/v5 v5.30.0 h1:lVREGQjNCSQ38PpMTGWRKav47pd/XT1x+Ig3fUXYW8A=
github.com/v2fly/v2ray-core/v5 v5.30.0/go.mod h1:qv4cRgZcZaYv5IWiCULK4KBR7utwbh302w02Py1Sb5g=
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
-github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
-github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
-github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
-go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
-go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
-google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
-google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/v2stat.amd64 b/v2stat.amd64
new file mode 100755
index 0000000..fa4f1c2
--- /dev/null
+++ b/v2stat.amd64
Binary files differ
diff --git a/v2stat.go b/v2stat.go
deleted file mode 100644
index 49b37a8..0000000
--- a/v2stat.go
+++ /dev/null
@@ -1,234 +0,0 @@
-package v2stat
-
-import (
- "context"
- "database/sql"
- "strings"
- "time"
-
- "github.com/sirupsen/logrus"
- "go.rikki.moe/v2stat/command"
- "google.golang.org/grpc"
-)
-
-type V2Stat struct {
- logger *logrus.Logger
- db *sql.DB
- conn *grpc.ClientConn
- stat command.StatsServiceClient
-}
-
-func NewV2Stat(logger *logrus.Logger, db *sql.DB, conn *grpc.ClientConn) *V2Stat {
- return &V2Stat{
- logger: logger,
- db: db,
- conn: conn,
- stat: command.NewStatsServiceClient(conn),
- }
-}
-
-func (v *V2Stat) Close() {
- if v.db != nil {
- v.db.Close()
- }
- if v.conn != nil {
- v.conn.Close()
- }
-}
-
-func (v *V2Stat) SetConn(conn *grpc.ClientConn) {
- if v.conn != nil {
- v.conn.Close()
- }
- v.conn = conn
- v.stat = command.NewStatsServiceClient(conn)
-}
-
-
-func (v *V2Stat) InitDB() error {
- stmts := []string{
- `CREATE TABLE IF NOT EXISTS conn (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- type INTEGER NOT NULL,
- name TEXT NOT NULL,
- UNIQUE (type, name)
- );`,
- `CREATE TABLE IF NOT EXISTS stats (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- conn_id INTEGER NOT NULL,
- timestamp INTEGER NOT NULL,
- traffic INTEGER NOT NULL,
- direction INTEGER NOT NULL,
- FOREIGN KEY (conn_id) REFERENCES conn (id)
- ON DELETE CASCADE
- ON UPDATE CASCADE
- );`,
- `CREATE INDEX IF NOT EXISTS idx_conn_id ON stats (conn_id);`,
- `CREATE INDEX IF NOT EXISTS idx_timestamp ON stats (timestamp);`,
- }
-
- for _, stmt := range stmts {
- if _, err := v.db.Exec(stmt); err != nil {
- return err
- }
- }
- return nil
-}
-
-func (v *V2Stat) RecordNow(ctx context.Context) error {
- resp, err := v.stat.QueryStats(ctx, &command.QueryStatsRequest{
- Reset_: true,
- })
- if err != nil {
- v.logger.Errorf("Failed to query stats: %v", err)
- return err
- }
-
- tx, err := v.db.Begin()
- if err != nil {
- v.logger.Errorf("Failed to begin transaction: %v", err)
- return err
- }
- defer tx.Rollback() // safe to call even if already committed
-
- insertConnStmt, err := tx.Prepare(`INSERT OR IGNORE INTO conn (type, name) VALUES (?, ?)`)
- if err != nil {
- v.logger.Errorf("Failed to prepare conn insert statement: %v", err)
- return err
- }
- defer insertConnStmt.Close()
-
- selectConnIDStmt, err := tx.Prepare(`SELECT id FROM conn WHERE type = ? AND name = ?`)
- if err != nil {
- v.logger.Errorf("Failed to prepare conn select statement: %v", err)
- return err
- }
- defer selectConnIDStmt.Close()
-
- insertStatsStmt, err := tx.Prepare(`
- INSERT INTO stats (conn_id, timestamp, traffic, direction)
- VALUES (?, ?, ?, ?)
- `)
- if err != nil {
- v.logger.Errorf("Failed to prepare stats insert statement: %v", err)
- return err
- }
- defer insertStatsStmt.Close()
-
- for _, stat := range resp.Stat {
- connType, connName, direction, ok := parseStatKey(stat.Name)
- if !ok {
- v.logger.Warnf("Skipping unrecognized stat key: %s", stat.Name)
- continue
- }
-
- if _, err := insertConnStmt.Exec(connType, connName); err != nil {
- v.logger.Errorf("Failed to insert conn: %v", err)
- continue
- }
-
- var connID int
- err = selectConnIDStmt.QueryRow(connType, connName).Scan(&connID)
- if err != nil {
- v.logger.Errorf("Failed to retrieve conn_id: %v", err)
- continue
- }
-
- timeNow := time.Now().Unix()
- if _, err := insertStatsStmt.Exec(connID, timeNow, stat.Value, direction); err != nil {
- v.logger.Errorf("Failed to insert stats: %v", err)
- continue
- }
-
- v.logger.Infof("Inserted stats: conn_id=%d, timestamp=%d, traffic=%d, direction=%d", connID, timeNow, stat.Value, direction)
- }
-
- if err := tx.Commit(); err != nil {
- v.logger.Errorf("Failed to commit transaction: %v", err)
- return err
- }
- return nil
-}
-
-func parseStatKey(key string) (connType ConnectionType, connName string, direction TrafficDirection, ok bool) {
- parts := strings.Split(key, ">>>")
- if len(parts) != 4 || parts[2] != "traffic" {
- return 0, "", 0, false
- }
-
- switch parts[0] {
- case "user":
- connType = ConnTypeUser
- case "inbound":
- connType = ConnTypeInbound
- case "outbound":
- connType = ConnTypeOutbound
- default:
- return 0, "", 0, false
- }
-
- connName = parts[1]
-
- switch parts[3] {
- case "downlink":
- direction = DirectionDownlink
- case "uplink":
- direction = DirectionUplink
- default:
- return 0, "", 0, false
- }
-
- return connType, connName, direction, true
-}
-
-func (v *V2Stat) QueryConn() ([]ConnInfo, error) {
- rows, err := v.db.Query("SELECT type, name FROM conn")
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var conns []ConnInfo
- for rows.Next() {
- var conn ConnInfo
- if err := rows.Scan(&conn.Type, &conn.Name); err != nil {
- return nil, err
- }
- conns = append(conns, conn)
- }
- return conns, nil
-}
-
-func (v *V2Stat) QueryStatsHourly(conn *ConnInfo) ([]TrafficStat, error) {
- rows, err := v.db.Query(`
- SELECT
- strftime('%Y-%m-%d %H:00:00', datetime(s.timestamp, 'unixepoch', '+8 hours')) AS time,
- SUM(CASE WHEN s.direction = 0 THEN s.traffic ELSE 0 END) AS downlink,
- SUM(CASE WHEN s.direction = 1 THEN s.traffic ELSE 0 END) AS uplink
- FROM stats s
- JOIN conn c ON s.conn_id = c.id
- WHERE c.type = ? AND c.name = ?
- GROUP BY time
- ORDER BY time;
-`, conn.Type, conn.Name)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var stats []TrafficStat
- for rows.Next() {
- var s TrafficStat
- if err := rows.Scan(&s.Time, &s.Downlink, &s.Uplink); err != nil {
- panic(err)
- }
- stats = append(stats, s)
- }
-
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return stats, nil
-}
-
-