pool-stats/main.go
Pijus Kamandulis 0a53686351 fix crash
2025-05-27 18:58:07 +03:00

301 lines
7.8 KiB
Go

package main
import (
"encoding/json"
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
"time"
"github.com/ostafen/clover/v2"
"github.com/ostafen/clover/v2/document"
c "github.com/ostafen/clover/v2/query"
badgerstore "github.com/ostafen/clover/v2/store/badger"
)
const logsDir = "/home/pk/pro/pkstats/logs"
const collectionName = "shares"
type ShareLog struct {
WorkInfoID uint64 `json:"workinfoid"` // ID of the associated work unit
ClientID int `json:"clientid"` // ID of the client/miner connection
Enonce1 string `json:"enonce1"` // Extra nonce 1 (assigned by pool)
Nonce2 string `json:"nonce2"` // Extra nonce 2 (sent by miner)
Nonce string `json:"nonce"` // Nonce used to generate the solution
NTime string `json:"ntime"` // Network time used in the block
Diff float64 `json:"diff"` // Target difficulty for the share
SDiff float64 `json:"sdiff"` // Share difficulty achieved
Hash string `json:"hash"` // Resulting hash from the share
Result bool `json:"result"` // Was this share a valid result
Errn int `json:"errn"` // Error code (0 = no error)
CreateDate string `json:"createdate"` // Timestamp: "seconds,nanoseconds"
CreateBy string `json:"createby"` // Origin of share creation ("code")
CreateCode string `json:"createcode"` // Component that created this entry
CreateInet string `json:"createinet"` // IP + port of submit origin
WorkerName string `json:"workername"` // Full worker name (username.worker)
Username string `json:"username"` // User's Bitcoin address
Address string `json:"address"` // IP address of the worker
Agent string `json:"agent"` // Miner agent string (e.g., bitaxe/BM1370)
}
type ShareStat struct {
Label string
Diff string
Time string
}
type Handlers struct {
DB *clover.DB
}
// ParseCreateDate can be used to convert ShareLog.CreateDate to time.Time
func (s *ShareLog) ParseCreateDate() (time.Time, error) {
var sec, nsec int64
_, err := fmt.Sscanf(s.CreateDate, "%d,%d", &sec, &nsec)
if err != nil {
return time.Time{}, err
}
return time.Unix(sec, nsec), nil
}
func main() {
store, _ := badgerstore.Open("badgerdb")
db, err := clover.OpenWithStore(store)
if err != nil {
log.Fatalf("Failed to open CloverDB: %v", err)
}
// Ensure collection exists
hasCollection, err := db.HasCollection(collectionName)
if err != nil {
log.Fatalf("Failed to check collection: %v", err)
}
if !hasCollection {
_ = db.CreateCollection(collectionName)
db.CreateIndex(collectionName, "CreateDate")
}
printAllHashes(db)
lastDayHighestShare, err := getHighestShareInRange(db, collectionName, time.Now().Add(-24*time.Hour))
if err != nil {
log.Printf("Failed to get highest share in last 24h: %v", err)
} else {
createdAt, _ := lastDayHighestShare.Get("CreateDate").(string)
fmt.Printf("Highest share in last 24h: %s, Diff: %f, Created at: %s\n",
lastDayHighestShare.Get("Hash"),
lastDayHighestShare.Get("SDiff"),
createdAt,
)
}
go watchAndIngest(db)
go func() {
handlers := Handlers{DB: db}
http.HandleFunc("/", handlers.indexHandler)
fmt.Println("Listening on :8081")
log.Fatal(http.ListenAndServe(":8081", nil))
}()
fmt.Println("Waitin for ctrl-c")
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
store.Close()
}
func printAllHashes(db *clover.DB) {
docs, err := db.FindAll(c.NewQuery(collectionName))
if err != nil {
log.Fatalf("Failed to read from collection: %v", err)
}
for _, doc := range docs {
hash := doc.Get("Hash")
fmt.Println(hash)
}
}
func watchAndIngest(db *clover.DB) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
<-ticker.C
ingestClosedBlocks(db)
}
}
func ingestClosedBlocks(db *clover.DB) {
entries, err := os.ReadDir(logsDir)
if err != nil {
log.Println("Error reading logsDir:", err)
return
}
// Find block-like directories (starts with 000)
blockDirs := []fs.DirEntry{}
for _, entry := range entries {
if entry.IsDir() && strings.HasPrefix(entry.Name(), "000") {
blockDirs = append(blockDirs, entry)
}
}
if len(blockDirs) <= 1 {
return // Nothing to ingest
}
// Sort dirs alphabetically (ascending), last is newest
sort.Slice(blockDirs, func(i, j int) bool {
return blockDirs[i].Name() < blockDirs[j].Name()
})
// Ingest all except last (current block dir)
for _, dir := range blockDirs[:len(blockDirs)-1] {
ingestBlockDir(db, filepath.Join(logsDir, dir.Name()))
_ = os.RemoveAll(filepath.Join(logsDir, dir.Name()))
}
}
func ingestBlockDir(db *clover.DB, dirPath string) {
files, err := os.ReadDir(dirPath)
if err != nil {
log.Printf("Failed to read block dir %s: %v", dirPath, err)
return
}
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".sharelog") {
continue
}
data, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
if err != nil {
log.Println("Failed to read file:", err)
continue
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
var share ShareLog
if err := json.Unmarshal([]byte(line), &share); err != nil {
log.Println("JSON parse error:", err)
continue
}
doc := document.NewDocumentOf(&share)
if _, err := db.InsertOne(collectionName, doc); err != nil {
log.Println("DB insert error:", err)
}
}
}
log.Printf("Ingested and deleted %s", dirPath)
}
func parseCreatedate(createdate string) time.Time {
parts := strings.Split(createdate, ",")
if len(parts) == 2 {
sec, _ := strconv.ParseInt(parts[0], 10, 64)
nsec, _ := strconv.ParseInt(parts[1], 10, 64)
return time.Unix(sec, nsec)
}
return time.Time{}
}
func getHighestShareInRange(db *clover.DB, collection string, since time.Time) (*document.Document, error) {
// Convert `since` to the format in `createdate`
lower := since.Unix()
upper := time.Now().Unix()
// Filter by timestamp range
criteria := c.Field("CreateDate").GtEq(fmt.Sprint(lower)).
And(c.Field("CreateDate").LtEq(fmt.Sprint(upper)))
// Query sorted by "sdiff" descending, limit 1
results, err := db.FindAll(c.NewQuery(collection).
Where(criteria).
Sort(c.SortOption{Field: "SDiff", Direction: -1}).
Limit(1))
if err != nil || len(results) == 0 {
return nil, err
}
return results[0], nil
}
func (h Handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
stats, err := getStats(h.DB)
if err != nil {
http.Error(w, "Failed to load stats", 500)
return
}
tmpl := template.Must(template.ParseFiles("templates/index.html"))
tmpl.Execute(w, stats)
}
func humanDiff(diff float64) string {
units := []string{"", "K", "M", "G", "T"}
i := 0
for diff >= 1000 && i < len(units)-1 {
diff /= 1000
i++
}
return fmt.Sprintf("%.2f%s", diff, units[i])
}
func getStats(db *clover.DB) ([]ShareStat, error) {
now := time.Now()
ranges := []struct {
Label string
Since time.Time
}{
{"Past Hour", now.Add(-1 * time.Hour)},
{"Past 24h", now.Add(-24 * time.Hour)},
{"Past 7d", now.Add(-7 * 24 * time.Hour)},
}
stats := []ShareStat{}
// All-time highest
doc, _ := getHighestShareInRange(db, collectionName, time.Unix(0, 0))
if doc != nil {
stats = append(stats, ShareStat{
Label: "All Time",
Diff: humanDiff(doc.Get("SDiff").(float64)),
Time: parseCreatedate(doc.Get("CreateDate").(string)).Format(time.RFC822),
})
}
for _, r := range ranges {
doc, _ := getHighestShareInRange(db, collectionName, r.Since)
if doc != nil {
stats = append(stats, ShareStat{
Label: r.Label,
Diff: humanDiff(doc.Get("SDiff").(float64)),
Time: parseCreatedate(doc.Get("CreateDate").(string)).Format(time.RFC822),
})
} else {
stats = append(stats, ShareStat{Label: r.Label, Diff: "-", Time: "-"})
}
}
return stats, nil
}