diff --git a/cache.go b/cache.go index 2fe8bf8..63fa9d1 100644 --- a/cache.go +++ b/cache.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" ) type ProxyWriter struct { diff --git a/config.go b/config.go index a7e1a32..cd1da5f 100644 --- a/config.go +++ b/config.go @@ -13,7 +13,7 @@ import ( "strings" "text/template" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" "github.com/xeipuuv/gojsonschema" ) diff --git a/daily_health.go b/daily_health.go index f4af184..7a8499e 100644 --- a/daily_health.go +++ b/daily_health.go @@ -7,7 +7,7 @@ import ( "sort" "time" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" "github.com/influxdata/influxdb-client-go/v2/api" "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" diff --git a/datarithms/README.md b/datarithms/README.md new file mode 100644 index 0000000..0bb85ac --- /dev/null +++ b/datarithms/README.md @@ -0,0 +1,3 @@ +# Datarithms + +This module includes practical algorithms and data structures for our [Mirror](https://github.com/COSI-Lab/Mirror) project. diff --git a/datarithms/binarysearch.go b/datarithms/binarysearch.go new file mode 100644 index 0000000..ba1633d --- /dev/null +++ b/datarithms/binarysearch.go @@ -0,0 +1,56 @@ +package datarithms + +import ( + "bufio" + "io" + "os" + "time" +) + +// BinarySearchFileByDate returns the offset of the first line which date is after lastUpdated +// this could panic if the parse function isn't guaranteed to work on every line of the file +func BinarySearchFileByDate(file string, lastUpdated time.Time, parse func(line string) (time.Time, error)) (offset int64, err error) { + // Open the log file + f, err := os.Open(file) + if err != nil { + return 0, err + } + + // Load all of the line offsets into memory + lines := make([]int64, 0) + + // Precalculate the seek offset of each line + scanner := bufio.NewScanner(f) + for scanner.Scan() { + lines = append(lines, offset) + offset += int64(len(scanner.Bytes())) + 1 + } + + // Run a binary search for the first line which is past the lastUpdated + start := 0 + end := len(lines) - 1 + for start < end { + mid := (start + end) / 2 + + // Get the text of the line + f.Seek(lines[mid], io.SeekStart) + scanner := bufio.NewScanner(f) + scanner.Scan() + line := scanner.Text() + + tm, err := parse(line) + if err != nil { + // if for some reason we can't parse the line we increment start and try again + start = mid + 1 + continue + } + + if tm.After(lastUpdated) { + end = mid + } else { + start = mid + 1 + } + } + + return lines[start], nil +} diff --git a/datarithms/datarithms_test.go b/datarithms/datarithms_test.go new file mode 100644 index 0000000..d737639 --- /dev/null +++ b/datarithms/datarithms_test.go @@ -0,0 +1,155 @@ +package datarithms_test + +import ( + "testing" + + "github.com/COSI-Lab/Mirror/datarithms" +) + +// Test the queue +func TestQueue(t *testing.T) { + // Create a new queue + q := datarithms.CircularQueueInit[int](5) + + if q.Capacity() != 5 { + t.Error("Capacity is not 5") + } + + // Push some elements + q.Push(1) + q.Push(2) + q.Push(3) + + // Check the length + if q.Len() != 3 { + t.Error("Expected 3, got", q.Len()) + } + + var element int + var err error + + // Pop the first element + if element, err = q.Pop(); err == nil && element != 1 { + t.Error("Expected 1, got", element) + } + + // Check the length + if q.Len() != 2 { + t.Error("Expected 2, got", q.Len()) + } + + // Pop the second element + if element, err = q.Pop(); err == nil && element != 2 { + t.Error("Expected 2, got", element) + } + + // Check the length + if q.Len() != 1 { + t.Error("Expected 1, got", q.Len()) + } + + // Pop the third element + if element, err = q.Pop(); err == nil && element != 3 { + t.Error("Expected 3, got", element) + } + + // Check the length + if q.Len() != 0 { + t.Error("Expected 0, got", q.Len()) + } + + // Pop the fourth element + if element, err = q.Pop(); err == nil && element == 0 { + t.Error("Expected nil, got", element) + } + + // Check the length + if q.Len() != 0 { + t.Error("Expected 0, got", q.Len()) + } + + // Push more elements than capacity + for i := 0; i < 10; i++ { + q.Push(i) + } + + // Check the length + if q.Len() != 5 { + t.Error("Expected 5, got", q.Len()) + } + + // Pop the first element + if element, err = q.Pop(); err != nil && element != 5 { + t.Error("Expected 5, got", element) + } + + // Check the length + if q.Len() != 4 { + t.Error("Expected 4, got", q.Len()) + } + + // Pop the second element + if element, err = q.Pop(); err != nil && element != 6 { + t.Error("Expected 6, got", element) + } + + // Check the length + if q.Len() != 3 { + t.Error("Expected 3, got", q.Len()) + } + + // Pop the third element + if element, err = q.Pop(); err != nil && element != 7 { + t.Error("Expected 7, got", element) + } + + // Check the length + if q.Len() != 2 { + t.Error("Expected 2, got", q.Len()) + } + + // Pop the fourth element + if element, err = q.Pop(); err != nil && element != 8 { + t.Error("Expected 8, got", element) + } + + // Check the length + if q.Len() != 1 { + t.Error("Expected 1, got", q.Len()) + } + + // Pop the fifth element + if element, err = q.Pop(); err != nil && element != 9 { + t.Error("Expected 9, got", element) + } + + // Check the length + if q.Len() != 0 { + t.Error("Expected 0, got", q.Len()) + } +} + +func TestSchedule(t *testing.T) { + // Create tasks + tasks := []datarithms.Task{ + {Short: "a", Syncs: 1}, + {Short: "b", Syncs: 2}, + {Short: "c", Syncs: 4}, + {Short: "d", Syncs: 8}, + } + + sched := datarithms.BuildSchedule(tasks) + t.Log(sched) + + verify := datarithms.Verify(sched, tasks) + if !verify { + t.Error("Schedule is invalid") + } + + // Next task is in the future + _, dt := sched.NextJob() + + if dt < 0 { + t.Error("Next task is in the past") + } +} diff --git a/datarithms/queue.go b/datarithms/queue.go new file mode 100644 index 0000000..deb01e3 --- /dev/null +++ b/datarithms/queue.go @@ -0,0 +1,98 @@ +package datarithms + +import ( + "fmt" + "sync" +) + +// Thread Safe circular queue implmentation using a slice for byte slices +type CircularQueue[T any] struct { + lock sync.RWMutex + queue []T + capacity int + start int + end int + length int +} + +// Creates a new circular queue of given capacity +func CircularQueueInit[T any](capacity int) *CircularQueue[T] { + q := new(CircularQueue[T]) + + q.queue = make([]T, capacity) + q.capacity = capacity + q.start = 0 + q.end = 0 + q.length = 0 + q.lock = sync.RWMutex{} + + return q +} + +// Adds a new element to the queue +func (q *CircularQueue[T]) Push(element T) { + q.lock.Lock() + q.queue[q.end] = element + q.end = (q.end + 1) % q.capacity + // If the queue is full, start overwriting from the beginning + if q.length == q.capacity { + q.start = (q.start + 1) % q.capacity + } else { + q.length++ + } + q.lock.Unlock() +} + +// Pops the element at the front of the queue +// If the queue is empty, returns the zero value followed by an error +func (q *CircularQueue[T]) Pop() (element T, err error) { + q.lock.Lock() + // If the queue is empty, return nil + if q.length == 0 { + q.lock.Unlock() + return element, fmt.Errorf("CircularQueue is empty") + } + element = q.queue[q.start] + q.start = (q.start + 1) % q.capacity + q.length-- + q.lock.Unlock() + return element, nil +} + +// Returns the element at the front of the queue +func (q *CircularQueue[T]) Front() T { + q.lock.RLock() + result := q.queue[q.start] + q.lock.RUnlock() + return result +} + +// Returns the number of elements in the queue +func (q *CircularQueue[T]) Len() int { + q.lock.RLock() + result := q.length + q.lock.RUnlock() + return result +} + +// Returns the capacity of the queue +func (q *CircularQueue[T]) Capacity() int { + q.lock.RLock() + result := q.capacity + q.lock.RUnlock() + return result +} + +// Returns all the elements of the queue +func (q *CircularQueue[T]) All() []T { + q.lock.RLock() + result := make([]T, 0, q.length) + + // From start to end + for i := q.start; i != q.end; i = (i + 1) % q.capacity { + result = append(result, q.queue[i]) + } + + q.lock.RUnlock() + return result +} diff --git a/datarithms/rsync_schedule.go b/datarithms/rsync_schedule.go new file mode 100644 index 0000000..3108277 --- /dev/null +++ b/datarithms/rsync_schedule.go @@ -0,0 +1,141 @@ +package datarithms + +import ( + "fmt" + "time" +) + +// Schedule is a struct that holds a list of tasks and their corresponding target time to run +// The invariant is that the target time must be increasing in the jobs list. +// So the excutation algorithm is trivial. Run the task, sleep until the next target time, repeat. +type Schedule struct { + jobs []Job + iterator int +} + +type Job struct { + short string + target_time float32 +} + +// Returns the job to run and how long to sleep until the next job +// v iterator +// [ ] -> [ ] -> [ ] -> [ ] +// current time ^ ^ new iterator +// |----| +// dt +// run this job sleep until the next job and change to the next job +func (s *Schedule) NextJob() (short string, dt time.Duration) { + // Calculate the time since midnight + now := time.Now().UTC() + pos := time.Since(time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)) + + // Convert time to position in the schedule (0.0 <= t <= 1.0) + t := float32(pos) / float32(24*time.Hour) + + // Find the first job that is greater than the current time + for s.iterator < len(s.jobs) && s.jobs[s.iterator].target_time <= t { + s.iterator++ + } + + // If we are at the end of the schedule, sleep until midnight + if s.iterator == len(s.jobs) { + defer func() { + s.iterator = 0 + }() + + dt = time.Until(time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)) + return s.jobs[len(s.jobs)-1].short, dt + } + + // Time to sleep until the next job + dt = time.Duration((s.jobs[s.iterator].target_time - t) * float32(24*time.Hour)) + + return s.jobs[s.iterator-1].short, dt +} + +// fed as input to the scheduling algorithm +type Task struct { + // Short name of the project + Short string + + // How many times does the project sync per day + Syncs int +} + +// Scheduling algorithm +func BuildSchedule(tasks []Task) *Schedule { + total_jobs := 0 + for _, task := range tasks { + total_jobs += task.Syncs + } + + // compute least common multiple of all sync frequencies + lcm := 1 + for _, task := range tasks { + // compute the greatest common divisor of best known LCM and sync frequency of the current task + var ( + a int + b int + ) + if lcm > task.Syncs { + a = lcm + b = task.Syncs + } else { + a = task.Syncs + b = lcm + } + for b != 0 { + rem := a % b + a = b + b = rem + } + // now a is the GCD; we can compute the next LCM + // FIXME: check for overflow in multiplication + lcm = lcm * task.Syncs / a + } + + jobs := make([]Job, total_jobs) + var interval float32 = 1.0 / float32(total_jobs) + c := 0 + for i := 0; i < lcm; i++ { + for _, task := range tasks { + if i%(lcm/task.Syncs) == 0 { + // emit a job + jobs[c].short = task.Short + jobs[c].target_time = interval * float32(c) + c += 1 + } + } + } + + return &Schedule{iterator: 0, jobs: jobs} +} + +// Verifies that the schedule has increasing target times, all of them are +// within the cycle (0.0 <= t <= 1.0), and that each task will be synced the +// correct number of times +func Verify(s *Schedule, tasks []Task) bool { + syncs := make(map[string]int) + for _, task := range tasks { + syncs[task.Short] = 0 + } + + var last_time float32 = 0.0 + for _, job := range s.jobs { + if job.target_time < last_time || job.target_time > 1.0 { + return false + } + last_time = job.target_time + syncs[job.short]++ + } + + for _, task := range tasks { + if syncs[task.Short] != task.Syncs { + fmt.Println(task.Short, "was expecting", task.Syncs, "syncs but found", syncs[task.Short]) + return false + } + } + + return true +} diff --git a/geoip/geoip.go b/geoip/geoip.go new file mode 100644 index 0000000..1cab81c --- /dev/null +++ b/geoip/geoip.go @@ -0,0 +1,94 @@ +package geoip + +import ( + "fmt" + "net" + "sync" + "time" + + "github.com/IncSW/geoip2" +) + +// GeoIPHandler will keep itself up-to-date with the latest GeoIP database by updating once every 24 hours +// Provides methods to lookup IP addresses and return the associated latitude and longitude +// The structure should be created with it's maxmind license key +type GeoIPHandler struct { + sync.RWMutex + + // The stop channel is used to end the goroutine that updates the database + stop chan struct{} + // Maxmind license key can be created at https://www.maxmind.com + licenseKey string + // underlying database object + db *geoip2.CityReader +} + +// NewGeoIPHandler creates a new GeoIPHandler with the given license key +func NewGeoIPHandler(licenseKey string) (*GeoIPHandler, error) { + // Download the database + bytes, err := downloadAndCheckHash(licenseKey) + if err != nil { + return nil, err + } + + // Create the database + db, err := geoip2.NewCityReader(bytes) + if err != nil { + return nil, err + } + + // Create the handler + handler := &GeoIPHandler{ + stop: make(chan struct{}), + licenseKey: licenseKey, + db: db, + } + + // update the database every 24 hours + go handler.update(handler.stop) + + return handler, nil +} + +func (g *GeoIPHandler) update(stop chan struct{}) { + // update the database every 24 hours + for { + select { + case <-stop: + return + case <-time.After(24 * time.Hour): + // Lock the database + g.Lock() + + // Download the database + bytes, err := downloadAndCheckHash(g.licenseKey) + if err != nil { + fmt.Println(err) + g.Unlock() + continue + } + + // Create the database + db, err := geoip2.NewCityReader(bytes) + + if err != nil { + fmt.Println(err) + g.Unlock() + continue + } + + // Create the handler + g.db = db + + g.Unlock() + } + } +} + +func (g *GeoIPHandler) Close() { + g.stop <- struct{}{} +} + +func (g *GeoIPHandler) Lookup(ip net.IP) (*geoip2.CityResult, error) { + return g.db.Lookup(ip) +} diff --git a/geoip/update.go b/geoip/update.go new file mode 100644 index 0000000..2751aeb --- /dev/null +++ b/geoip/update.go @@ -0,0 +1,112 @@ +package geoip + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" +) + +// Creates a new geoip2 reader by downloading the database from MaxMind. +// We also preform a sha256 check to ensure the database is not corrupt. + +const CHECKSUM_URL string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz.sha256&license_key=" +const DATABASE_URL string = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=" + +// Uses the MaxMind permalink to download the most recent sha256 checksum of the database. +func downloadHash(licenseKey string) (string, error) { + resp, err := http.Get(CHECKSUM_URL + licenseKey) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + // Check the status code + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP Status %d while trying to download the sha256 checksum", resp.StatusCode) + } + + // Return the sha256 checksum + return string(body[:64]), nil +} + +func downloadDatabase(licenseKey, checksum string) ([]byte, error) { + resp, err := http.Get(DATABASE_URL + licenseKey) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Check the status code + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP Status %d while trying to download the database", resp.StatusCode) + } + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println(err) + return nil, err + } + + // Calculate the sha256 checksum of the tarball + calculatedHash := sha256.Sum256(body) + calculatedHashString := fmt.Sprintf("%x", calculatedHash) + + // Check the checksum + if checksum != calculatedHashString { + return nil, fmt.Errorf("checksum mismatch. Expected %s, got %s", checksum, calculatedHashString) + } + + // Here we have a tar.gz file. We need to extract it. + gzr, err := gzip.NewReader(bytes.NewReader(body)) + if err != nil { + fmt.Println("Error creating gzip reader:", err) + return nil, err + } + defer gzr.Close() + + // Read the files names of the things in the tar file + tarReader := tar.NewReader(gzr) + for { + header, err := tarReader.Next() + + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + // Name ends with "GeoLite2-City.mmdb" + if strings.HasSuffix(header.Name, "GeoLite2-City.mmdb") { + // We found the database file. Read it. + return ioutil.ReadAll(tarReader) + } + } + + // Return the database + return nil, fmt.Errorf("database not found in the tarball") +} + +func downloadAndCheckHash(licenseKey string) ([]byte, error) { + // Download the hash + hash, err := downloadHash(licenseKey) + if err != nil { + return nil, err + } + + // Download the database + return downloadDatabase(licenseKey, hash) +} \ No newline at end of file diff --git a/geoip/update_test.go b/geoip/update_test.go new file mode 100644 index 0000000..923b7f7 --- /dev/null +++ b/geoip/update_test.go @@ -0,0 +1,93 @@ +package geoip + +import ( + "encoding/hex" + "net" + "os" + "testing" + + "github.com/IncSW/geoip2" +) + +// GeoIP tests +func TestGetSHA256(t *testing.T) { + // Get the license key through environment variables + licenseKey := os.Getenv("MAXMIND_LICENSE_KEY") + if licenseKey == "" { + t.Error("MAXMIND_LICENSE_KEY environment variable not set") + return + } + + // Download the sha256 checksum + sha256, err := downloadHash(licenseKey) + if err != nil { + t.Error(err) + } + + // the checksum should be a hex string that is 64 characters long + if len(sha256) != 64 { + t.Error("sha256 checksum is not 64 characters long") + } + + // decode the sha256 checksum + _, err = hex.DecodeString(sha256) + if err != nil { + t.Error(err) + } +} + +// Download a new database +func TestDownloadDatabase(t *testing.T) { + // Get the license key through environment variables + licenseKey := os.Getenv("MAXMIND_LICENSE_KEY") + if licenseKey == "" { + t.Error("MAXMIND_LICENSE_KEY environment variable not set") + return + } + + // Prepare the checksum + sha256, err := downloadHash(licenseKey) + if err != nil { + t.Error(err) + } + + // Download the database + bytes, err := downloadDatabase(licenseKey, sha256) + if err != nil { + t.Error(err) + } + + // Verify that the database can be opened + _, err = geoip2.NewCityReader(bytes) + if err != nil { + t.Error(err) + } +} + +func TestLookups(t *testing.T) { + // Get the license key through environment variables + licenseKey := os.Getenv("MAXMIND_LICENSE_KEY") + if licenseKey == "" { + t.Error("MAXMIND_LICENSE_KEY environment variable not set") + return + } + + geoip, err := NewGeoIPHandler(licenseKey) + if err != nil { + t.Error(err) + return + } + + // Lookup some IP addresses + ips := []string{"128.153.145.19", "2605:6480:c051:100::1"} + for _, ip := range ips { + _, err := geoip.Lookup(net.ParseIP(ip)) + if err != nil { + t.Error(ip, err) + } + } + + // TODO: Add a test that ensures maxmind knows where mirror is + + geoip.Close() +} \ No newline at end of file diff --git a/go.mod b/go.mod index 79f6cb6..a03444e 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,8 @@ -module github.com/COSI_Lab/Mirror +module github.com/COSI-Lab/Mirror go 1.20 require ( - github.com/COSI-Lab/datarithms v1.0.2 - github.com/COSI-Lab/geoip v1.0.0 - github.com/COSI-Lab/logging v1.0.3 github.com/IncSW/geoip2 v0.1.2 github.com/gocolly/colly v1.2.0 github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index 4d1be46..1904983 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,15 @@ -github.com/COSI-Lab/datarithms v1.0.2 h1:lBCmJs6f1HBT8WpkPdVkz2jIIz+4SmhWapPmJDodMUw= -github.com/COSI-Lab/datarithms v1.0.2/go.mod h1:RbztXJr04lNIwJxbc6X5a4RUJTng9tuR0C0htlzhOrk= -github.com/COSI-Lab/geoip v1.0.0 h1:wZEWHfw6mex+Vfk3BPKpVP1Ia1Y+ARhqwgs7QqckP9o= -github.com/COSI-Lab/geoip v1.0.0/go.mod h1:/XlojAhPMNnxoBhTSk3o/dFAW+BsywOHJffEmLQOMjo= -github.com/COSI-Lab/logging v1.0.3 h1:D/ritIpI0o8WWgywF9/nQlJbGTu1lLQGAeophjPkLuU= -github.com/COSI-Lab/logging v1.0.3/go.mod h1:7JdoE7ECtg/nWA5FjTAL8f/amR6lvp4cYy+wmXbEYYw= github.com/IncSW/geoip2 v0.1.2 h1:v7iAyDiNZjHES45P1JPM3SMvkw0VNeJtz0XSVxkRwOY= github.com/IncSW/geoip2 v0.1.2/go.mod h1:adcasR40vXiUBjtzdaTTKL/6wSf+fgO4M8Gve/XzPUk= -github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= -github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= -github.com/antchfx/xmlquery v1.3.15 h1:aJConNMi1sMha5G8YJoAIF5P+H+qG1L73bSItWHo8Tw= -github.com/antchfx/xmlquery v1.3.15/go.mod h1:zMDv5tIGjOxY/JCNNinnle7V/EwthZ5IT8eeCGJKRWA= github.com/antchfx/xmlquery v1.3.16 h1:OCevguHq93z9Y4vb9xpRmU4Cc9lMVoiMkMbBNZVDeBM= github.com/antchfx/xmlquery v1.3.16/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA= -github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes= github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY= github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= @@ -37,8 +25,6 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s= -github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas= github.com/deepmap/oapi-codegen v1.13.0 h1:cnFHelhsRQbYvanCUAbRSn/ZpkUb1HPRlQcu8YqSORQ= github.com/deepmap/oapi-codegen v1.13.0/go.mod h1:Amy7tbubKY9qkZOXqymI3Z6xSbndmu+atMJheLdyg44= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -50,7 +36,7 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -69,11 +55,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -83,8 +66,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/influxdata/influxdb-client-go/v2 v2.12.2 h1:uYABKdrEKlYm+++qfKdbgaHKBPmoWR5wpbmj6MBB/2g= -github.com/influxdata/influxdb-client-go/v2 v2.12.2/go.mod h1:YteV91FiQxRdccyJ2cHvj2f/5sq4y4Njqu1fQzsQCOU= github.com/influxdata/influxdb-client-go/v2 v2.12.3 h1:28nRlNMRIV4QbtIUvxhWqaxn0IpXeMSkY/uJa/O/vC4= github.com/influxdata/influxdb-client-go/v2 v2.12.3/go.mod h1:IrrLUbCjjfkmRuaCiGQg4m2GbkaeJDcuWoxiWdQEbA0= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= @@ -135,9 +116,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= @@ -168,8 +149,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg= golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -181,7 +160,6 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= @@ -202,7 +180,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -219,7 +196,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= @@ -235,10 +211,9 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/influx.go b/influx.go index 901dd1c..154442a 100644 --- a/influx.go +++ b/influx.go @@ -6,7 +6,7 @@ import ( "fmt" "sort" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" "github.com/influxdata/influxdb-client-go/v2/api/write" diff --git a/logging/README.md b/logging/README.md new file mode 100644 index 0000000..f0e85f0 --- /dev/null +++ b/logging/README.md @@ -0,0 +1,38 @@ +# logging + +This module provides thread-safe logging and Discord notifications as a part of our [Mirror](https://github.com/COSI-Lab/Mirror) project. + +![Screenshot](screenshot.png) + +There are plenty of low-priority improvements that can be made to this module. + +## Example + +```go +package main + +import ( + "github.com/COSI-Lab/logging" +) + +func main() { + // Optional: If you only want messages to be displayed in the terminal there is no need to run `Setup` + // logging.Setup("https://discord.com/api/webhooks/987654321123456789/S3cR3TwebH00KuRl", "01234567890123456789") + + ch := make(chan struct{}) + + go func() { + // The last argument to all the logging functions is a variadic ...interface{} which is eventually evulated with fmt.Println(...v) + logging.InfoWithAttachment(bytes("Attachment"), "Hello", "World!") + ch <- struct{}{} + }() + + go func() { + logging.ErrorToDiscord("This subsystem failed!") + ch <- struct{}{} + }() + + <-ch + <-ch +} +``` diff --git a/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000..7305975 --- /dev/null +++ b/logging/logging.go @@ -0,0 +1,277 @@ +package logging + +import ( + "bytes" + "encoding/json" + "fmt" + "mime/multipart" + "net/http" + "sync" + "time" +) + +type messageType int + +const ( + typeInfo messageType = iota + typeWarning + typeError + typePanic + typeSuccess +) + +var logger = threadSafeLogger{} + +type threadSafeLogger struct { + sync.Mutex + sendHooks bool + discordHookURL string + discordPingID string +} + +// Setup primes the threadSafeLogger to send messages to a Discord server +// hookURL is the weebhook URL created by the Discord server in the Integrations settings +// pingID is the Discord user or role ID to ping when sending important messages +// Setup can be safely called at any time to change the Discord server hookURL and pingID +func Setup(hookURL, pingID string) { + logger.Lock() + logger.discordHookURL = hookURL + logger.discordPingID = pingID + logger.sendHooks = hookURL != "" && pingID != "" + logger.Unlock() +} + +// sendFile creates a multipart form message and sends it to the specified URL +// with the specified content as a file attachment +func sendFile(content []byte) { + if content == nil { + return + } + + logger.Lock() + defer logger.Unlock() + + if !logger.sendHooks { + return + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Add a file attachment to the multipart writer + part, err := writer.CreateFormFile("text", "attachment.txt") + if err != nil { + fmt.Println(time.Now().Format("2006/01/02 15:04:05 "), "\033[1m\033[31m[ERROR] \033[0m| ", err) + return + } + part.Write(content) + writer.Close() + + // Build the request + request, err := http.NewRequest("POST", logger.discordHookURL, body) + if err != nil { + fmt.Println(time.Now().Format("2006/01/02 15:04:05 "), "\033[1m\033[31m[ERROR] \033[0m| ", err) + return + } + request.Header.Add("Content-Type", writer.FormDataContentType()) + + // Execute the request + client := &http.Client{} + response, err := client.Do(request) + + if err != nil { + fmt.Println(time.Now().Format("2006/01/02 15:04:05 "), "\033[1m\033[31m[ERROR] \033[0m| ", err) + return + } + + response.Body.Close() +} + +func sendHook(ping bool, content ...interface{}) { + logger.Lock() + defer logger.Unlock() + + if !logger.sendHooks { + return + } + + var values map[string]string + if ping { + values = map[string]string{"content": fmt.Sprintf("<@%s> %v", logger.discordPingID, fmt.Sprint(content...))} + } else { + values = map[string]string{"content": fmt.Sprintf("%v", fmt.Sprint(content...))} + } + json_data, err := json.Marshal(values) + if err != nil { + fmt.Println(time.Now().Format("2006/01/02 15:04:05 "), "\033[1m\033[31m[ERROR] \033[0m| ", err) + return + } + + send := bytes.NewBuffer(json_data) + _, err = http.Post(logger.discordHookURL, "application/json", send) + if err != nil { + fmt.Println(time.Now().Format("2006/01/02 15:04:05 "), "\033[1m\033[31m[ERROR] \033[0m| ", err) + return + } +} + +func log(mt messageType, v ...interface{}) { + logger.Lock() + fmt.Print(time.Now().Format("2006/01/02 15:04:05 ")) + + switch mt { + case typeInfo: + fmt.Print("\033[1m[INFO] \033[0m| ") + case typeWarning: + fmt.Print("\033[1m\033[33m[WARN] \033[0m| ") + case typeError: + fmt.Print("\033[1m\033[31m[ERROR] \033[0m| ") + case typePanic: + fmt.Print("\033[1m\033[34m[PANIC] \033[0m| ") + case typeSuccess: + fmt.Print("\033[1m\033[32m[SUCCESS] \033[0m| ") + } + + fmt.Println(v...) + logger.Unlock() +} + +// Info logs a message to the terminal with [INFO] prefix +func Info(v ...interface{}) { + log(typeInfo, v...) +} + +// InfoToDiscord logs a message to the terminal with [INFO] prefix +// The message is also forwarded to the Discord server without pinging users +func InfoToDiscord(v ...interface{}) { + log(typeInfo, v...) + go sendHook(false, v...) +} + +// InfoWithAttachment logs a message to the terminal with [INFO] prefix +// If we can send a webhook, the message and attachment are forwarded to the Discord server without pinging users +// If we cannot send a webhook the attachment is sent to the terminal +func InfoWithAttachment(attachment []byte, v ...interface{}) { + log(typeInfo, v...) + if !logger.sendHooks { + log(typeInfo, string(attachment)) + } else { + go func() { + // TODO handle error returned by sendFile + sendFile(attachment) + sendHook(false, v...) + }() + } +} + +// Warn logs a message to the terminal with [WARN] prefix +func Warn(v ...interface{}) { + log(typeWarning, v...) +} + +// WarnToDiscord logs a message to the terminal with [WARN] prefix +// The message is also forwarded to the Discord server without pinging users +func WarnToDiscord(v ...interface{}) { + log(typeWarning, v...) + go sendHook(false, v...) +} + +// WarnWithAttachment logs a message to the terminal with [WARN] prefix +// If we can send a webhook, the message and attachment are forwarded to the Discord server without pinging users +// If we cannot send a webhook the attachment is sent to the terminal +func WarnWithAttachment(attachment []byte, v ...interface{}) { + log(typeWarning, v...) + if !logger.sendHooks { + log(typeWarning, string(attachment)) + } else { + go func() { + // TODO handle error returned by sendFile + sendFile(attachment) + sendHook(false, v...) + }() + } +} + +// Error logs a message to the terminal with [ERROR] prefix +func Error(v ...interface{}) { + log(typeError, v...) +} + +// ErrorToDiscord logs a message to the terminal with [ERROR] prefix +// The message is also forwarded to the Discord server without pinging users +func ErrorToDiscord(v ...interface{}) { + log(typeError, v...) + go sendHook(false, v...) +} + +// ErrorWithAttachment logs a message to the terminal with [ERROR] prefix +// If we can send a webhook, the message and attachment are forwarded to the Discord server without pinging users +// If we cannot send a webhook the attachment is sent to the terminal +func ErrorWithAttachment(attachment []byte, v ...interface{}) { + log(typeError, v...) + if !logger.sendHooks { + log(typeError, string(attachment)) + } else { + go func() { + // TODO handle error returned by sendFile + sendFile(attachment) + sendHook(false, v...) + }() + } +} + +// Panic logs a message to the terminal with [PANIC] prefix +func Panic(v ...interface{}) { + log(typePanic, v...) +} + +// PanicToDiscord logs a message to the terminal with [PANIC] prefix +// The message is also forwarded to the Discord server and pings the users +func PanicToDiscord(v ...interface{}) { + log(typePanic, v...) + go sendHook(true, v...) +} + +// PanicWithAttachment logs a message to the terminal with [PANIC] prefix +// If we can send a webhook, the message and attachment are forwarded to the Discord server and pings the users +// If we cannot send a webhook the attachment is sent to the terminal +func PanicWithAttachment(attachment []byte, v ...interface{}) { + log(typePanic, v...) + if !logger.sendHooks { + log(typePanic, string(attachment)) + } else { + go func() { + // TODO handle error returned by sendFile + sendFile(attachment) + sendHook(true, v...) + }() + } +} + +// Success logs a message to the terminal with [SUCCESS] prefix +func Success(v ...interface{}) { + log(typeSuccess, v...) +} + +// SuccessToDiscord logs a message to the terminal with [SUCCESS] prefix +// The message is also forwarded to the Discord server without pinging users +func SuccessToDiscord(v ...interface{}) { + log(typeSuccess, v...) + go sendHook(false, v...) +} + +// SuccessWithAttachment logs a message to the terminal with [SUCCESS] prefix +// If we can send a webhook, the message and attachment are forwarded to the Discord server without pinging users +// If we cannot send a webhook the attachment is sent to the terminal +func SuccessWithAttachment(attachment []byte, v ...interface{}) { + log(typeSuccess, v...) + if !logger.sendHooks { + log(typeSuccess, string(attachment)) + } else { + go func() { + // TODO handle error returned by sendFile + sendFile(attachment) + sendHook(false, v...) + }() + } +} diff --git a/logging/screenshot.png b/logging/screenshot.png new file mode 100644 index 0000000..48ae04d Binary files /dev/null and b/logging/screenshot.png differ diff --git a/main.go b/main.go index 99bbbe1..01cc04c 100644 --- a/main.go +++ b/main.go @@ -10,8 +10,8 @@ import ( "syscall" "time" - "github.com/COSI-Lab/geoip" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/geoip" + "github.com/COSI-Lab/Mirror/logging" "github.com/joho/godotenv" ) diff --git a/map.go b/map.go index 70d2b3e..d2998a4 100644 --- a/map.go +++ b/map.go @@ -4,7 +4,7 @@ import ( "net" "net/http" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" "github.com/gorilla/mux" "github.com/gorilla/websocket" ) diff --git a/nginx.go b/nginx.go index 63b30e6..2243743 100644 --- a/nginx.go +++ b/nginx.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "github.com/COSI-Lab/datarithms" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/datarithms" + "github.com/COSI-Lab/Mirror/logging" "github.com/IncSW/geoip2" "github.com/nxadm/tail" ) diff --git a/rsyncd.go b/rsyncd.go index 74bc056..96d8ec4 100644 --- a/rsyncd.go +++ b/rsyncd.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/COSI-Lab/datarithms" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/datarithms" + "github.com/COSI-Lab/Mirror/logging" "github.com/nxadm/tail" ) diff --git a/sync.go b/sync.go index 9c9afe4..864f500 100644 --- a/sync.go +++ b/sync.go @@ -8,8 +8,8 @@ import ( "sync" "time" - "github.com/COSI-Lab/datarithms" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/datarithms" + "github.com/COSI-Lab/Mirror/logging" ) type Status struct { diff --git a/torrent.go b/torrent.go index 989bfc1..172c8c0 100644 --- a/torrent.go +++ b/torrent.go @@ -11,7 +11,7 @@ import ( "syscall" "time" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" "github.com/gocolly/colly" ) diff --git a/tracking.go b/tracking.go index e5293d7..741a773 100644 --- a/tracking.go +++ b/tracking.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" influxdb2 "github.com/influxdata/influxdb-client-go/v2" "github.com/influxdata/influxdb-client-go/v2/api" ) diff --git a/webserver.go b/webserver.go index d7ab107..6a4aaa2 100644 --- a/webserver.go +++ b/webserver.go @@ -6,7 +6,7 @@ import ( "net/http" "sync" - "github.com/COSI-Lab/logging" + "github.com/COSI-Lab/Mirror/logging" "github.com/gorilla/mux" "github.com/wcharczuk/go-chart/v2" )