- 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.
246 lines
6.8 KiB
Go
246 lines
6.8 KiB
Go
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func testStore(t *testing.T, cfg StoreConfig) *Store {
|
|
t.Helper()
|
|
cfg.Dir = t.TempDir()
|
|
s, err := NewStore(cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
t.Cleanup(s.Close)
|
|
return s
|
|
}
|
|
|
|
func TestStorePiseIVracaLinije(t *testing.T) {
|
|
s := testStore(t, StoreConfig{})
|
|
t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
|
|
|
if err := s.Write("1CDBD4A6A460", t0, []string{"prva", "druga"}); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
if err := s.Write("1CDBD4A6A460", t0.Add(time.Second), []string{"treca"}); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
|
|
lines, err := s.Tail("1CDBD4A6A460", 10, "")
|
|
if err != nil {
|
|
t.Fatalf("Tail: %v", err)
|
|
}
|
|
if len(lines) != 3 {
|
|
t.Fatalf("linija = %d: %q", len(lines), lines)
|
|
}
|
|
if !strings.HasSuffix(lines[0], "prva") || !strings.HasSuffix(lines[2], "treca") {
|
|
t.Errorf("redosled nije hronoloski: %q", lines)
|
|
}
|
|
if !strings.HasPrefix(lines[0], "2026-07-25 12:00:00") {
|
|
t.Errorf("nedostaje vremenska oznaka: %q", lines[0])
|
|
}
|
|
}
|
|
|
|
func TestStorePretraga(t *testing.T) {
|
|
s := testStore(t, StoreConfig{})
|
|
t0 := time.Now()
|
|
s.Write("dev", t0, []string{"[stampac] greska", "[kapija] prolaz", "[stampac] ok"})
|
|
|
|
lines, _ := s.Tail("dev", 10, "STAMPAC")
|
|
if len(lines) != 2 {
|
|
t.Fatalf("pretraga = %d linija: %q", len(lines), lines)
|
|
}
|
|
lines, _ = s.Tail("dev", 10, "nepostojece")
|
|
if len(lines) != 0 {
|
|
t.Errorf("ocekivano 0 linija, dobijeno %q", lines)
|
|
}
|
|
}
|
|
|
|
func TestStoreOgranicavaBrojLinija(t *testing.T) {
|
|
s := testStore(t, StoreConfig{})
|
|
t0 := time.Now()
|
|
for i := 0; i < 100; i++ {
|
|
s.Write("dev", t0, []string{fmt.Sprintf("linija %d", i)})
|
|
}
|
|
lines, _ := s.Tail("dev", 10, "")
|
|
if len(lines) != 10 {
|
|
t.Fatalf("linija = %d", len(lines))
|
|
}
|
|
if !strings.HasSuffix(lines[9], "linija 99") {
|
|
t.Errorf("poslednja linija = %q", lines[9])
|
|
}
|
|
}
|
|
|
|
func TestStoreRotacijaPoVelicini(t *testing.T) {
|
|
s := testStore(t, StoreConfig{MaxFileBytes: 200})
|
|
t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
|
for i := 0; i < 30; i++ {
|
|
if err := s.Write("dev", t0, []string{fmt.Sprintf("popunjavanje %02d", i)}); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
}
|
|
files, err := os.ReadDir(filepath.Join(s.cfg.Dir, "dev"))
|
|
if err != nil {
|
|
t.Fatalf("ReadDir: %v", err)
|
|
}
|
|
if len(files) < 2 {
|
|
t.Fatalf("ocekivana rotacija, fajlova = %d", len(files))
|
|
}
|
|
var imena []string
|
|
for _, f := range files {
|
|
imena = append(imena, f.Name())
|
|
if info, _ := f.Info(); info.Size() > 400 {
|
|
t.Errorf("fajl %s je %d B — granica se ne postuje", f.Name(), info.Size())
|
|
}
|
|
}
|
|
if imena[0] != "2026-07-25.log" || imena[1] != "2026-07-25.log.001" {
|
|
t.Errorf("imena fajlova = %q", imena)
|
|
}
|
|
// Sve linije se i dalje citaju, hronoloski.
|
|
lines, _ := s.Tail("dev", 100, "")
|
|
if len(lines) != 30 {
|
|
t.Errorf("procitano %d linija, ocekivano 30", len(lines))
|
|
}
|
|
if !strings.HasSuffix(lines[0], "popunjavanje 00") || !strings.HasSuffix(lines[29], "popunjavanje 29") {
|
|
t.Errorf("redosled kroz rotirane fajlove nije dobar: %q ... %q", lines[0], lines[29])
|
|
}
|
|
}
|
|
|
|
func TestStoreNoviDanNoviFajl(t *testing.T) {
|
|
s := testStore(t, StoreConfig{})
|
|
t0 := time.Date(2026, 7, 25, 23, 59, 0, 0, time.UTC)
|
|
s.Write("dev", t0, []string{"juce"})
|
|
s.Write("dev", t0.Add(2*time.Minute), []string{"danas"})
|
|
|
|
dani := s.Days("dev")
|
|
if len(dani) != 2 || dani[0] != "2026-07-26" || dani[1] != "2026-07-25" {
|
|
t.Errorf("dani = %q", dani)
|
|
}
|
|
}
|
|
|
|
func TestCleanupBriseStareDnevnike(t *testing.T) {
|
|
s := testStore(t, StoreConfig{KeepDays: 3})
|
|
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
|
for i := 0; i < 10; i++ {
|
|
s.Write("dev", now.AddDate(0, 0, -i), []string{"nesto"})
|
|
}
|
|
s.Close() // da ciscenje sme da dira i tekuci fajl
|
|
|
|
n, err := s.Cleanup(now)
|
|
if err != nil {
|
|
t.Fatalf("Cleanup: %v", err)
|
|
}
|
|
if n == 0 {
|
|
t.Fatal("nista nije obrisano")
|
|
}
|
|
dani := s.Days("dev")
|
|
for _, d := range dani {
|
|
dd, _ := time.Parse(dayLayout, d)
|
|
if dd.Before(now.AddDate(0, 0, -3)) {
|
|
t.Errorf("ostao stari dnevnik %s", d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCleanupPostujeKvotuPoUredjaju(t *testing.T) {
|
|
s := testStore(t, StoreConfig{MaxFileBytes: 300, MaxDeviceBytes: 600, KeepDays: 3650})
|
|
base := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
|
for i := 0; i < 20; i++ {
|
|
s.Write("dev", base.AddDate(0, 0, i), []string{strings.Repeat("x", 200)})
|
|
}
|
|
s.Close()
|
|
|
|
if _, err := s.Cleanup(base.AddDate(0, 0, 20)); err != nil {
|
|
t.Fatalf("Cleanup: %v", err)
|
|
}
|
|
total, _ := s.Usage()
|
|
if total > 600 {
|
|
t.Errorf("posle ciscenja %d B, kvota je 600 B", total)
|
|
}
|
|
if total == 0 {
|
|
t.Error("ciscenje je obrisalo sve — najnoviji dnevnik mora ostati")
|
|
}
|
|
}
|
|
|
|
func TestCleanupPostujeUkupnuKvotu(t *testing.T) {
|
|
s := testStore(t, StoreConfig{MaxFileBytes: 300, MaxDeviceBytes: 1 << 20, MaxTotalBytes: 800, KeepDays: 3650})
|
|
base := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
|
for i := 0; i < 10; i++ {
|
|
s.Write(fmt.Sprintf("dev%d", i), base.AddDate(0, 0, i), []string{strings.Repeat("y", 200)})
|
|
}
|
|
s.Close()
|
|
|
|
if _, err := s.Cleanup(base.AddDate(0, 0, 10)); err != nil {
|
|
t.Fatalf("Cleanup: %v", err)
|
|
}
|
|
total, _ := s.Usage()
|
|
if total > 800 {
|
|
t.Errorf("posle ciscenja %d B, ukupna kvota je 800 B", total)
|
|
}
|
|
}
|
|
|
|
func TestCleanupNeBriseOtvoreniFajl(t *testing.T) {
|
|
// Fajl u koji se trenutno pise ne sme da nestane: na Linuxu bi upisi
|
|
// posle brisanja tiho odlazili u nepostojeci fajl.
|
|
s := testStore(t, StoreConfig{MaxTotalBytes: 1, MaxDeviceBytes: 1, KeepDays: 1})
|
|
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
|
s.Write("dev", now, []string{"vazna linija"})
|
|
|
|
if _, err := s.Cleanup(now); err != nil {
|
|
t.Fatalf("Cleanup: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(s.cfg.Dir, "dev", "2026-07-25.log")); err != nil {
|
|
t.Errorf("otvoreni fajl je obrisan: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreOdbijaIzlazakIzFoldera(t *testing.T) {
|
|
s := testStore(t, StoreConfig{})
|
|
now := time.Now()
|
|
if err := s.Write("../../../pobegao", now, []string{"linija"}); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
// Sve mora ostati unutar Dir.
|
|
var vanFoldera []string
|
|
filepath.Walk(filepath.Dir(s.cfg.Dir), func(p string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return nil
|
|
}
|
|
if !strings.HasPrefix(p, s.cfg.Dir) {
|
|
vanFoldera = append(vanFoldera, p)
|
|
}
|
|
return nil
|
|
})
|
|
if len(vanFoldera) > 0 {
|
|
t.Errorf("fajlovi napravljeni van foldera dnevnika: %q", vanFoldera)
|
|
}
|
|
}
|
|
|
|
func TestTailPreseceneLinijeNaGranici(t *testing.T) {
|
|
s := testStore(t, StoreConfig{})
|
|
now := time.Now()
|
|
s.Write("dev", now, []string{"cela linija jedan", "cela linija dva"})
|
|
lines, _ := s.Tail("dev", 5, "")
|
|
for _, ln := range lines {
|
|
if !strings.Contains(ln, "cela linija") {
|
|
t.Errorf("krnja linija u ispisu: %q", ln)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTailNepostojeciUredjaj(t *testing.T) {
|
|
s := testStore(t, StoreConfig{})
|
|
lines, err := s.Tail("nema-ga", 10, "")
|
|
if err != nil {
|
|
t.Fatalf("Tail: %v", err)
|
|
}
|
|
if len(lines) != 0 {
|
|
t.Errorf("linije = %q", lines)
|
|
}
|
|
}
|