-
Notifications
You must be signed in to change notification settings - Fork 117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Allow struct field tags for influxdb with a marshaller #389
Changes from all commits
66f10b8
d6a531a
1de226b
ecfa15b
a322109
cf6cb72
6b78fbc
9ff9f0f
fcf097a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package api | ||
|
||
/* | ||
MarshalStructToWritePoint accepts a value that is a custom struct a user creates. It optionally takes in a timestamp that becomes the *write.Point timestamp. | ||
if the timestamp argument is nil | ||
|
||
Example: | ||
|
||
package main | ||
|
||
import ( | ||
"github.com/influxdata/influxdb-client-go/v2/api" | ||
"log" | ||
) | ||
|
||
type influxTestType struct { | ||
Measurement string `influxdb:"measurement"` | ||
Name string `influxdb:"name"` | ||
Title string `influxdb:"title,tag"` | ||
Distance int64 `influxdb:"distance"` | ||
Description string `influxdb:"Description"` | ||
} | ||
|
||
func main() { | ||
writer := api.NewWriteAPI("org", "bucket", nil, nil) | ||
|
||
influxArg := influxTestType{ | ||
Measurement: "foo", | ||
Name: "bar", | ||
Title: "test of the struct write point marshaller", | ||
Distance: 39, | ||
Description: "This tests the MarshalStructToWritePoint", | ||
} | ||
|
||
point, err := api.MarshalStructToWritePoint(influxArg, nil) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
writer.WritePoint(point) | ||
} | ||
*/ | ||
|
||
import ( | ||
"errors" | ||
"github.com/influxdata/influxdb-client-go/v2/api/write" | ||
"log" | ||
"reflect" | ||
"regexp" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const ( | ||
influxdbTag = "influxdb" | ||
tooManyMeasurementsErrorMsg = "more than 1 struct field is tagged as a measurement. Please pick only 1 struct field to be a measurement" | ||
measurementIsNotStringErrorMsg = "the value for the struct field tagged for measurement is not of type string" | ||
tagValueNotStringErrorMsg = "the value for the struct field for a tag is not of type string" | ||
noMeasurementPresentErrorMsg = "no struct field is tagged as a measurement. You must have a measurement" | ||
tooManyTagArgs = "your influx tag contains more than the allowed number of arguments" | ||
secondTagArgPassedButNotTagErrorMsg = "your influx tag has a second argument but it is not for a tag. If you're trying to set a struct field to be a measurement than the only argument that can be passed is 'measurement'" | ||
) | ||
|
||
// Tags is exported in case this is a type a user wants to use in their code | ||
type Tags map[string]string | ||
|
||
// Fields is exported in case this is a type a user wants to use in their code | ||
type Fields map[string]interface{} | ||
Comment on lines
+64
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no need to export this, and if there is no need, we should better not. If it would have to be exported, it would be already done to support the |
||
|
||
// MarshalStructToWritePoint accepts an argument that has a custom struct provided by the user & marshals it into a *write.Point | ||
func MarshalStructToWritePoint(arg interface{}, timestamp *time.Time) (*write.Point, error) { | ||
var measurement string | ||
var tags Tags = make(map[string]string) | ||
var fields Fields = make(map[string]interface{}) | ||
|
||
measurementCount := 0 | ||
ts := time.Now().UTC() | ||
|
||
if timestamp != nil { | ||
ts = *timestamp | ||
} | ||
log.SetFlags(log.Lshortfile) | ||
|
||
argType := reflect.TypeOf(arg) | ||
val := reflect.ValueOf(arg) | ||
|
||
numFields := val.NumField() | ||
|
||
for i := 0; i < numFields; i++ { | ||
if measurementCount > 1 { | ||
return nil, errors.New(tooManyMeasurementsErrorMsg) | ||
} | ||
structFieldVal := val.Field(i) | ||
structFieldName := argType.Field(i).Tag.Get(influxdbTag) | ||
|
||
err := checkEitherTagOrMeasurement(structFieldName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if structFieldName == "measurement" { | ||
measurementFieldVal, ok := structFieldVal.Interface().(string) | ||
if !ok { | ||
return nil, errors.New(measurementIsNotStringErrorMsg) | ||
} | ||
measurement = measurementFieldVal | ||
measurementCount++ | ||
continue | ||
} | ||
|
||
if strings.Contains(structFieldName, "tag") { | ||
stringTagVal, ok := structFieldVal.Interface().(string) | ||
if !ok { | ||
return nil, errors.New(tagValueNotStringErrorMsg) | ||
} | ||
tags[structFieldName] = stringTagVal | ||
continue | ||
} | ||
|
||
parsedFieldVal := fieldTypeHandler(structFieldVal) | ||
fields[structFieldName] = parsedFieldVal | ||
} | ||
|
||
if measurementCount == 0 { | ||
return nil, errors.New(noMeasurementPresentErrorMsg) | ||
} | ||
|
||
if measurementCount > 1 { | ||
return nil, errors.New(tooManyMeasurementsErrorMsg) | ||
} | ||
|
||
return write.NewPoint(measurement, tags, fields, ts), nil | ||
} | ||
|
||
func fieldTypeHandler(fieldVal interface{}) interface{} { | ||
spaces := regexp.MustCompile(`\s+`) | ||
|
||
switch fieldValType := fieldVal.(type) { | ||
case string: | ||
lowerVal := strings.ToLower(fieldValType) | ||
influxStringVal := spaces.ReplaceAllString(lowerVal, "_") | ||
return influxStringVal | ||
|
||
case time.Time: | ||
return fieldValType.Unix() | ||
|
||
default: | ||
return fieldVal | ||
} | ||
} | ||
|
||
func checkEitherTagOrMeasurement(influxTag string) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a nit: this fn performs a slightly different thing to what the name suggests ... it should be better named |
||
tags := strings.Split(influxTag, ",") | ||
|
||
if len(tags) > 2 { | ||
return errors.New(tooManyTagArgs) | ||
} | ||
|
||
if len(tags) == 2 && !strings.Contains(tags[1], "tag") { | ||
return errors.New(secondTagArgPassedButNotTagErrorMsg) | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package api | ||
|
||
import ( | ||
"github.com/stretchr/testify/assert" | ||
"testing" | ||
) | ||
|
||
type goodInfluxTestType struct { | ||
Measurement string `influxdb:"measurement"` | ||
Name string `influxdb:"name"` | ||
Title string `influxdb:"title,tag"` | ||
Distance int64 `influxdb:"distance"` | ||
Description string `influxdb:"Description"` | ||
} | ||
|
||
type badInfluxTestType2ndArgNotTag struct { | ||
Measurement string `influxdb:"name,measurement"` | ||
Name string `influxdb:"name"` | ||
Title string `influxdb:"title,tag"` | ||
Distance int64 `influxdb:"distance"` | ||
Description string `influxdb:"Description"` | ||
} | ||
|
||
type badInfluxTestTypeTooManyMeasurements struct { | ||
Measurement string `influxdb:"measurement"` | ||
Name string `influxdb:"name"` | ||
Title string `influxdb:"title,tag"` | ||
Distance int64 `influxdb:"distance"` | ||
Description string `influxdb:"measurement"` | ||
} | ||
|
||
type badInfluxTestTypeNoMeasurements struct { | ||
Measurement string `influxdb:"none"` | ||
Name string `influxdb:"name"` | ||
Title string `influxdb:"title,tag"` | ||
Distance int64 `influxdb:"distance"` | ||
Description string `influxdb:"Description"` | ||
} | ||
|
||
var ( | ||
goodInfluxArg = goodInfluxTestType{ | ||
Measurement: "foo", | ||
Name: "bar", | ||
Title: "test of the struct write point marshaller", | ||
Distance: 39, | ||
Description: "This tests the MarshalStructToWritePoint", | ||
} | ||
|
||
badInfluxArg2ndArgNotTag = badInfluxTestType2ndArgNotTag{ | ||
Measurement: "foo", | ||
Name: "bar", | ||
Title: "test of the struct write point marshaller", | ||
Distance: 39, | ||
Description: "This tests the MarshalStructToWritePoint", | ||
} | ||
|
||
badInfluxArgTooManyMeasurements = badInfluxTestTypeTooManyMeasurements{ | ||
Measurement: "foo", | ||
Name: "bar", | ||
Title: "test of the struct write point marshaller", | ||
Distance: 39, | ||
Description: "This tests the MarshalStructToWritePoint", | ||
} | ||
|
||
badInfluxArgNoMeasurements = badInfluxTestTypeNoMeasurements{ | ||
Measurement: "foo", | ||
Name: "bar", | ||
Title: "test of the struct write point marshaller", | ||
Distance: 39, | ||
Description: "This tests the MarshalStructToWritePoint", | ||
} | ||
) | ||
|
||
func Test_MarshalStructToWritePoint_Happy_Path(t *testing.T) { | ||
point, err := MarshalStructToWritePoint(goodInfluxArg, nil) | ||
assert.NoError(t, err) | ||
assert.NotNil(t, point) | ||
|
||
assert.Equal(t, 1, len(point.TagList())) | ||
assert.Equal(t, 3, len(point.FieldList())) | ||
assert.Equal(t, "foo", point.Name()) | ||
} | ||
|
||
func Test_MarshalStructToWritePoint_Sad_Path_2nd_Arg_Not_Taf(t *testing.T) { | ||
_, err := MarshalStructToWritePoint(badInfluxArg2ndArgNotTag, nil) | ||
assert.Error(t, err) | ||
assert.Equal(t, secondTagArgPassedButNotTagErrorMsg, err.Error()) | ||
} | ||
|
||
func Test_MarshalStructToWritePoint_Sad_Path_Too_Many_Measurements(t *testing.T) { | ||
_, err := MarshalStructToWritePoint(badInfluxArgTooManyMeasurements, nil) | ||
assert.Error(t, err) | ||
assert.Equal(t, tooManyMeasurementsErrorMsg, err.Error()) | ||
} | ||
|
||
func Test_MarshalStructToWritePoint_Sad_Path_No_Measurements(t *testing.T) { | ||
_, err := MarshalStructToWritePoint(badInfluxArgNoMeasurements, nil) | ||
assert.Error(t, err) | ||
assert.Equal(t, noMeasurementPresentErrorMsg, err.Error()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pls use "github.com/influxdata/influxdb-client-go/v2/internal/log" as the rest of the client code does.