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,6 +1,7 @@
package config package config
import ( import (
"fmt"
"os" "os"
"strconv" "strconv"
) )
@ -14,13 +15,25 @@ type Config struct {
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{
@ -32,6 +45,7 @@ func LoadConfig() *Config {
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)) conditionInSec := int64(sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit))
conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec 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)