Skip to content

Commit 0744428

Browse files
authored
Add support for histograms to metrics intake (#5360)
* model/modeldecoder: add metric type and unit * systemtest: test histogram metrics * Update changelog * systemtest: fix min docs expectation in test
1 parent 16618f2 commit 0744428

File tree

21 files changed

+634
-153
lines changed

21 files changed

+634
-153
lines changed

beater/test_approved_es_documents/TestPublishIntegrationMetricsets.approved.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,72 @@
298298
"id": "axb123hg",
299299
"name": "logged-in-user"
300300
}
301+
},
302+
{
303+
"@timestamp": "2017-05-30T18:53:41.366Z",
304+
"_doc_count": 6,
305+
"_metric_descriptions": {
306+
"latency_distribution": {
307+
"type": "histogram",
308+
"unit": "s"
309+
}
310+
},
311+
"agent": {
312+
"name": "elastic-node",
313+
"version": "3.14.0"
314+
},
315+
"ecs": {
316+
"version": "1.8.0"
317+
},
318+
"host": {
319+
"ip": "127.0.0.1"
320+
},
321+
"labels": {
322+
"tag1": "one",
323+
"tag2": 2
324+
},
325+
"latency_distribution": {
326+
"counts": [
327+
1,
328+
2,
329+
3
330+
],
331+
"values": [
332+
1.1,
333+
2.2,
334+
3.3
335+
]
336+
},
337+
"metricset.name": "app",
338+
"observer": {
339+
"ephemeral_id": "00000000-0000-0000-0000-000000000000",
340+
"hostname": "",
341+
"id": "fbba762a-14dd-412c-b7e9-b79f903eb492",
342+
"type": "test-apm-server",
343+
"version": "1.2.3",
344+
"version_major": 1
345+
},
346+
"process": {
347+
"pid": 1234
348+
},
349+
"processor": {
350+
"event": "metric",
351+
"name": "metric"
352+
},
353+
"service": {
354+
"language": {
355+
"name": "ecmascript"
356+
},
357+
"name": "1234_service-12a3",
358+
"node": {
359+
"name": "node-1"
360+
}
361+
},
362+
"user": {
363+
"email": "user@mail.com",
364+
"id": "axb123hg",
365+
"name": "logged-in-user"
366+
}
301367
}
302368
]
303369
}

changelogs/head.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ https://github.com/elastic/apm-server/compare/7.13\...master[View commits]
2626
* Translate otel messaging.* semantic conventions to ECS {pull}5334[5334]
2727
* Add support for dynamic histogram metrics {pull}5239[5239]
2828
* Tail-sampling processor now resumes subscription from previous position after restart {pull}5350[5350]
29+
* Add support for histograms to metrics intake {pull}5360[5360]
2930

3031
[float]
3132
==== Deprecated

docs/spec/v2/metricset.json

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,118 @@
1313
"object"
1414
],
1515
"properties": {
16+
"counts": {
17+
"description": "Counts holds the bucket counts for histogram metrics. These numbers must be positive or zero. If Counts is specified, then Values is expected to be specified with the same number of elements, and with the same order.",
18+
"type": [
19+
"null",
20+
"array"
21+
],
22+
"items": {
23+
"type": "integer",
24+
"minimum": 0
25+
},
26+
"minItems": 0
27+
},
28+
"type": {
29+
"description": "Type holds an optional metric type: gauge, counter, or histogram. If Type is unknown, it will be ignored.",
30+
"type": [
31+
"null",
32+
"string"
33+
]
34+
},
35+
"unit": {
36+
"description": "Unit holds an optional unit for the metric. - \"percent\" (value is in the range [0,1]) - \"byte\" - a time unit: \"nanos\", \"micros\", \"ms\", \"s\", \"m\", \"h\", \"d\" If Unit is unknown, it will be ignored.",
37+
"type": [
38+
"null",
39+
"string"
40+
]
41+
},
1642
"value": {
1743
"description": "Value holds the value of a single metric sample.",
18-
"type": "number"
44+
"type": [
45+
"null",
46+
"number"
47+
]
48+
},
49+
"values": {
50+
"description": "Values holds the bucket values for histogram metrics. Values must be provided in ascending order; failure to do so will result in the metric being discarded.",
51+
"type": [
52+
"null",
53+
"array"
54+
],
55+
"items": {
56+
"type": "number"
57+
},
58+
"minItems": 0
1959
}
2060
},
21-
"required": [
22-
"value"
61+
"allOf": [
62+
{
63+
"if": {
64+
"properties": {
65+
"counts": {
66+
"type": "array"
67+
}
68+
},
69+
"required": [
70+
"counts"
71+
]
72+
},
73+
"then": {
74+
"properties": {
75+
"values": {
76+
"type": "array"
77+
}
78+
},
79+
"required": [
80+
"values"
81+
]
82+
}
83+
},
84+
{
85+
"if": {
86+
"properties": {
87+
"values": {
88+
"type": "array"
89+
}
90+
},
91+
"required": [
92+
"values"
93+
]
94+
},
95+
"then": {
96+
"properties": {
97+
"counts": {
98+
"type": "array"
99+
}
100+
},
101+
"required": [
102+
"counts"
103+
]
104+
}
105+
}
106+
],
107+
"anyOf": [
108+
{
109+
"properties": {
110+
"value": {
111+
"type": "number"
112+
}
113+
},
114+
"required": [
115+
"value"
116+
]
117+
},
118+
{
119+
"properties": {
120+
"values": {
121+
"type": "array"
122+
}
123+
},
124+
"required": [
125+
"values"
126+
]
127+
}
23128
]
24129
}
25130
}

model/modeldecoder/generator/code.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,28 +161,37 @@ func (val *%s) IsSet() bool {
161161
if key != "" {
162162
key += "."
163163
}
164-
prefix := ``
164+
prefix := ` `
165165
for i := 0; i < len(structTyp.fields); i++ {
166166
f := structTyp.fields[i]
167167
if !f.Exported() {
168168
continue
169169
}
170-
switch t := f.Type().Underlying().(type) {
171-
case *types.Slice, *types.Map:
172-
fmt.Fprintf(&g.buf, `%s len(val.%s) > 0`, prefix, f.Name())
173-
case *types.Struct:
174-
fmt.Fprintf(&g.buf, `%s val.%s.IsSet()`, prefix, f.Name())
175-
default:
176-
return fmt.Errorf("unhandled type %T for IsSet() for '%s%s'", t, key, jsonName(f))
170+
g.buf.WriteString(prefix)
171+
if err := generateIsSet(&g.buf, f, "val."); err != nil {
172+
return errors.Wrapf(err, "error generating IsSet() for '%s%s'", key, jsonName(f))
177173
}
178-
prefix = ` ||`
174+
prefix = ` || `
179175
}
180176
fmt.Fprint(&g.buf, `
181177
}
182178
`)
183179
return nil
184180
}
185181

182+
func generateIsSet(w io.Writer, field structField, fieldSelectorPrefix string) error {
183+
switch typ := field.Type().Underlying(); typ.(type) {
184+
case *types.Slice, *types.Map:
185+
fmt.Fprintf(w, "(len(%s%s) > 0)", fieldSelectorPrefix, field.Name())
186+
return nil
187+
case *types.Struct:
188+
fmt.Fprintf(w, "%s%s.IsSet()", fieldSelectorPrefix, field.Name())
189+
return nil
190+
default:
191+
return fmt.Errorf("unhandled type %T generating IsSet() for '%s'", typ, jsonName(field))
192+
}
193+
}
194+
186195
// generateReset creates `Reset` methods for struct fields setting them to
187196
// their zero values or calling their `Reset` methods
188197
// it only considers exported fields

model/modeldecoder/generator/jsonnumber.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,24 @@
1717

1818
package generator
1919

20+
import "encoding/json"
21+
2022
func generateJSONPropertyJSONNumber(info *fieldInfo, parent *property, child *property) error {
2123
child.Type.add(TypeNameNumber)
2224
parent.Properties[jsonSchemaName(info.field)] = child
2325
return setPropertyRulesInteger(info, child)
2426
}
27+
28+
func setPropertyRulesNumber(info *fieldInfo, p *property) error {
29+
for tagName, tagValue := range info.tags {
30+
switch tagName {
31+
case tagMax:
32+
p.Max = json.Number(tagValue)
33+
delete(info.tags, tagName)
34+
case tagMin:
35+
p.Min = json.Number(tagValue)
36+
delete(info.tags, tagName)
37+
}
38+
}
39+
return nil
40+
}

model/modeldecoder/generator/jsonschema.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ var (
215215
"float64": TypeNameNumber,
216216
nullableTypeInt: TypeNameInteger,
217217
"int": TypeNameInteger,
218+
"int64": TypeNameInteger,
218219
nullableTypeTimeMicrosUnix: TypeNameInteger,
219220
nullableTypeString: TypeNameString,
220221
"string": TypeNameString,

model/modeldecoder/generator/nstring.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func generateNullableStringValidation(w io.Writer, fields []structField, f struc
4040
case tagRequired:
4141
ruleNullableRequired(w, f)
4242
case tagRequiredIfAny:
43-
if err = ruleRequiredIfAny(w, fields, f, rule.value); err != nil {
43+
if err := ruleRequiredIfAny(w, fields, f, rule.value); err != nil {
4444
return errors.Wrap(err, "nullableString")
4545
}
4646
default:

model/modeldecoder/generator/slice.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package generator
1919

2020
import (
21+
"encoding/json"
2122
"fmt"
2223
"go/types"
2324
"io"
@@ -45,10 +46,14 @@ for _, elem := range val.%s{
4546
switch rule.name {
4647
case tagMinLength, tagMaxLength:
4748
err = sliceRuleMinMaxLength(w, f, rule)
49+
case tagMinVals:
50+
err = sliceRuleMinVals(w, f, rule)
4851
case tagRequired:
4952
sliceRuleRequired(w, f, rule)
5053
case tagRequiredAnyOf:
5154
err = ruleRequiredOneOf(w, fields, rule.value)
55+
case tagRequiredIfAny:
56+
err = ruleRequiredIfAny(w, fields, f, rule.value)
5257
default:
5358
return errors.Wrap(errUnhandledTagRule(rule), "slice")
5459
}
@@ -79,6 +84,17 @@ for _, elem := range val.%s{
7984
return fmt.Errorf("unhandled tag rule max for type %s", f.Type().Underlying())
8085
}
8186

87+
func sliceRuleMinVals(w io.Writer, f structField, rule validationRule) error {
88+
fmt.Fprintf(w, `
89+
for _, elem := range val.%s{
90+
if elem %s %s{
91+
return fmt.Errorf("'%s': validation rule '%s(%s)' violated")
92+
}
93+
}
94+
`[1:], f.Name(), ruleMinMaxOperator(rule.name), rule.value, jsonName(f), rule.name, rule.value)
95+
return nil
96+
}
97+
8298
func sliceRuleRequired(w io.Writer, f structField, rule validationRule) {
8399
fmt.Fprintf(w, `
84100
if len(val.%s) == 0{
@@ -110,11 +126,19 @@ func generateJSONPropertySlice(info *fieldInfo, parent *property, child *propert
110126
// NOTE(simi): set required=true to be aligned with previous JSON schema definitions
111127
items := property{Type: &propertyType{names: []propertyTypeName{itemsType}, required: true}}
112128
switch itemsType {
129+
case TypeNameInteger:
130+
setPropertyRulesInteger(info, &items)
131+
case TypeNameNumber:
132+
setPropertyRulesNumber(info, &items)
113133
case TypeNameString:
114134
setPropertyRulesString(info, &items)
115135
default:
116136
return fmt.Errorf("unhandled slice item type %s", itemsType)
117137
}
138+
if minVals, ok := info.tags[tagMinVals]; ok {
139+
items.Min = json.Number(minVals)
140+
delete(info.tags, tagMinVals)
141+
}
118142
child.Items = &items
119143
return nil
120144
}

0 commit comments

Comments
 (0)