Skip to content

Commit fd3e384

Browse files
authored
Added raw data extraction support and documentation screenshots (#2)
1 parent 8d4d81c commit fd3e384

File tree

14 files changed

+258
-101
lines changed

14 files changed

+258
-101
lines changed

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary, built with `go test -c`
9+
*.test
10+
11+
# Output of the go coverage tool, specifically when used with LiteIDE
12+
*.out
13+
14+
# Dependency directories (remove the comment below to include it)
15+
vendor/
16+
17+
# IDEs and editors
18+
.vscode
19+
.idea
20+
.env
21+
.tool-versions
22+
23+
cover.out

README.md

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
# Arduino AWS S3 exporter
1+
# Arduino AWS S3 CSV exporter
22

33
This project provides a way to extract time series samples from Arduino cloud, publishing to a S3 destination bucket.
4-
Things can be filterd by tags.
4+
Data are extracted at the given resolution via a scheduled Lambda function. Then samples are stored in CSV files and saved to S3.
55

6-
## Deployment schema
6+
## Architecture
77

8-
S3 exporter is based on a Go lambda function triggered by periodic events from EventBridge.
9-
Job is configured to extract samples for a 60min time window.
10-
One file is created per run, containing all samples for the given hour. Time series samples are exported in UTC.
8+
S3 exporter is based on a Go lambda function triggered by periodic event from EventBridge.
9+
Job is configured to extract samples for a 60min time window with the default resolution of 5min.
10+
One file is created per execution, containing all samples for selected things. Time series samples are exported at UTC timezone.
11+
By default, all Arduino things present in the account are exported: it is possible to filter them via tags configuration.
1112

1213
CSV produced has the following structure:
1314
```console
@@ -28,19 +29,40 @@ Files are organized in S3 bucket by date and files of the same day are grouped.
2829
## Deployment via Cloud Formation Template
2930

3031
It is possible to deploy required resources via [cloud formation template](deployment/cloud-formation-template/deployment.yaml)
31-
Required steps to deploy project:
32-
* compile lambda
33-
```console
34-
foo@bar:~$ ./compile-lambda.sh
35-
arduino-s3-integration-lambda.zip archive created
36-
```
37-
* Save zip file on an S3 bucket accessible by the AWS account
38-
* Start creation of a new cloud formation stack provising the [cloud formation template](deployment/cloud-formation-template/deployment.yaml)
39-
* Fill all required parameters (mandatory: Arduino API key and secret, S3 bucket and key where code has been uploaded, destination S3 bucket. Optionally, tag filter for filtering things, organization identifier and samples resolution)
32+
33+
CFT deployment requires:
34+
* an AWS account with rights for creating a new CFT stack. Account must have rights to create:
35+
* S3 buckets
36+
* IAM Roles
37+
* Lambda functions
38+
* EventBridge rules
39+
* SSM parameters (Parameter store)
40+
41+
Before stack creation, two S3 buckets has to be created:
42+
* a temporary bucket where lambda binaries and CFT can be uploaded
43+
* CSVs destination bucket, where all generated file will be uploaded
44+
bucket must be in the same region where stack will be created.
45+
46+
To deploy stack, follow these steps:
47+
* download [lambda code binaries](deployment/binaries/arduino-s3-integration-lambda.zip) and [Cloud Formation Template](deployment/cloud-formation-template/deployment.yaml)
48+
* Upload CFT and binary zip file on an S3 bucket accessible by the AWS account. For the CFT yaml file, copy the Object URL (it will be required in next step).
49+
50+
![object URL](docs/objecturl.png)
51+
52+
* Start creation of a new cloud formation stack. Follow these steps:
53+
54+
![CFT 1](docs/cft-stack-1.png)
55+
56+
* Fill all required parameters.
57+
<br/>**Mandatory**: Arduino API key and secret, S3 bucket where code has been uploaded, destination S3 bucket
58+
<br/>**Optional**: tag filter for filtering things, organization identifier and samples resolution
59+
60+
![CFT 2](docs/cft-stack-2.png)
4061

4162
### Configuration parameters
4263

43-
Here is a list of all configuration properties available in 'Parameter store'.
64+
Here is a list of all configuration properties available in 'Parameter store'.
65+
These parameters are filled by CFT and can be adjusted later in case of need (for example, API keys rotation)
4466

4567
| Parameter | Description |
4668
| --------- | ----------- |
@@ -51,6 +73,28 @@ Here is a list of all configuration properties available in 'Parameter store'.
5173
| /arduino/s3-importer/iot/samples-resolution-seconds | (optional) samples resolution (default: 300s) |
5274
| /arduino/s3-importer/destination-bucket | S3 destination bucket |
5375

54-
### Policies
76+
### Tag filtering
77+
78+
It is possible to filter only the Things of interest.
79+
You can use tag filtering if you need to reduce data export to a specific set of Things.
5580

56-
See policies defined in [cloud formation template](deployment/cloud-formation-template/deployment.yaml)
81+
* Add a tag in Arduino Cloud UI on all Things you want to export. To do that, select a thing and go in 'metadata' section and add a new tag.
82+
83+
84+
![tag 1](docs/tag-1.png)
85+
86+
![tag 2](docs/tag-2.png)
87+
88+
* Configure tag filter during CFT creation of by editing '/arduino/s3-importer/iot/filter/tags' parameter.
89+
90+
![tag filter](docs/tag-filter.png)
91+
92+
### Building code
93+
94+
Code requires go v 1.22.
95+
To compile code:
96+
97+
```console
98+
foo@bar:~$ ./compile-lambda.sh
99+
arduino-s3-integration-lambda.zip archive created
100+
```

business/tsextractor/tsextractor.go

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,27 @@ func (a *TsExtractor) ExportTSToS3(
8181
defer func() { <-tokens }()
8282
defer wg.Done()
8383

84-
// Populate numeric time series data
85-
err := a.populateNumericTSDataIntoS3(ctx, from, to, thingID, thing, resolution, writer)
86-
if err != nil {
87-
a.logger.Error("Error populating time series data: ", err)
88-
return
89-
}
90-
91-
// Populate string time series data, if any
92-
err = a.populateStringTSDataIntoS3(ctx, from, to, thingID, thing, resolution, writer)
93-
if err != nil {
94-
a.logger.Error("Error populating string time series data: ", err)
95-
return
84+
if resolution <= 0 {
85+
// Populate raw time series data
86+
err := a.populateRawTSDataIntoS3(ctx, from, to, thingID, thing, writer)
87+
if err != nil {
88+
a.logger.Error("Error populating raw time series data: ", err)
89+
return
90+
}
91+
} else {
92+
// Populate numeric time series data
93+
err := a.populateNumericTSDataIntoS3(ctx, from, to, thingID, thing, resolution, writer)
94+
if err != nil {
95+
a.logger.Error("Error populating time series data: ", err)
96+
return
97+
}
98+
99+
// Populate string time series data, if any
100+
err = a.populateStringTSDataIntoS3(ctx, from, to, thingID, thing, resolution, writer)
101+
if err != nil {
102+
a.logger.Error("Error populating string time series data: ", err)
103+
return
104+
}
96105
}
97106
}(thingID, thing, writer)
98107
}
@@ -121,6 +130,10 @@ func (a *TsExtractor) populateNumericTSDataIntoS3(
121130
resolution int,
122131
writer *csv.CsvWriter) error {
123132

133+
if resolution <= 60 {
134+
resolution = 60
135+
}
136+
124137
var batched *iotclient.ArduinoSeriesBatch
125138
var err error
126139
var retry bool
@@ -254,9 +267,7 @@ func (a *TsExtractor) populateStringTSDataIntoS3(
254267
if value == nil {
255268
continue
256269
}
257-
if strValue, ok := value.(string); ok {
258-
samples = append(samples, composeRow(ts, thingID, thing.Name, propertyID, propertyName, strValue))
259-
}
270+
samples = append(samples, composeRow(ts, thingID, thing.Name, propertyID, propertyName, interfaceToString(value)))
260271
}
261272
}
262273

@@ -270,3 +281,78 @@ func (a *TsExtractor) populateStringTSDataIntoS3(
270281

271282
return nil
272283
}
284+
285+
func (a *TsExtractor) populateRawTSDataIntoS3(
286+
ctx context.Context,
287+
from time.Time,
288+
to time.Time,
289+
thingID string,
290+
thing iotclient.ArduinoThing,
291+
writer *csv.CsvWriter) error {
292+
293+
var batched *iotclient.ArduinoSeriesRawBatch
294+
var err error
295+
var retry bool
296+
for i := 0; i < 3; i++ {
297+
batched, retry, err = a.iotcl.GetRawTimeSeriesByThing(ctx, thingID, from, to)
298+
if !retry {
299+
break
300+
} else {
301+
// This is due to a rate limit on the IoT API, we need to wait a bit before retrying
302+
a.logger.Infof("Rate limit reached for thing %s. Waiting 1 second before retrying.\n", thingID)
303+
time.Sleep(1 * time.Second)
304+
}
305+
}
306+
if err != nil {
307+
return err
308+
}
309+
310+
sampleCount := int64(0)
311+
samples := [][]string{}
312+
for _, response := range batched.Responses {
313+
if response.CountValues == 0 {
314+
continue
315+
}
316+
317+
propertyID := strings.Replace(response.Query, "property.", "", 1)
318+
a.logger.Infof("Thing %s - Query %s Property %s - %d values\n", thingID, response.Query, propertyID, response.CountValues)
319+
sampleCount += response.CountValues
320+
321+
propertyName := extractPropertyName(thing, propertyID)
322+
323+
for i := 0; i < len(response.Times); i++ {
324+
325+
ts := response.Times[i]
326+
value := response.Values[i]
327+
if value == nil {
328+
continue
329+
}
330+
samples = append(samples, composeRow(ts, thingID, thing.Name, propertyID, propertyName, interfaceToString(value)))
331+
}
332+
}
333+
334+
// Write samples to csv ouput file
335+
if len(samples) > 0 {
336+
if err := writer.Write(samples); err != nil {
337+
return err
338+
}
339+
a.logger.Debugf("Thing %s [%s] raw data saved %d values\n", thingID, thing.Name, sampleCount)
340+
}
341+
342+
return nil
343+
}
344+
345+
func interfaceToString(value interface{}) string {
346+
switch v := value.(type) {
347+
case string:
348+
return v
349+
case int:
350+
return strconv.Itoa(v)
351+
case float64:
352+
return strconv.FormatFloat(v, 'f', 3, 64)
353+
case bool:
354+
return strconv.FormatBool(v)
355+
default:
356+
return fmt.Sprintf("%v", v)
357+
}
358+
}

compile-lambda.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/bin/bash
22

3+
mkdir -p deployment/binaries
34
GOOS=linux CGO_ENABLED=0 go build -o bootstrap -tags lambda.norpc lambda.go
45
zip arduino-s3-integration-lambda.zip bootstrap
6+
mv arduino-s3-integration-lambda.zip deployment/binaries/
57
rm bootstrap
6-
echo "arduino-s3-integration-lambda.zip archive created"
8+
echo "deployment/binaries/arduino-s3-integration-lambda.zip archive created"
Binary file not shown.

deployment/cloud-formation-template/deployment.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Description: Arduino S3 data exporter. For deployment and architectural details,
44
Parameters:
55
LambdaFunctionName:
66
Type: String
7-
Default: 'arduino-s3-data-exporter'
7+
Default: 'arduino-s3-csv-data-exporter'
88
Description: Name of the Lambda function.
99

1010
LambdaCodeS3Bucket:
@@ -38,12 +38,11 @@ Parameters:
3838
Resolution:
3939
Type: Number
4040
Default: 300
41-
Description: Samples resolution in seconds. Default is 5 minutes (300s).
41+
Description: Samples resolution in seconds. Default is 5 minutes (300s). Set to -1 to export raw data.
4242

4343
DestinationS3Bucket:
4444
Type: String
45-
Default: 'aws-arduino-data-export'
46-
Description: S3 bucket where saved samples will be stored.
45+
Description: S3 bucket where CSV files will be stored.
4746

4847
Resources:
4948

@@ -164,6 +163,7 @@ Resources:
164163
Targets:
165164
- Arn: !GetAtt LambdaFunction.Arn
166165
Id: LambdaTarget
166+
Input: '{}'
167167
State: ENABLED
168168

169169
# Permission for EventBridge to invoke Lambda

docs/cft-stack-1.png

95.3 KB
Loading

docs/cft-stack-2.png

79.5 KB
Loading

docs/objecturl.png

7.82 KB
Loading

docs/tag-1.png

22.7 KB
Loading

docs/tag-2.png

26.6 KB
Loading

docs/tag-filter.png

14.9 KB
Loading

0 commit comments

Comments
 (0)