github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/db/metric_bucket.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2016-2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package db 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "time" 26 27 "github.com/pkg/errors" 28 "gorm.io/gorm" 29 30 "github.com/e154/smart-home/common" 31 "github.com/e154/smart-home/common/apperr" 32 ) 33 34 // MetricBuckets ... 35 type MetricBuckets struct { 36 Db *gorm.DB 37 Timescale bool 38 } 39 40 // MetricBucket ... 41 type MetricBucket struct { 42 Value json.RawMessage `gorm:"type:jsonb;not null"` 43 Metric *Metric 44 Mins time.Time `gorm:"->"` 45 MetricId int64 46 Time time.Time 47 } 48 49 // TableName ... 50 func (d *MetricBucket) TableName() string { 51 return "metric_bucket" 52 } 53 54 // Add ... 55 func (n MetricBuckets) Add(ctx context.Context, metric *MetricBucket) (err error) { 56 if err = n.Db.WithContext(ctx).Create(&metric).Error; err != nil { 57 err = errors.Wrap(apperr.ErrMetricBucketAdd, err.Error()) 58 } 59 return 60 } 61 62 // List ... 63 func (n *MetricBuckets) List(ctx context.Context, metricId int64, optionItems []string, rFrom, rTo *time.Time, metricRange *common.MetricRange) (list []*MetricBucket, err error) { 64 65 var str string 66 for i, item := range optionItems { 67 str += fmt.Sprintf(" '%s', trunc(avg((value ->> '%[1]s')::numeric), 2)", item) 68 if i+1 < len(optionItems) { 69 str += "," 70 } 71 } 72 73 var interval string 74 var from time.Time 75 var to = time.Now() 76 77 if rTo != nil { 78 to = common.TimeValue(rTo) 79 } 80 81 if metricRange != nil { 82 switch *metricRange { 83 case common.MetricRange6H: 84 interval = "6 seconds" 85 from = to.Add(-6 * time.Hour) 86 case common.MetricRange12H: 87 interval = "12 seconds" 88 from = to.Add(-12 * time.Hour) 89 case common.MetricRange24H: 90 interval = "24 seconds" 91 from = to.Add(-24 * time.Hour) 92 case common.MetricRange7d: 93 interval = "150 seconds" 94 from = to.Add(-24 * 7 * time.Hour) 95 case common.MetricRange30d, common.MetricRange1m: 96 interval = "7 minutes" 97 from = to.Add(-24 * 30 * time.Hour) 98 default: 99 err = errors.Wrap(apperr.ErrMetricBucketGet, fmt.Sprintf("unknown filter %s", metricRange)) 100 return 101 } 102 } 103 104 if rFrom != nil { 105 from = common.TimeValue(rFrom) 106 } 107 108 var num int64 = 1 109 t := from.Sub(to).Seconds() 110 if t > 3600 { 111 num = int64(t / 3600) 112 } 113 114 if metricRange == nil { 115 interval = fmt.Sprintf("%d seconds", num) 116 } 117 118 list = make([]*MetricBucket, 0) 119 120 // c.time between ? and ? 121 if n.Timescale { 122 var q = `SELECT time_bucket('%s', time) AS mins, json_build_object(%s) as value 123 FROM metric_bucket c 124 WHERE c.metric_id = ? and c.time > ? and c.time < ? 125 GROUP BY mins 126 ORDER BY mins ASC 127 LIMIT 3600` 128 if err = n.Db.WithContext(ctx).Raw(fmt.Sprintf(q, interval, str), metricId, from.UTC().Format(time.RFC3339), to.UTC().Format(time.RFC3339)).Scan(&list).Error; err != nil { 129 err = errors.Wrap(apperr.ErrMetricBucketGet, err.Error()) 130 } 131 132 for _, metric := range list { 133 metric.Time = metric.Mins 134 } 135 136 return 137 } 138 139 q := `SELECT TIMESTAMP WITH TIME ZONE 'epoch' + 140 INTERVAL '1 second' * round(extract('epoch' from time) / %[1]d) * %[1]d as time, json_build_object(%s) as value 141 FROM metric_bucket c 142 WHERE c.metric_id = ? and c.time > ? and c.time < ? 143 GROUP BY round(extract('epoch' from c.time) / %[1]d) 144 order by time asc 145 LIMIT 3600` 146 if err = n.Db.WithContext(ctx).Raw(fmt.Sprintf(q, num, str), metricId, from.UTC().Format(time.RFC3339), to.UTC().Format(time.RFC3339)).Scan(&list).Error; err != nil { 147 err = errors.Wrap(apperr.ErrMetricBucketGet, err.Error()) 148 } 149 150 return 151 } 152 153 // DeleteOldest ... 154 func (n *MetricBuckets) DeleteOldest(ctx context.Context, days int) (err error) { 155 bucket := &MetricBucket{} 156 if err = n.Db.WithContext(ctx).Last(&bucket).Error; err != nil { 157 err = errors.Wrap(apperr.ErrLogDelete, err.Error()) 158 return 159 } 160 err = n.Db.WithContext(ctx).Delete(&MetricBucket{}, 161 fmt.Sprintf(`time < CAST('%s' AS DATE) - interval '%d days'`, 162 bucket.Time.UTC().Format("2006-01-02 15:04:05"), days)).Error 163 if err != nil { 164 err = errors.Wrap(apperr.ErrMetricBucketDelete, err.Error()) 165 } 166 return 167 } 168 169 // DeleteByMetricId ... 170 func (n MetricBuckets) DeleteByMetricId(ctx context.Context, metricId int64) (err error) { 171 if err = n.Db.WithContext(ctx).Delete(&MetricBucket{}, "metric_id = ?", metricId).Error; err != nil { 172 err = errors.Wrap(apperr.ErrMetricBucketDelete, err.Error()) 173 } 174 return 175 } 176 177 // DeleteById ... 178 func (n MetricBuckets) DeleteById(ctx context.Context, id int64) (err error) { 179 if err = n.Db.WithContext(ctx).Delete(&MetricBucket{}, "id = ?", id).Error; err != nil { 180 err = errors.Wrap(apperr.ErrMetricBucketDelete, err.Error()) 181 } 182 return 183 } 184 185 // AddMultiple ... 186 func (n *MetricBuckets) AddMultiple(ctx context.Context, buckets []*MetricBucket) (err error) { 187 if err = n.Db.WithContext(ctx).Create(&buckets).Error; err != nil { 188 err = errors.Wrap(apperr.ErrMetricBucketAdd, err.Error()) 189 } 190 return 191 }