Skip to content

Commit 1f88246

Browse files
bacherflevan-bradleyTylerHelmuth
authored
[pkg/ottl] Add support for scaling values (#33246)
**Description:** Adds a `Scale` function to the OTTL package. This function can be applied to `int`/`double` values, as well as metrics of the following types: - Sum - Gauge - Histogram **Link to tracking Issue:** #16214 **Testing:** Added Unit and E2E tests in the OTTL package. Tested manually in a sample environment with the following example configuration: ``` receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 processors: transform: error_mode: ignore metric_statements: - context: metric statements: - set(data_points, Scale(data_points, 10.0)) exporters: debug: verbosity: detailed otlphttp: endpoint: "######" headers: Authorization: "#####" service: pipelines: metrics: receivers: [otlp] exporters: [otlphttp, debug] processors: [transform] ``` **Documentation:** Added documentation in the `README` describing all functions in the `ottl` package --------- Signed-off-by: Florian Bacher <[email protected]> Co-authored-by: Evan Bradley <[email protected]> Co-authored-by: Tyler Helmuth <[email protected]>
1 parent 3cb736b commit 1f88246

File tree

7 files changed

+394
-0
lines changed

7 files changed

+394
-0
lines changed

.chloggen/add-scale-function.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: processor/transform
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add `scale_metric` function that scales all data points in a metric.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [16214]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

processor/transformprocessor/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ In addition to OTTL functions, the processor defines its own functions to help w
212212
- [convert_summary_count_val_to_sum](#convert_summary_count_val_to_sum)
213213
- [convert_summary_sum_val_to_sum](#convert_summary_sum_val_to_sum)
214214
- [copy_metric](#copy_metric)
215+
- [scale_metric](#scale_metric)
215216

216217
### convert_sum_to_gauge
217218

@@ -347,6 +348,21 @@ Examples:
347348

348349
- `copy_metric(desc="new desc") where description == "old desc"`
349350

351+
### scale_metric
352+
353+
`scale_metric(factor, Optional[unit])`
354+
355+
The `scale_metric` function multiplies the values in the data points in the metric by the float value `factor`.
356+
If the optional string `unit` is provided, the metric's unit will be set to this value.
357+
The supported data types are:
358+
359+
Supported metric types are `Gauge`, `Sum`, `Histogram`, and `Summary`.
360+
361+
Examples:
362+
363+
- `scale_metric(0.1)`: Scale the metric by a factor of `0.1`. The unit of the metric will not be modified.
364+
- `scale_metric(10.0, "kWh")`: Scale the metric by a factor of `10.0` and sets the unit to `kWh`.
365+
350366
## Examples
351367

352368
### Perform transformation if field does not exist
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package metrics // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/transformprocessor/internal/metrics"
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
11+
"go.opentelemetry.io/collector/pdata/pmetric"
12+
13+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
14+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlmetric"
15+
)
16+
17+
type ScaleArguments struct {
18+
Multiplier float64
19+
Unit ottl.Optional[ottl.StringGetter[ottlmetric.TransformContext]]
20+
}
21+
22+
func newScaleMetricFactory() ottl.Factory[ottlmetric.TransformContext] {
23+
return ottl.NewFactory("scale_metric", &ScaleArguments{}, createScaleFunction)
24+
}
25+
26+
func createScaleFunction(_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[ottlmetric.TransformContext], error) {
27+
args, ok := oArgs.(*ScaleArguments)
28+
29+
if !ok {
30+
return nil, fmt.Errorf("ScaleFactory args must be of type *ScaleArguments[K]")
31+
}
32+
33+
return Scale(*args)
34+
}
35+
36+
func Scale(args ScaleArguments) (ottl.ExprFunc[ottlmetric.TransformContext], error) {
37+
return func(ctx context.Context, tCtx ottlmetric.TransformContext) (any, error) {
38+
metric := tCtx.GetMetric()
39+
40+
var unit *string
41+
if !args.Unit.IsEmpty() {
42+
u, err := args.Unit.Get().Get(ctx, tCtx)
43+
if err != nil {
44+
return nil, fmt.Errorf("could not get unit from ScaleArguments: %w", err)
45+
}
46+
unit = &u
47+
}
48+
49+
switch metric.Type() {
50+
case pmetric.MetricTypeGauge:
51+
scaleMetric(metric.Gauge().DataPoints(), args.Multiplier)
52+
case pmetric.MetricTypeHistogram:
53+
scaleHistogram(metric.Histogram().DataPoints(), args.Multiplier)
54+
case pmetric.MetricTypeSummary:
55+
scaleSummarySlice(metric.Summary().DataPoints(), args.Multiplier)
56+
case pmetric.MetricTypeSum:
57+
scaleMetric(metric.Sum().DataPoints(), args.Multiplier)
58+
case pmetric.MetricTypeExponentialHistogram:
59+
return nil, errors.New("exponential histograms are not supported by the 'scale_metric' function")
60+
default:
61+
return nil, fmt.Errorf("unsupported metric type: '%v'", metric.Type())
62+
}
63+
if unit != nil {
64+
metric.SetUnit(*unit)
65+
}
66+
67+
return nil, nil
68+
}, nil
69+
}
70+
71+
func scaleExemplar(ex *pmetric.Exemplar, multiplier float64) {
72+
switch ex.ValueType() {
73+
case pmetric.ExemplarValueTypeInt:
74+
ex.SetIntValue(int64(float64(ex.IntValue()) * multiplier))
75+
case pmetric.ExemplarValueTypeDouble:
76+
ex.SetDoubleValue(ex.DoubleValue() * multiplier)
77+
}
78+
}
79+
80+
func scaleSummarySlice(values pmetric.SummaryDataPointSlice, multiplier float64) {
81+
for i := 0; i < values.Len(); i++ {
82+
dp := values.At(i)
83+
84+
dp.SetSum(dp.Sum() * multiplier)
85+
86+
for i := 0; i < dp.QuantileValues().Len(); i++ {
87+
qv := dp.QuantileValues().At(i)
88+
qv.SetValue(qv.Value() * multiplier)
89+
}
90+
}
91+
}
92+
93+
func scaleHistogram(datapoints pmetric.HistogramDataPointSlice, multiplier float64) {
94+
for i := 0; i < datapoints.Len(); i++ {
95+
dp := datapoints.At(i)
96+
97+
if dp.HasSum() {
98+
dp.SetSum(dp.Sum() * multiplier)
99+
}
100+
if dp.HasMin() {
101+
dp.SetMin(dp.Min() * multiplier)
102+
}
103+
if dp.HasMax() {
104+
dp.SetMax(dp.Max() * multiplier)
105+
}
106+
107+
for bounds, bi := dp.ExplicitBounds(), 0; bi < bounds.Len(); bi++ {
108+
bounds.SetAt(bi, bounds.At(bi)*multiplier)
109+
}
110+
111+
for exemplars, ei := dp.Exemplars(), 0; ei < exemplars.Len(); ei++ {
112+
exemplar := exemplars.At(ei)
113+
scaleExemplar(&exemplar, multiplier)
114+
}
115+
}
116+
}
117+
118+
func scaleMetric(points pmetric.NumberDataPointSlice, multiplier float64) {
119+
for i := 0; i < points.Len(); i++ {
120+
dp := points.At(i)
121+
switch dp.ValueType() {
122+
case pmetric.NumberDataPointValueTypeInt:
123+
dp.SetIntValue(int64(float64(dp.IntValue()) * multiplier))
124+
125+
case pmetric.NumberDataPointValueTypeDouble:
126+
dp.SetDoubleValue(dp.DoubleValue() * multiplier)
127+
default:
128+
}
129+
}
130+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package metrics
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"go.opentelemetry.io/collector/pdata/pcommon"
12+
"go.opentelemetry.io/collector/pdata/pmetric"
13+
14+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
15+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlmetric"
16+
)
17+
18+
func TestScale(t *testing.T) {
19+
type testCase struct {
20+
name string
21+
args ScaleArguments
22+
valueFunc func() pmetric.Metric
23+
wantFunc func() pmetric.Metric
24+
wantErr bool
25+
}
26+
tests := []testCase{
27+
{
28+
name: "scale gauge float metric",
29+
valueFunc: func() pmetric.Metric {
30+
metric := pmetric.NewMetric()
31+
metric.SetName("test-metric")
32+
metric.SetEmptyGauge()
33+
metric.Gauge().DataPoints().AppendEmpty().SetDoubleValue(10.0)
34+
35+
return metric
36+
},
37+
args: ScaleArguments{
38+
Multiplier: 10.0,
39+
Unit: ottl.NewTestingOptional[ottl.StringGetter[ottlmetric.TransformContext]](ottl.StandardStringGetter[ottlmetric.TransformContext]{
40+
Getter: func(_ context.Context, _ ottlmetric.TransformContext) (any, error) {
41+
return "kWh", nil
42+
},
43+
}),
44+
},
45+
wantFunc: func() pmetric.Metric {
46+
metric := pmetric.NewMetric()
47+
metric.SetName("test-metric")
48+
metric.SetEmptyGauge()
49+
metric.SetUnit("kWh")
50+
metric.Gauge().DataPoints().AppendEmpty().SetDoubleValue(100.0)
51+
52+
return metric
53+
},
54+
wantErr: false,
55+
},
56+
{
57+
name: "scale gauge int metric",
58+
valueFunc: func() pmetric.Metric {
59+
metric := pmetric.NewMetric()
60+
metric.SetName("test-metric")
61+
metric.SetEmptyGauge()
62+
metric.Gauge().DataPoints().AppendEmpty().SetIntValue(10)
63+
64+
return metric
65+
},
66+
args: ScaleArguments{
67+
Multiplier: 10.0,
68+
},
69+
wantFunc: func() pmetric.Metric {
70+
metric := pmetric.NewMetric()
71+
metric.SetName("test-metric")
72+
metric.SetEmptyGauge()
73+
metric.Gauge().DataPoints().AppendEmpty().SetIntValue(100.0)
74+
75+
return metric
76+
},
77+
wantErr: false,
78+
},
79+
{
80+
name: "scale sum metric",
81+
valueFunc: func() pmetric.Metric {
82+
metric := pmetric.NewMetric()
83+
metric.SetName("test-metric")
84+
metric.SetEmptySum()
85+
metric.Sum().DataPoints().AppendEmpty().SetDoubleValue(10.0)
86+
87+
return metric
88+
},
89+
args: ScaleArguments{
90+
Multiplier: 10.0,
91+
},
92+
wantFunc: func() pmetric.Metric {
93+
metric := pmetric.NewMetric()
94+
metric.SetName("test-metric")
95+
metric.SetEmptySum()
96+
metric.Sum().DataPoints().AppendEmpty().SetDoubleValue(100.0)
97+
98+
return metric
99+
},
100+
wantErr: false,
101+
},
102+
{
103+
name: "scale histogram metric",
104+
valueFunc: func() pmetric.Metric {
105+
metric := getTestScalingHistogramMetric(1, 4, 1, 3, []float64{1, 10}, []uint64{1, 2}, []float64{1.0}, 1, 1)
106+
return metric
107+
},
108+
args: ScaleArguments{
109+
Multiplier: 10.0,
110+
},
111+
wantFunc: func() pmetric.Metric {
112+
metric := getTestScalingHistogramMetric(1, 40, 10, 30, []float64{10, 100}, []uint64{1, 2}, []float64{10.0}, 1, 1)
113+
return metric
114+
},
115+
wantErr: false,
116+
},
117+
{
118+
name: "scale summary metric",
119+
valueFunc: func() pmetric.Metric {
120+
metric := pmetric.NewMetric()
121+
dp := metric.SetEmptySummary().DataPoints().AppendEmpty()
122+
dp.SetSum(10.0)
123+
qv := dp.QuantileValues().AppendEmpty()
124+
qv.SetValue(10.0)
125+
126+
return metric
127+
},
128+
args: ScaleArguments{
129+
Multiplier: 10.0,
130+
},
131+
wantFunc: func() pmetric.Metric {
132+
metric := pmetric.NewMetric()
133+
dp := metric.SetEmptySummary().DataPoints().AppendEmpty()
134+
dp.SetSum(100.0)
135+
qv := dp.QuantileValues().AppendEmpty()
136+
qv.SetValue(100.0)
137+
138+
return metric
139+
},
140+
wantErr: false,
141+
},
142+
{
143+
name: "unsupported: exponential histogram metric",
144+
valueFunc: func() pmetric.Metric {
145+
metric := pmetric.NewMetric()
146+
metric.SetEmptyExponentialHistogram()
147+
return metric
148+
},
149+
args: ScaleArguments{
150+
Multiplier: 10.0,
151+
},
152+
wantFunc: func() pmetric.Metric {
153+
// value should not be modified
154+
metric := pmetric.NewMetric()
155+
metric.SetEmptyExponentialHistogram()
156+
return metric
157+
},
158+
wantErr: true,
159+
},
160+
}
161+
for _, tt := range tests {
162+
t.Run(tt.name, func(t *testing.T) {
163+
target := ottlmetric.NewTransformContext(
164+
tt.valueFunc(),
165+
pmetric.NewMetricSlice(),
166+
pcommon.NewInstrumentationScope(),
167+
pcommon.NewResource(),
168+
pmetric.NewScopeMetrics(),
169+
pmetric.NewResourceMetrics(),
170+
)
171+
172+
expressionFunc, _ := Scale(tt.args)
173+
_, err := expressionFunc(context.Background(), target)
174+
175+
if tt.wantErr {
176+
assert.Error(t, err)
177+
} else {
178+
assert.NoError(t, err)
179+
}
180+
assert.EqualValues(t, tt.wantFunc(), target.GetMetric())
181+
})
182+
}
183+
}
184+
185+
func getTestScalingHistogramMetric(count uint64, sum, min, max float64, bounds []float64, bucketCounts []uint64, exemplars []float64, start, timestamp pcommon.Timestamp) pmetric.Metric {
186+
metric := pmetric.NewMetric()
187+
metric.SetName("test-metric")
188+
metric.SetEmptyHistogram()
189+
histogramDatapoint := metric.Histogram().DataPoints().AppendEmpty()
190+
histogramDatapoint.SetCount(count)
191+
histogramDatapoint.SetSum(sum)
192+
histogramDatapoint.SetMin(min)
193+
histogramDatapoint.SetMax(max)
194+
histogramDatapoint.ExplicitBounds().FromRaw(bounds)
195+
histogramDatapoint.BucketCounts().FromRaw(bucketCounts)
196+
for i := 0; i < len(exemplars); i++ {
197+
exemplar := histogramDatapoint.Exemplars().AppendEmpty()
198+
exemplar.SetTimestamp(1)
199+
exemplar.SetDoubleValue(exemplars[i])
200+
}
201+
histogramDatapoint.SetStartTimestamp(start)
202+
histogramDatapoint.SetTimestamp(timestamp)
203+
return metric
204+
}

0 commit comments

Comments
 (0)