@@ -17,6 +17,7 @@ package tsextractor
17
17
18
18
import (
19
19
"context"
20
+ "encoding/json"
20
21
"fmt"
21
22
"strconv"
22
23
"strings"
@@ -28,7 +29,6 @@ import (
28
29
29
30
"github.com/arduino/aws-s3-integration/internal/csv"
30
31
"github.com/arduino/aws-s3-integration/internal/iot"
31
- "github.com/arduino/aws-s3-integration/internal/s3"
32
32
iotclient "github.com/arduino/iot-client-go/v2"
33
33
"github.com/sirupsen/logrus"
34
34
)
@@ -37,11 +37,11 @@ const importConcurrency = 10
37
37
const retryCount = 5
38
38
39
39
type TsExtractor struct {
40
- iotcl * iot.Client
40
+ iotcl iot.API
41
41
logger * logrus.Entry
42
42
}
43
43
44
- func New (iotcl * iot.Client , logger * logrus.Entry ) * TsExtractor {
44
+ func New (iotcl iot.API , logger * logrus.Entry ) * TsExtractor {
45
45
return & TsExtractor {iotcl : iotcl , logger : logger }
46
46
}
47
47
@@ -51,36 +51,42 @@ func computeTimeAlignment(resolutionSeconds, timeWindowInMinutes int) (time.Time
51
51
resolutionSeconds = 300 // Align to 5 minutes
52
52
}
53
53
to := time .Now ().Truncate (time .Duration (resolutionSeconds ) * time .Second ).UTC ()
54
+ if resolutionSeconds <= 900 {
55
+ // Shift time window to avoid missing data
56
+ to = to .Add (- time .Duration (300 ) * time .Second )
57
+ }
54
58
from := to .Add (- time .Duration (timeWindowInMinutes ) * time .Minute )
55
59
return from , to
56
60
}
57
61
58
- func (a * TsExtractor ) ExportTSToS3 (
62
+ func isRawResolution (resolution int ) bool {
63
+ return resolution <= 0
64
+ }
65
+
66
+ func (a * TsExtractor ) ExportTSToFile (
59
67
ctx context.Context ,
60
68
timeWindowInMinutes int ,
61
69
thingsMap map [string ]iotclient.ArduinoThing ,
62
70
resolution int ,
63
- destinationS3Bucket string ) error {
71
+ aggregationStat string ) ( * csv. CsvWriter , time. Time , error ) {
64
72
65
73
// Truncate time to given resolution
66
74
from , to := computeTimeAlignment (resolution , timeWindowInMinutes )
67
75
68
- // Open s3 output writer
69
- s3cl , err := s3 .NewS3Client (destinationS3Bucket )
70
- if err != nil {
71
- return err
72
- }
73
-
74
76
// Open csv output writer
75
- writer , err := csv .NewWriter (from , a .logger )
77
+ writer , err := csv .NewWriter (from , a .logger , isRawResolution ( resolution ) )
76
78
if err != nil {
77
- return err
79
+ return nil , from , err
78
80
}
79
81
80
82
var wg sync.WaitGroup
81
83
tokens := make (chan struct {}, importConcurrency )
82
84
83
- a .logger .Infoln ("=====> Exporting data. Time window: " , timeWindowInMinutes , "m (resolution: " , resolution , "s). From " , from , " to " , to )
85
+ if isRawResolution (resolution ) {
86
+ a .logger .Infoln ("=====> Exporting data. Time window: " , timeWindowInMinutes , "m (resolution: " , resolution , "s). From " , from , " to " , to , " - aggregation: raw" )
87
+ } else {
88
+ a .logger .Infoln ("=====> Exporting data. Time window: " , timeWindowInMinutes , "m (resolution: " , resolution , "s). From " , from , " to " , to , " - aggregation: " , aggregationStat )
89
+ }
84
90
for thingID , thing := range thingsMap {
85
91
86
92
if thing .Properties == nil || len (thing .Properties ) == 0 {
@@ -95,7 +101,7 @@ func (a *TsExtractor) ExportTSToS3(
95
101
defer func () { <- tokens }()
96
102
defer wg .Done ()
97
103
98
- if resolution <= 0 {
104
+ if isRawResolution ( resolution ) {
99
105
// Populate raw time series data
100
106
err := a .populateRawTSDataIntoS3 (ctx , from , to , thingID , thing , writer )
101
107
if err != nil {
@@ -104,7 +110,7 @@ func (a *TsExtractor) ExportTSToS3(
104
110
}
105
111
} else {
106
112
// Populate numeric time series data
107
- err := a .populateNumericTSDataIntoS3 (ctx , from , to , thingID , thing , resolution , writer )
113
+ err := a .populateNumericTSDataIntoS3 (ctx , from , to , thingID , thing , resolution , aggregationStat , writer )
108
114
if err != nil {
109
115
a .logger .Error ("Error populating time series data: " , err )
110
116
return
@@ -124,17 +130,7 @@ func (a *TsExtractor) ExportTSToS3(
124
130
a .logger .Infoln ("Waiting for all data extraction jobs to terminate..." )
125
131
wg .Wait ()
126
132
127
- // Close csv output writer and upload to s3
128
- writer .Close ()
129
- defer writer .Delete ()
130
-
131
- destinationKey := fmt .Sprintf ("%s/%s.csv" , from .Format ("2006-01-02" ), from .Format ("2006-01-02-15-04" ))
132
- a .logger .Infof ("Uploading file %s to bucket %s\n " , destinationKey , s3cl .DestinationBucket ())
133
- if err := s3cl .WriteFile (ctx , destinationKey , writer .GetFilePath ()); err != nil {
134
- return err
135
- }
136
-
137
- return nil
133
+ return writer , from , nil
138
134
}
139
135
140
136
func randomRateLimitingSleep () {
@@ -155,6 +151,7 @@ func (a *TsExtractor) populateNumericTSDataIntoS3(
155
151
thingID string ,
156
152
thing iotclient.ArduinoThing ,
157
153
resolution int ,
154
+ aggregationStat string ,
158
155
writer * csv.CsvWriter ) error {
159
156
160
157
if resolution <= 60 {
@@ -165,7 +162,7 @@ func (a *TsExtractor) populateNumericTSDataIntoS3(
165
162
var err error
166
163
var retry bool
167
164
for i := 0 ; i < retryCount ; i ++ {
168
- batched , retry , err = a .iotcl .GetTimeSeriesByThing (ctx , thingID , from , to , int64 (resolution ))
165
+ batched , retry , err = a .iotcl .GetTimeSeriesByThing (ctx , thingID , from , to , int64 (resolution ), aggregationStat )
169
166
if ! retry {
170
167
break
171
168
} else {
@@ -195,7 +192,7 @@ func (a *TsExtractor) populateNumericTSDataIntoS3(
195
192
196
193
ts := response .Times [i ]
197
194
value := response .Values [i ]
198
- samples = append (samples , composeRow (ts , thingID , thing .Name , propertyID , propertyName , propertyType , strconv .FormatFloat (value , 'f' , - 1 , 64 )))
195
+ samples = append (samples , composeRow (ts , thingID , thing .Name , propertyID , propertyName , propertyType , strconv .FormatFloat (value , 'f' , - 1 , 64 ), aggregationStat ))
199
196
}
200
197
}
201
198
@@ -210,7 +207,20 @@ func (a *TsExtractor) populateNumericTSDataIntoS3(
210
207
return nil
211
208
}
212
209
213
- func composeRow (ts time.Time , thingID string , thingName string , propertyID string , propertyName string , propertyType string , value string ) []string {
210
+ func composeRow (ts time.Time , thingID string , thingName string , propertyID string , propertyName string , propertyType string , value string , aggregation string ) []string {
211
+ row := make ([]string , 8 )
212
+ row [0 ] = ts .UTC ().Format (time .RFC3339 )
213
+ row [1 ] = thingID
214
+ row [2 ] = thingName
215
+ row [3 ] = propertyID
216
+ row [4 ] = propertyName
217
+ row [5 ] = propertyType
218
+ row [6 ] = value
219
+ row [7 ] = aggregation
220
+ return row
221
+ }
222
+
223
+ func composeRawRow (ts time.Time , thingID string , thingName string , propertyID string , propertyName string , propertyType string , value string ) []string {
214
224
row := make ([]string , 7 )
215
225
row [0 ] = ts .UTC ().Format (time .RFC3339 )
216
226
row [1 ] = thingID
@@ -300,7 +310,7 @@ func (a *TsExtractor) populateStringTSDataIntoS3(
300
310
if value == nil {
301
311
continue
302
312
}
303
- samples = append (samples , composeRow (ts , thingID , thing .Name , propertyID , propertyName , propertyType , interfaceToString (value )))
313
+ samples = append (samples , composeRow (ts , thingID , thing .Name , propertyID , propertyName , propertyType , a . interfaceToString (value ), "" ))
304
314
}
305
315
}
306
316
@@ -360,7 +370,7 @@ func (a *TsExtractor) populateRawTSDataIntoS3(
360
370
if value == nil {
361
371
continue
362
372
}
363
- samples = append (samples , composeRow (ts , thingID , thing .Name , propertyID , propertyName , propertyType , interfaceToString (value )))
373
+ samples = append (samples , composeRawRow (ts , thingID , thing .Name , propertyID , propertyName , propertyType , a . interfaceToString (value )))
364
374
}
365
375
}
366
376
@@ -375,7 +385,7 @@ func (a *TsExtractor) populateRawTSDataIntoS3(
375
385
return nil
376
386
}
377
387
378
- func interfaceToString (value interface {}) string {
388
+ func ( a * TsExtractor ) interfaceToString (value interface {}) string {
379
389
switch v := value .(type ) {
380
390
case string :
381
391
return v
@@ -385,6 +395,13 @@ func interfaceToString(value interface{}) string {
385
395
return strconv .FormatFloat (v , 'f' , - 1 , 64 )
386
396
case bool :
387
397
return strconv .FormatBool (v )
398
+ case map [string ]any :
399
+ encoded , err := json .Marshal (v )
400
+ if err != nil {
401
+ a .logger .Error ("Error encoding map to json: " , err )
402
+ return fmt .Sprintf ("%v" , v )
403
+ }
404
+ return string (encoded )
388
405
default :
389
406
return fmt .Sprintf ("%v" , v )
390
407
}
0 commit comments