- 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.
195 lines
4.4 KiB
Go
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)
|
|
}
|