Data retention

This commit is contained in:
Jan Zípek 2024-04-01 10:33:20 +02:00
parent 2421536497
commit 74a4019dc5
Signed by: kamen
GPG Key ID: A17882625B33AC31
9 changed files with 93 additions and 34 deletions

View File

@ -56,6 +56,7 @@ ENV AUTH_USERNAME admin
ENV AUTH_PASSWORD password ENV AUTH_PASSWORD password
ENV AUTH_KEY password ENV AUTH_KEY password
ENV AUTH_ENABLED true ENV AUTH_ENABLED true
ENV DATA_RETENTION_IN_DAYS 0
EXPOSE ${PORT} EXPOSE ${PORT}
VOLUME [ "/data" ] VOLUME [ "/data" ]

View File

@ -47,13 +47,19 @@ export const DashboardContextProvider = ({
}) => { }) => {
const viewport = useViewportSize() const viewport = useViewportSize()
const [dashboardId, setDashboardId] = useState(-1) const { getValues: getQuery, setValues: setQuery } = useQueryString()
const [dashboardId, setDashboardId] = useState(() => {
const query = getQuery()
const queryDashboardId = +(query['dashboard'] ?? '')
return !isNaN(queryDashboardId) ? queryDashboardId : -1
})
const isDashboardSelected = !isNaN(dashboardId) && dashboardId >= 0 const isDashboardSelected = !isNaN(dashboardId) && dashboardId >= 0
const dashboards = useQuery(['/dashboards'], getDashboards) const dashboards = useQuery(['/dashboards'], getDashboards)
const { getValues: getQuery, setValues: setQuery } = useQueryString()
const dashboard = useQuery( const dashboard = useQuery(
['/dashboards', dashboardId], ['/dashboards', dashboardId],
() => getDashboard(dashboardId), () => getDashboard(dashboardId),
@ -105,9 +111,9 @@ export const DashboardContextProvider = ({
const [filter, setFilter] = useState<FilterValue>(() => { const [filter, setFilter] = useState<FilterValue>(() => {
const query = getQuery() const query = getQuery()
const queryFilter = query.get('interval') const queryFilter = query['interval']
const queryFilterFrom = query.get('from') const queryFilterFrom = query['from']
const queryFilterTo = query.get('to') const queryFilterTo = query['to']
const presetInterval = const presetInterval =
queryFilter && queryFilter &&
@ -129,13 +135,15 @@ export const DashboardContextProvider = ({
useEffect(() => { useEffect(() => {
setQuery({ setQuery({
...getQuery(),
dashboard: dashboardId.toString(),
interval: filter.interval, interval: filter.interval,
...(filter.interval === 'custom' && { ...(filter.interval === 'custom' && {
from: filter.customFrom.toISOString(), from: filter.customFrom.toISOString(),
to: filter.customTo.toISOString(), to: filter.customTo.toISOString(),
}), }),
}) })
}, [filter]) }, [filter, dashboardId])
const verticalMode = viewport.width < 800 const verticalMode = viewport.width < 800

View File

@ -21,8 +21,13 @@ export const useQueryString = () => {
const getValues = useCallback(() => { const getValues = useCallback(() => {
const [, searchParams] = valuesRef.current const [, searchParams] = valuesRef.current
const result = {} as Record<string, string>
return searchParams searchParams.forEach((value, key) => {
result[key] = value
})
return result
}, []) }, [])
const setValues = useCallback((values: Record<string, string>) => { const setValues = useCallback((values: Record<string, string>) => {

View File

@ -6,3 +6,5 @@ AUTH_ENABLED=true
AUTH_USERNAME=admin AUTH_USERNAME=admin
AUTH_PASSWORD=password AUTH_PASSWORD=password
AUTH_KEY=password AUTH_KEY=password
# How long should the data be stored
DATA_RETENTION_IN_DAYS=365

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"log"
"time" "time"
) )
@ -9,7 +10,19 @@ func (s *Server) StartCleaner() {
go func() { go func() {
for { for {
s.Services.Sessions.Cleanup() err := s.Services.Sessions.Cleanup()
if err != nil {
log.Println("Error cleaning up sessions:", err)
}
if s.Config.DataRetentionInDays > 0 {
err := s.Services.SensorValues.Cleanup(s.Config.DataRetentionInDays)
if err != nil {
log.Println("Error cleaning up sensor values:", err)
}
}
<-ticker.C <-ticker.C
} }
}() }()

View File

@ -1,37 +1,51 @@
package config package config
import ( import (
"fmt"
"os" "os"
"strconv" "strconv"
) )
type Config struct { type Config struct {
Mode string Mode string
DatabaseUrl string DatabaseUrl string
Port int Port int
Ip string Ip string
AuthEnabled bool AuthEnabled bool
AuthUsername string AuthUsername string
AuthPassword string AuthPassword string
AuthKey string AuthKey string
DataRetentionInDays int64
} }
func LoadConfig() *Config { func LoadConfig() *Config {
port, err := strconv.Atoi(os.Getenv("PORT")) port, err := strconv.Atoi(os.Getenv("PORT"))
if err != nil { if err != nil {
panic(err) panic(fmt.Errorf("PORT must be an integer: %v", err))
}
dataRetentionInDaysStr := os.Getenv("DATA_RETENTION_IN_DAYS")
dataRetentionInDays := 0
if dataRetentionInDaysStr != "" {
dataRetentionInDays, err = strconv.Atoi(dataRetentionInDaysStr)
if err != nil {
panic(fmt.Errorf("DATA_RETENTION_IN_DAYS must be an integer: %v", err))
}
} }
config := Config{ config := Config{
Mode: os.Getenv("GIN_MODE"), Mode: os.Getenv("GIN_MODE"),
DatabaseUrl: os.Getenv("DATABASE_URL"), DatabaseUrl: os.Getenv("DATABASE_URL"),
Port: port, Port: port,
Ip: os.Getenv("BIND_IP"), Ip: os.Getenv("BIND_IP"),
AuthEnabled: os.Getenv("AUTH_ENABLED") != "false", AuthEnabled: os.Getenv("AUTH_ENABLED") != "false",
AuthUsername: os.Getenv("AUTH_USERNAME"), AuthUsername: os.Getenv("AUTH_USERNAME"),
AuthPassword: os.Getenv("AUTH_PASSWORD"), AuthPassword: os.Getenv("AUTH_PASSWORD"),
AuthKey: os.Getenv("AUTH_KEY"), AuthKey: os.Getenv("AUTH_KEY"),
DataRetentionInDays: int64(dataRetentionInDays),
} }
// TODO: Crash when any auth* param is empty // TODO: Crash when any auth* param is empty

View File

@ -84,10 +84,9 @@ func (s *AlertsEvaluatorService) EvaluateAlert(alert *models.AlertItem) error {
Value: condition["value"].(float64), Value: condition["value"].(float64),
} }
value, err := s.ctx.Services.SensorValues.GetLatest(sensorValueCondition.SensorId, time.Now().Unix())
lastValue = value.Value
sensorId = int64(sensorValueCondition.SensorId) sensorId = int64(sensorValueCondition.SensorId)
value, err := s.ctx.Services.SensorValues.GetLatest(sensorValueCondition.SensorId, time.Now().Unix())
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
lastValue = float64(0) lastValue = float64(0)
@ -96,6 +95,7 @@ func (s *AlertsEvaluatorService) EvaluateAlert(alert *models.AlertItem) error {
return fmt.Errorf("error getting sensor value: %v", err) return fmt.Errorf("error getting sensor value: %v", err)
} }
} else { } else {
lastValue = value.Value
conditionMet = evaluateSensorValueCondition(sensorValueCondition, value) conditionMet = evaluateSensorValueCondition(sensorValueCondition, value)
} }
@ -110,22 +110,23 @@ func (s *AlertsEvaluatorService) EvaluateAlert(alert *models.AlertItem) error {
ValueUnit: condition["valueUnit"].(string), ValueUnit: condition["valueUnit"].(string),
} }
value, err := s.ctx.Services.SensorValues.GetLatest(sensorLastContactCondition.SensorId, time.Now().Unix())
lastValue = float64(value.Timestamp)
sensorId = sensorLastContactCondition.SensorId sensorId = sensorLastContactCondition.SensorId
value, err := s.ctx.Services.SensorValues.GetLatest(sensorLastContactCondition.SensorId, time.Now().Unix())
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
lastValue = float64(0) lastValue = float64(0)
} else { } else {
return fmt.Errorf("error getting sensor last contact value: %v", err) return fmt.Errorf("error getting sensor last contact value: %v", err)
} }
} else {
lastValue = float64(value.Timestamp)
conditionInSec := int64(sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit))
conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec
} }
conditionInSec := int64(sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit))
conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec
break break
} }

View File

@ -0,0 +1 @@
package services

View File

@ -2,6 +2,7 @@ package services
import ( import (
"database/sql" "database/sql"
"fmt"
"time" "time"
) )
@ -41,6 +42,19 @@ func (s *SensorValuesService) GetLatest(sensorId int64, to int64) (*sensorValue,
return &value, nil return &value, nil
} }
func (s *SensorValuesService) Cleanup(retentionInDays int64) error {
if retentionInDays <= 0 {
return fmt.Errorf("retentionInDays must be greater than 0")
}
_, err := s.ctx.DB.Exec("DELETE FROM sensor_values WHERE timestamp < ?", time.Now().Unix()-(retentionInDays*24*60*60))
if err != nil {
return err
}
return nil
}
func (s *SensorValuesService) getValueListQuery(sensorId int64, from int64, to int64, divide int64) (*sql.Rows, error) { func (s *SensorValuesService) getValueListQuery(sensorId int64, from int64, to int64, divide int64) (*sql.Rows, error) {
if divide == 1 { if divide == 1 {
return s.ctx.DB.Query("SELECT timestamp, value FROM sensor_values WHERE sensor_id = ? AND timestamp > ? AND timestamp < ? ORDER BY timestamp ASC", sensorId, from, to) return s.ctx.DB.Query("SELECT timestamp, value FROM sensor_values WHERE sensor_id = ? AND timestamp > ? AND timestamp < ? ORDER BY timestamp ASC", sensorId, from, to)