Skip to content

Commit 91cde84

Browse files
carsoniplahsivjar
andauthored
[exporter/elasticsearch] Merge *.geo.location.{lat,lon} to *.geo.location in OTel mode (open-telemetry#36594)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description In OTel mapping mode, merge *.geo.location.{lat,lon} to *.geo.location such that they are stored as [geo_point](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html) in Elasticsearch. <!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes open-telemetry#36565 <!--Describe what testing was performed and which tests were added.--> #### Testing <!--Describe the documentation added.--> #### Documentation <!--Please delete paragraphs that you did not use before submitting.--> --------- Co-authored-by: Vishal Raj <[email protected]>
1 parent dd600c1 commit 91cde84

File tree

3 files changed

+137
-1
lines changed

3 files changed

+137
-1
lines changed
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: elasticsearchexporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Map *.geo.location.{lat,lon} as geo_point field in OTel mode
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: [36565]
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: In OTel mapping mode, merge *.geo.location.{lat,lon} to *.geo.location such that they are stored as geo_point in Elasticsearch.
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: [user]

exporter/elasticsearchexporter/model.go

+77-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"hash/fnv"
1414
"math"
1515
"slices"
16+
"strings"
1617
"time"
1718

1819
jsoniter "github.com/json-iterator/go"
@@ -599,7 +600,7 @@ func (m *encodeModel) encodeResourceOTelMode(document *objmodel.Document, resour
599600
}
600601
return false
601602
})
602-
603+
mergeGeolocation(resourceAttrMap)
603604
document.Add("resource", objmodel.ValueFromAttribute(resourceMapVal))
604605
}
605606

@@ -625,6 +626,7 @@ func (m *encodeModel) encodeScopeOTelMode(document *objmodel.Document, scope pco
625626
}
626627
return false
627628
})
629+
mergeGeolocation(scopeAttrMap)
628630
document.Add("scope", objmodel.ValueFromAttribute(scopeMapVal))
629631
}
630632

@@ -644,6 +646,7 @@ func (m *encodeModel) encodeAttributesOTelMode(document *objmodel.Document, attr
644646
}
645647
return false
646648
})
649+
mergeGeolocation(attrsCopy)
647650
document.AddAttributes("attributes", attrsCopy)
648651
}
649652

@@ -998,3 +1001,76 @@ func sliceHash(h hash.Hash, s pcommon.Slice) {
9981001
valueHash(h, s.At(i))
9991002
}
10001003
}
1004+
1005+
// mergeGeolocation mutates attributes map to merge all `geo.location.{lon,lat}`,
1006+
// and namespaced `*.geo.location.{lon,lat}` to unnamespaced and namespaced `geo.location`.
1007+
// This is to match the geo_point type in Elasticsearch.
1008+
func mergeGeolocation(attributes pcommon.Map) {
1009+
const (
1010+
lonKey = "geo.location.lon"
1011+
latKey = "geo.location.lat"
1012+
mergedKey = "geo.location"
1013+
)
1014+
// Prefix is the attribute name without lonKey or latKey suffix
1015+
// e.g. prefix of "foo.bar.geo.location.lon" is "foo.bar.", prefix of "geo.location.lon" is "".
1016+
prefixToGeo := make(map[string]struct {
1017+
lon, lat float64
1018+
lonSet, latSet bool
1019+
})
1020+
setLon := func(prefix string, v float64) {
1021+
g := prefixToGeo[prefix]
1022+
g.lon = v
1023+
g.lonSet = true
1024+
prefixToGeo[prefix] = g
1025+
}
1026+
setLat := func(prefix string, v float64) {
1027+
g := prefixToGeo[prefix]
1028+
g.lat = v
1029+
g.latSet = true
1030+
prefixToGeo[prefix] = g
1031+
}
1032+
attributes.RemoveIf(func(key string, val pcommon.Value) bool {
1033+
if val.Type() != pcommon.ValueTypeDouble {
1034+
return false
1035+
}
1036+
1037+
if key == lonKey {
1038+
setLon("", val.Double())
1039+
return true
1040+
} else if key == latKey {
1041+
setLat("", val.Double())
1042+
return true
1043+
} else if namespace, found := strings.CutSuffix(key, "."+lonKey); found {
1044+
prefix := namespace + "."
1045+
setLon(prefix, val.Double())
1046+
return true
1047+
} else if namespace, found := strings.CutSuffix(key, "."+latKey); found {
1048+
prefix := namespace + "."
1049+
setLat(prefix, val.Double())
1050+
return true
1051+
}
1052+
return false
1053+
})
1054+
1055+
for prefix, geo := range prefixToGeo {
1056+
if geo.lonSet && geo.latSet {
1057+
key := prefix + mergedKey
1058+
// Geopoint expressed as an array with the format: [lon, lat]
1059+
s := attributes.PutEmptySlice(key)
1060+
s.EnsureCapacity(2)
1061+
s.AppendEmpty().SetDouble(geo.lon)
1062+
s.AppendEmpty().SetDouble(geo.lat)
1063+
continue
1064+
}
1065+
1066+
// Place the attributes back if lon and lat are not present together
1067+
if geo.lonSet {
1068+
key := prefix + lonKey
1069+
attributes.PutDouble(key, geo.lon)
1070+
}
1071+
if geo.latSet {
1072+
key := prefix + latKey
1073+
attributes.PutDouble(key, geo.lat)
1074+
}
1075+
}
1076+
}

exporter/elasticsearchexporter/model_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -1278,3 +1278,36 @@ func TestEncodeLogBodyMapMode(t *testing.T) {
12781278
require.Error(t, err)
12791279
require.ErrorIs(t, err, ErrInvalidTypeForBodyMapMode)
12801280
}
1281+
1282+
func TestMergeGeolocation(t *testing.T) {
1283+
attributes := map[string]any{
1284+
"geo.location.lon": 1.1,
1285+
"geo.location.lat": 2.2,
1286+
"foo.bar.geo.location.lon": 3.3,
1287+
"foo.bar.geo.location.lat": 4.4,
1288+
"a.geo.location.lon": 5.5,
1289+
"b.geo.location.lat": 6.6,
1290+
"unrelatedgeo.location.lon": 7.7,
1291+
"unrelatedgeo.location.lat": 8.8,
1292+
"d": 9.9,
1293+
"e.geo.location.lon": "foo",
1294+
"e.geo.location.lat": "bar",
1295+
}
1296+
wantAttributes := map[string]any{
1297+
"geo.location": []any{1.1, 2.2},
1298+
"foo.bar.geo.location": []any{3.3, 4.4},
1299+
"a.geo.location.lon": 5.5,
1300+
"b.geo.location.lat": 6.6,
1301+
"unrelatedgeo.location.lon": 7.7,
1302+
"unrelatedgeo.location.lat": 8.8,
1303+
"d": 9.9,
1304+
"e.geo.location.lon": "foo",
1305+
"e.geo.location.lat": "bar",
1306+
}
1307+
input := pcommon.NewMap()
1308+
err := input.FromRaw(attributes)
1309+
require.NoError(t, err)
1310+
mergeGeolocation(input)
1311+
after := input.AsRaw()
1312+
assert.Equal(t, wantAttributes, after)
1313+
}

0 commit comments

Comments
 (0)