licence-server/logger/registry.go
Nenad Djukic 3480766c3e Prijemnik dijagnostike sa TERMINIA uredjaja (logger/)
- UDP prijemnik (podrazumevano 46465, NE 46464 gde uredjaji slusaju discovery):
  dnevnik rada sa zaglavljem #TERMINIA, heartbeat JSON, sve ostalo se odbacuje
- paket bez zaglavlja (FW < 0.13.4) se vodi kao "nepoznat uredjaj" po izvornoj
  IP adresi i tako je i oznacen u prikazu
- dnevnik u fajlove po uredjaju i danu, rotacija po velicini + tri kvote
  (starost, po uredjaju, ukupno); otvoren fajl se nikad ne brise
- heartbeat u memoriji; interval se meri iz razmaka javljanja, "tih" = 3x interval
- web stranica (podrazumevano 127.0.0.1:8081): spisak uredjaja, crveno za tihe,
  poslednjih N linija sa pretragom, /dnevnik.txt i /api/uredjaji
- bez baze i bez spoljnih zavisnosti; ne dira licencni servis, njegovu bazu,
  rute ni nginx
- sanitizacija svega iz paketa (duzine, kontrolni znaci, ime foldera) — bez
  izlaska iz foldera dnevnika
- systemd unit + primer env fajla + README sa uputstvom za pustanje na server
- testovi: parsiranje (zaglavlje/JSON/bez zaglavlja/smece), registar, rotacija
  i kvote, web rute

Nije pusteno na server — puštanje radi vlasnik po logger/README.md.
2026-07-25 17:40:27 +02:00

195 lines
4.4 KiB
Go

package logger
import (
"sort"
"sync"
"time"
)
// Device je sve sto pamtimo o jednom uredjaju. Sve je u memoriji i sve je
// ograniceno po velicini - dnevnik ide u fajlove, ne ovde.
type Device struct {
Key string
Name string
MAC string
IP string // adresa koju uredjaj sam prijavi (heartbeat)
SourceIP string // adresa sa koje je paket stvarno stigao
FW string
Serial string
Identified bool // znamo li ko je (zaglavlje/heartbeat) ili samo nagadjamo po IP-u
FirstSeen time.Time
LastSeen time.Time
LastHeartbeat time.Time
Heartbeats uint64
LogPackets uint64
LogLines uint64
IntervalSec int // izmeren razmak izmedju dva heartbeata (0 = jos ne znamo)
LastLine string // poslednja linija dnevnika, samo za pregled
}
// Registry cuva uredjaje u memoriji. Broj uredjaja je ogranicen jer paket moze
// poslati bilo ko - bez granice bi jedan izvor sa laznim MAC-ovima pojeo RAM.
type Registry struct {
mu sync.RWMutex
devices map[string]*Device
maxDevices int
defaultHb int // pretpostavljeni interval kad uredjaj nikad nije poslao heartbeat
}
func NewRegistry(maxDevices, defaultHbSec int) *Registry {
if maxDevices <= 0 {
maxDevices = 500
}
if defaultHbSec <= 0 {
defaultHbSec = 60
}
return &Registry{
devices: make(map[string]*Device),
maxDevices: maxDevices,
defaultHb: defaultHbSec,
}
}
// Update ubacuje paket u registar i vraca kljuc uredjaja (i da li je primljen).
// Paket koji nije prepoznat se ignorise.
func (r *Registry) Update(p Packet, now time.Time) (string, bool) {
if p.Kind == KindGarbage {
return "", false
}
key := DeviceKey(p.MAC, p.SourceIP)
r.mu.Lock()
defer r.mu.Unlock()
d, ok := r.devices[key]
if !ok {
if len(r.devices) >= r.maxDevices {
r.evictOldestLocked()
}
d = &Device{Key: key, FirstSeen: now}
r.devices[key] = d
}
d.LastSeen = now
d.SourceIP = p.SourceIP
if p.Name != "" {
d.Name = p.Name
}
if p.MAC != "" {
d.MAC = p.MAC
}
if p.FW != "" {
d.FW = p.FW
}
if p.Serial != "" {
d.Serial = p.Serial
}
if p.Identified {
d.Identified = true
}
switch p.Kind {
case KindHeartbeat:
if p.IP != "" {
d.IP = p.IP
}
// Interval ne dolazi u paketu - meri se izmedju dva javljanja.
// Uzima se poslednji razmak, uz clamp da jedan izgubljen paket ili
// restart uredjaja ne razvuku prag na sate.
if !d.LastHeartbeat.IsZero() {
if sec := int(now.Sub(d.LastHeartbeat).Seconds()); sec >= 5 && sec <= 3600 {
d.IntervalSec = sec
}
}
d.LastHeartbeat = now
d.Heartbeats++
case KindLog:
d.LogPackets++
d.LogLines += uint64(len(p.Lines))
if n := len(p.Lines); n > 0 {
d.LastLine = p.Lines[n-1]
}
}
return key, true
}
// evictOldestLocked izbacuje uredjaj koji se najduze nije javio. Poziva se samo
// kad je registar pun.
func (r *Registry) evictOldestLocked() {
var oldestKey string
var oldest time.Time
for k, d := range r.devices {
if oldestKey == "" || d.LastSeen.Before(oldest) {
oldestKey, oldest = k, d.LastSeen
}
}
if oldestKey != "" {
delete(r.devices, oldestKey)
}
}
// Interval vraca interval po kom se procenjuje da li je uredjaj tih.
func (r *Registry) Interval(d *Device) int {
if d.IntervalSec > 0 {
return d.IntervalSec
}
return r.defaultHb
}
// Stale je tacno kad se uredjaj nije javio duze od 3x svog intervala.
func (r *Registry) Stale(d *Device, now time.Time) bool {
limit := time.Duration(3*r.Interval(d)) * time.Second
return now.Sub(d.LastSeen) > limit
}
// Snapshot vraca kopije uredjaja, sortirane: prvo tihi (da bode oci), pa po
// nazivu/kljucu.
func (r *Registry) Snapshot(now time.Time) []Device {
r.mu.RLock()
out := make([]Device, 0, len(r.devices))
for _, d := range r.devices {
out = append(out, *d)
}
r.mu.RUnlock()
sort.Slice(out, func(i, j int) bool {
si, sj := r.Stale(&out[i], now), r.Stale(&out[j], now)
if si != sj {
return si
}
ni, nj := out[i].Name, out[j].Name
if ni == "" {
ni = out[i].Key
}
if nj == "" {
nj = out[j].Key
}
if ni != nj {
return ni < nj
}
return out[i].Key < out[j].Key
})
return out
}
// Get vraca kopiju jednog uredjaja.
func (r *Registry) Get(key string) (Device, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
d, ok := r.devices[key]
if !ok {
return Device{}, false
}
return *d, true
}
// Count vraca broj uredjaja u registru.
func (r *Registry) Count() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.devices)
}