Skip to content
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

Explore: Add transformations to correlation data links #61799

Merged
merged 36 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4b7f38d
bring in source from database
gelicia Jan 14, 2023
54fa1e1
bring in transformations from database
gelicia Jan 17, 2023
35f2c6a
add regex transformations to scopevar
gelicia Jan 19, 2023
80369bd
Consolidate types, add better example, cleanup
gelicia Jan 19, 2023
2ec4f1f
Add var only if match
gelicia Jan 19, 2023
d76656c
Change ScopedVar to not require text, do not leak transformation-made…
gelicia Jan 19, 2023
2aef4d9
Add mappings and start implementing logfmt
gelicia Jan 19, 2023
daa9370
Add mappings and start implementing logfmt
gelicia Jan 19, 2023
4250d75
Remove mappings, turn off global regex
gelicia Jan 25, 2023
9e979f7
Add example yaml and omit transformations if empty
gelicia Jan 25, 2023
c838601
Fix the yaml
gelicia Jan 25, 2023
b1cf69b
Add logfmt transformation
gelicia Jan 27, 2023
3d1e571
Merge branch 'main' of https://github.com/grafana/grafana into kristi…
gelicia Jan 31, 2023
527901c
Cleanup transformations and yaml
gelicia Feb 1, 2023
db3a85e
add transformation field to FE types and use it, safeStringify logfmt…
gelicia Feb 1, 2023
003c5d4
Add tests, only safe stringify if non-string, fix bug with safe strin…
gelicia Feb 1, 2023
409901c
Add test for transformation field
gelicia Feb 1, 2023
ed3f375
Merge branch 'main' of https://github.com/grafana/grafana into kristi…
gelicia Feb 1, 2023
0cccdd1
Do not add null transformations object
gelicia Feb 1, 2023
776a2b5
Merge branch 'main' of https://github.com/grafana/grafana into kristi…
gelicia Feb 2, 2023
e68003c
Merge branch 'main' of https://github.com/grafana/grafana into kristi…
gelicia Feb 6, 2023
cb79371
Break out transformation logic, add tests to backend code
gelicia Feb 6, 2023
d9dd95b
Fix lint errors I understand 😅
gelicia Feb 7, 2023
b299c1d
Fix the backend lint error
gelicia Feb 7, 2023
2cab225
Remove unnecessary code and mark new Transformations object as internal
gelicia Feb 7, 2023
220a9ee
Add support for named capture groups
gelicia Feb 8, 2023
091149e
Remove type assertion
gelicia Feb 9, 2023
23c77fe
Merge branch 'main' of https://github.com/grafana/grafana into kristi…
gelicia Feb 9, 2023
df36e99
Remove variable name from transformation
gelicia Feb 9, 2023
321d489
Add test for overriding regexes
gelicia Feb 9, 2023
e695298
Add back variable name field, but change to mapValue
gelicia Feb 9, 2023
8abfa8a
fix go api test
gelicia Feb 9, 2023
c743f42
Change transformation types to enum, add better provisioning checks f…
gelicia Feb 10, 2023
ec1e426
Merge branch 'main' of https://github.com/grafana/grafana into kristi…
gelicia Feb 15, 2023
023fd66
Merge branch 'main' of https://github.com/grafana/grafana into kristi…
gelicia Feb 21, 2023
2a08a5a
Check for expression with regex transformations
gelicia Feb 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .betterer.results
Original file line number Diff line number Diff line change
Expand Up @@ -3574,9 +3574,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
],
"public/app/features/explore/utils/links.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/expressions/ExpressionDatasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"@types/js-yaml": "^4.0.5",
"@types/jsurl": "^1.2.28",
"@types/lodash": "4.14.187",
"@types/logfmt": "^1.2.1",
"@types/logfmt": "^1.2.3",
"@types/mousetrap": "1.6.10",
"@types/node": "18.14.0",
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.0.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/grafana-data/src/types/ScopedVars.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface ScopedVar<T = any> {
text: any;
text?: any;
gelicia marked this conversation as resolved.
Show resolved Hide resolved
value: T;
[key: string]: any;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/grafana-data/src/types/dataLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,27 @@ export interface DataLink<T extends DataQuery = any> {
internal?: InternalDataLink<T>;
}

/** @internal */
export enum SupportedTransformationTypes {
Regex = 'regex',
Logfmt = 'logfmt',
}

/** @internal */
export interface DataLinkTransformationConfig {
type: SupportedTransformationTypes;
field?: string;
expression?: string;
mapValue?: string;
}

/** @internal */
export interface InternalDataLink<T extends DataQuery = any> {
query: T;
datasourceUid: string;
datasourceName: string; // used as a title if `DataLink.title` is empty
panelsState?: ExplorePanelsState;
transformations?: DataLinkTransformationConfig[];
}

export type LinkTarget = '_blank' | '_self' | undefined;
Expand Down
3 changes: 3 additions & 0 deletions pkg/services/correlations/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ func (s CorrelationsService) updateCorrelation(ctx context.Context, cmd UpdateCo
if cmd.Config.Target != nil {
correlation.Config.Target = *cmd.Config.Target
}
if cmd.Config.Transformations != nil {
correlation.Config.Transformations = cmd.Config.Transformations
}
}

updateCount, err := session.Where("uid = ? AND source_uid = ?", correlation.UID, correlation.SourceUID).Limit(1).Update(correlation)
Expand Down
50 changes: 44 additions & 6 deletions pkg/services/correlations/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,21 @@ var (
ErrCorrelationNotFound = errors.New("correlation not found")
ErrUpdateCorrelationEmptyParams = errors.New("not enough parameters to edit correlation")
ErrInvalidConfigType = errors.New("invalid correlation config type")
ErrInvalidTransformationType = errors.New("invalid transformation type")
ErrTransformationNotNested = errors.New("transformations must be nested under config")
ErrTransformationRegexReqExp = errors.New("regex transformations require expression")
)

type CorrelationConfigType string

type Transformation struct {
//Enum: regex,logfmt
Type string `json:"type"`
Expression string `json:"expression,omitempty"`
Field string `json:"field,omitempty"`
MapValue string `json:"mapValue,omitempty"`
}

const (
ConfigTypeQuery CorrelationConfigType = "query"
)
Expand All @@ -29,6 +40,19 @@ func (t CorrelationConfigType) Validate() error {
return nil
}

func (t Transformations) Validate() error {
for _, v := range t {
if v.Type != "regex" && v.Type != "logfmt" {
return fmt.Errorf("%s: \"%s\"", ErrInvalidTransformationType, t)
} else if v.Type == "regex" && len(v.Expression) == 0 {
return fmt.Errorf("%s: \"%s\"", ErrTransformationRegexReqExp, t)
}
}
return nil
}

type Transformations []Transformation

// swagger:model
type CorrelationConfig struct {
// Field used to attach the correlation link
Expand All @@ -42,21 +66,28 @@ type CorrelationConfig struct {
// required:true
// example: { "expr": "job=app" }
Target map[string]interface{} `json:"target" binding:"Required"`
// Source data transformations
// required:false
// example: [{"type": "logfmt"}]
Transformations Transformations `json:"transformations,omitempty"`
}

func (c CorrelationConfig) MarshalJSON() ([]byte, error) {
target := c.Target
transformations := c.Transformations
if target == nil {
target = map[string]interface{}{}
}
return json.Marshal(struct {
Type CorrelationConfigType `json:"type"`
Field string `json:"field"`
Target map[string]interface{} `json:"target"`
Type CorrelationConfigType `json:"type"`
Field string `json:"field"`
Target map[string]interface{} `json:"target"`
Transformations Transformations `json:"transformations,omitempty"`
}{
Type: ConfigTypeQuery,
Field: c.Field,
Target: target,
Type: ConfigTypeQuery,
Field: c.Field,
Target: target,
Transformations: transformations,
})
}

Expand Down Expand Up @@ -117,6 +148,10 @@ func (c CreateCorrelationCommand) Validate() error {
if c.TargetUID == nil && c.Config.Type == ConfigTypeQuery {
return fmt.Errorf("correlations of type \"%s\" must have a targetUID", ConfigTypeQuery)
}

if err := c.Config.Transformations.Validate(); err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -151,6 +186,9 @@ type CorrelationConfigUpdateDTO struct {
// Target data query
// example: { "expr": "job=app" }
Target *map[string]interface{} `json:"target"`
// Source data transformations
// example: [{"type": "logfmt"},{"type":"regex","expression":"(Superman|Batman)", "variable":"name"}]
Transformations []Transformation `json:"transformations"`
}

func (c CorrelationConfigUpdateDTO) Validate() error {
Expand Down
4 changes: 4 additions & 0 deletions pkg/services/provisioning/datasources/datasources.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ func makeCreateCorrelationCommand(correlation map[string]interface{}, SourceUID
createCommand.TargetUID = &targetUID
}

if correlation["transformations"] != nil {
return correlations.CreateCorrelationCommand{}, correlations.ErrTransformationNotNested
}

if correlation["config"] != nil {
jsonbody, err := json.Marshal(correlation["config"])
if err != nil {
Expand Down
10 changes: 9 additions & 1 deletion pkg/tests/api/correlations/correlations_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ func TestIntegrationCreateCorrelation(t *testing.T) {
label := "a label"
fieldName := "fieldName"
configType := correlations.ConfigTypeQuery
transformation := correlations.Transformation{Type: "logfmt"}
transformation2 := correlations.Transformation{Type: "regex", Expression: "testExpression", MapValue: "testVar"}
res := ctx.Post(PostParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations", writableDs),
body: fmt.Sprintf(`{
Expand All @@ -246,7 +248,11 @@ func TestIntegrationCreateCorrelation(t *testing.T) {
"config": {
"type": "%s",
"field": "%s",
"target": { "expr": "foo" }
"target": { "expr": "foo" },
"transformations": [
{"type": "logfmt"},
{"type": "regex", "expression": "testExpression", "mapValue": "testVar"}
]
}
}`, writableDs, description, label, configType, fieldName),
user: adminUser,
Expand All @@ -268,6 +274,8 @@ func TestIntegrationCreateCorrelation(t *testing.T) {
require.Equal(t, configType, response.Result.Config.Type)
require.Equal(t, fieldName, response.Result.Config.Field)
require.Equal(t, map[string]interface{}{"expr": "foo"}, response.Result.Config.Target)
require.Equal(t, transformation, response.Result.Config.Transformations[0])
require.Equal(t, transformation2, response.Result.Config.Transformations[1])

require.NoError(t, res.Body.Close())
})
Expand Down
3 changes: 3 additions & 0 deletions pkg/tests/api/correlations/correlations_read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ func TestIntegrationReadCorrelation(t *testing.T) {
Type: correlations.ConfigTypeQuery,
Field: "foo",
Target: map[string]interface{}{},
Transformations: []correlations.Transformation{
{Type: "logfmt"},
},
},
})

Expand Down
4 changes: 3 additions & 1 deletion pkg/tests/api/correlations/correlations_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
"config": {
"field": "field",
"type": "query",
"target": { "expr": "bar" }
"target": { "expr": "bar" },
"transformations": [ {"type": "logfmt"} ]
}
}`,
})
Expand All @@ -305,6 +306,7 @@ func TestIntegrationUpdateCorrelation(t *testing.T) {
require.Equal(t, "1", response.Result.Description)
require.Equal(t, "field", response.Result.Config.Field)
require.Equal(t, map[string]interface{}{"expr": "bar"}, response.Result.Config.Target)
require.Equal(t, correlations.Transformation{Type: "logfmt"}, response.Result.Config.Transformations[0])
require.NoError(t, res.Body.Close())

// partially updating only label
Expand Down
2 changes: 1 addition & 1 deletion public/app/core/utils/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export const safeParseJson = (text?: string): any | undefined => {
};

export const safeStringifyValue = (value: any, space?: number) => {
if (!value) {
if (value === undefined || value === null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this slightly changes the behaviour of a bunch of things, like the panel model inspector in dashboards and variables, did we test it's safe? (especially for variables as false and 0 would have resulted in an empty string, but "false" and "0" now, not sure if that's even possible tho)

Copy link
Contributor Author

@gelicia gelicia Feb 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just couldn't imagine why we would want true to evaluate to "true" and false to evaluate to an empty string. Let's look at the usages of this function outside how it is used in this PR

  • features/dashboard/state/PanelModel.ts - passes the entire model into this function, and that model is a required parameter. I am extremely confident that PanelModel will never evaluate to false or 0, there are a lot of required fields in it.
  • features/variables/utils.ts - safe-stringifies the first arg, concatenates all args together split by a space except the last one and checks that string has matches for any of the three formats for variables. I do not believe that having false or 0 evaluate to an empty string or not will have any impact on this, because the safe-stringify only runs on the first argument, and in all cases a variable reference must start with either $ or [ which is not possible with a false or 0 scenario.
  • features/variables/inspect/utils.ts - Similar to the above, we safe-stringify a value and check if it matches any of the variable patterns. False or 0 or empty string will all not match. That value is not used again - it will use the matching groups instead.
  • plugins/datasource/prometheus/datasource.tsx - This will only run if the if statement depending on the same value is true, so if the value passed in is 0 or false, it will never run in the first place

I'm very confident based on those usages that this logic will not impact anything.

return '';
}

Expand Down
34 changes: 34 additions & 0 deletions public/app/features/correlations/transformations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logfmt from 'logfmt';

import { ScopedVars, DataLinkTransformationConfig, SupportedTransformationTypes } from '@grafana/data';
import { safeStringifyValue } from 'app/core/utils/explore';

export const getTransformationVars = (
transformation: DataLinkTransformationConfig,
fieldValue: string,
fieldName: string
): ScopedVars => {
let transformationScopedVars: ScopedVars = {};
let transformVal: { [key: string]: string | boolean | null | undefined } = {};
if (transformation.type === SupportedTransformationTypes.Regex && transformation.expression) {
const regexp = new RegExp(transformation.expression, 'gi');
const matches = fieldValue.matchAll(regexp);
for (const match of matches) {
if (match.groups) {
transformVal = match.groups;
} else {
transformVal[transformation.mapValue || fieldName] = match[1] || match[0];
}
}
} else if (transformation.type === SupportedTransformationTypes.Logfmt) {
transformVal = logfmt.parse(fieldValue);
}

Object.keys(transformVal).forEach((key) => {
const transformValString =
typeof transformVal[key] === 'string' ? transformVal[key] : safeStringifyValue(transformVal[key]);
transformationScopedVars[key] = { value: transformValString };
});

return transformationScopedVars;
};
4 changes: 4 additions & 0 deletions public/app/features/correlations/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { DataLinkTransformationConfig } from '@grafana/data';

export interface AddCorrelationResponse {
correlation: Correlation;
}

export type GetCorrelationsResponse = Correlation[];

type CorrelationConfigType = 'query';

export interface CorrelationConfig {
field: string;
target: object;
type: CorrelationConfigType;
transformations?: DataLinkTransformationConfig[];
}

export interface Correlation {
Expand Down
1 change: 1 addition & 0 deletions public/app/features/correlations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const decorateDataFrameWithInternalDataLinks = (dataFrame: DataFrame, correlatio
query: correlation.config?.target,
datasourceUid: correlation.target.uid,
datasourceName: correlation.target.name,
transformations: correlation.config?.transformations,
},
url: '',
title: correlation.label || correlation.target.name,
Expand Down
Loading