init
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
"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(badger.DefaultOptions("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)
|
||||
|
||||
handlers := Handlers{DB: db}
|
||||
http.HandleFunc("/", handlers.indexHandler)
|
||||
fmt.Println("Listening on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
|
||||
select {} // Keep the program running
|
||||
}
|
||||
|
||||
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{"SDiff", -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
|
||||
}
|
||||
Reference in New Issue
Block a user