diff options
| -rw-r--r-- | cmd/v2stat/daemon.go | 45 | ||||
| -rw-r--r-- | cmd/v2stat/main.go | 192 | ||||
| -rw-r--r-- | def.go | 66 | ||||
| -rw-r--r-- | go.mod | 34 | ||||
| -rw-r--r-- | go.sum | 95 | ||||
| -rwxr-xr-x | v2stat.amd64 | bin | 0 -> 19730756 bytes | |||
| -rw-r--r-- | v2stat.go | 234 |
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 } @@ -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 @@ -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 ) @@ -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 Binary files differnew file mode 100755 index 0000000..fa4f1c2 --- /dev/null +++ b/v2stat.amd64 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 -} - - |
