From ef8307434e75b4c8595872e7257f95263c9e55e5 Mon Sep 17 00:00:00 2001 From: Sean Porter Date: Mon, 9 Jan 2023 09:00:09 -0800 Subject: [PATCH] Integrate collector with the new metadata api (#858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * begin integrating collector with new metadata api Signed-off-by: Sean Porter * metadata api, use collector fields for tag details Signed-off-by: Sean Porter * metadata api, real host details Signed-off-by: Sean Porter * metadata api, get host ip Signed-off-by: Sean Porter * metadata api, proxy info Signed-off-by: Sean Porter * metadata api, send metadata on start, updated tests Signed-off-by: Sean Porter * metadata api, changelog entry Signed-off-by: Sean Porter * metadata api, attempt to fix lint errors Signed-off-by: Sean Porter * metadata api, update gopsutil Signed-off-by: Sean Porter * metadata api, go mod tidy Signed-off-by: Sean Porter * metadata api, updated go mod for exporter Signed-off-by: Sean Porter * metadata api, go mod tidy and lint fixes Signed-off-by: Sean Porter * metadata api, exporter go mod tidy Signed-off-by: Sean Porter * metadata api, use retry with backoff Signed-off-by: Sean Porter * metadata api, added new host details environment Signed-off-by: Sean Porter * metadata api, cleaned up changelog Signed-off-by: Sean Porter * metadata api, fixed auth Signed-off-by: Sean Porter * metadata api, fixed redirect tests Signed-off-by: Sean Porter * metadata api, added environment configuration field and removed network proxy info Signed-off-by: Sean Porter * metadata api, corrected metadata api URL Signed-off-by: Sean Porter * metadata api, always send tag details, fixed host address Signed-off-by: Sean Porter * metadata api, payload casing and agent -> collector rename Signed-off-by: Sean Porter * metadata api, removed omitempty all fields are required Signed-off-by: Sean Porter * metadata api, info -> debug request logging Signed-off-by: Sean Porter * metadata api, feature gate update metadata Signed-off-by: Sean Porter * metadata api, err check feature gate apply Signed-off-by: Sean Porter * Update pkg/extension/sumologicextension/extension.go Co-authored-by: Mikołaj Świątek * metadata api, use real build version for running version Signed-off-by: Sean Porter * metadata api, make it clear we don't connect to determine address Signed-off-by: Sean Porter * metadata api, doesn't respond with a 204 Signed-off-by: Sean Porter Signed-off-by: Sean Porter Co-authored-by: Mikołaj Świątek --- CHANGELOG.md | 5 + pkg/exporter/sumologicexporter/go.mod | 7 + pkg/exporter/sumologicexporter/go.sum | 18 + .../sumologicextension/api/metadata.go | 37 ++ pkg/extension/sumologicextension/config.go | 3 + pkg/extension/sumologicextension/extension.go | 158 +++++++- .../sumologicextension/extension_test.go | 338 +++++++++++++----- pkg/extension/sumologicextension/factory.go | 2 +- pkg/extension/sumologicextension/go.mod | 7 + pkg/extension/sumologicextension/go.sum | 18 + 10 files changed, 511 insertions(+), 82 deletions(-) create mode 100644 pkg/extension/sumologicextension/api/metadata.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 77ef94833b..8dee348fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- feat: Integrated collector with new metadata API [#858] + [Unreleased]: https://github.com/SumoLogic/sumologic-otel-collector/compare/v0.68.0-sumo-0...main +[#858]: https://github.com/SumoLogic/sumologic-otel-collector/pull/858 ## [v0.68.0-sumo-0] diff --git a/pkg/exporter/sumologicexporter/go.mod b/pkg/exporter/sumologicexporter/go.mod index 1cf75b9d7b..a878d5e914 100644 --- a/pkg/exporter/sumologicexporter/go.mod +++ b/pkg/exporter/sumologicexporter/go.mod @@ -22,6 +22,7 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -31,13 +32,19 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/knadh/koanf v1.4.4 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rs/cors v1.8.2 // indirect + github.com/shirou/gopsutil/v3 v3.22.11 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect go.opentelemetry.io/collector/confmap v0.68.0 // indirect go.opentelemetry.io/collector/featuregate v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.37.0 // indirect diff --git a/pkg/exporter/sumologicexporter/go.sum b/pkg/exporter/sumologicexporter/go.sum index cc0ccb93cb..695a4ba0f1 100644 --- a/pkg/exporter/sumologicexporter/go.sum +++ b/pkg/exporter/sumologicexporter/go.sum @@ -75,6 +75,8 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -116,6 +118,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -188,6 +191,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -240,6 +245,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -268,6 +275,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil/v3 v3.22.11 h1:kxsPKS+Eeo+VnEQ2XCaGJepeP6KY53QoRTETx3+1ndM= +github.com/shirou/gopsutil/v3 v3.22.11/go.mod h1:xl0EeL4vXJ+hQMAGN8B9VFpxukEMA0XdevQOe5MZ1oY= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -287,9 +296,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= @@ -387,6 +402,7 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -401,12 +417,14 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/pkg/extension/sumologicextension/api/metadata.go b/pkg/extension/sumologicextension/api/metadata.go new file mode 100644 index 0000000000..4df009d318 --- /dev/null +++ b/pkg/extension/sumologicextension/api/metadata.go @@ -0,0 +1,37 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +type OpenMetadataHostDetails struct { + Name string `json:"name"` + OsName string `json:"osName"` + OsVersion string `json:"osVersion"` + Environment string `json:"environment"` +} + +type OpenMetadataCollectorDetails struct { + RunningVersion string `json:"runningVersion"` +} + +type OpenMetadataNetworkDetails struct { + HostIpAddress string `json:"hostIpAddress"` +} + +type OpenMetadataRequestPayload struct { + HostDetails OpenMetadataHostDetails `json:"hostDetails"` + CollectorDetails OpenMetadataCollectorDetails `json:"collectorDetails"` + NetworkDetails OpenMetadataNetworkDetails `json:"networkDetails"` + TagDetails map[string]interface{} `json:"tagDetails"` +} diff --git a/pkg/extension/sumologicextension/config.go b/pkg/extension/sumologicextension/config.go index 4f39356b5f..e694132468 100644 --- a/pkg/extension/sumologicextension/config.go +++ b/pkg/extension/sumologicextension/config.go @@ -37,6 +37,9 @@ type Config struct { // Please note that registering a collector under a name which is already // used is not allowed. CollectorName string `mapstructure:"collector_name"` + // CollectorEnvironment is the environment which will be used when updating + // the collector metadata. + CollectorEnvironment string `mapstructure:"collector_environment"` // CollectorDescription is the description which will be used when the // collector is being registered. CollectorDescription string `mapstructure:"collector_description"` diff --git a/pkg/extension/sumologicextension/extension.go b/pkg/extension/sumologicextension/extension.go index d9f4e76ea8..d056d3206b 100644 --- a/pkg/extension/sumologicextension/extension.go +++ b/pkg/extension/sumologicextension/extension.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/url" "os" @@ -31,9 +32,11 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/google/uuid" + "github.com/shirou/gopsutil/v3/host" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/extension/auth" + "go.opentelemetry.io/collector/featuregate" "go.uber.org/zap" grpccredentials "google.golang.org/grpc/credentials" @@ -43,6 +46,7 @@ import ( type SumologicExtension struct { collectorName string + buildVersion string // The lock around baseUrl is needed because sumologicexporter is using // it as base URL for API requests and this access has to be coordinated. @@ -57,6 +61,7 @@ type SumologicExtension struct { hashKey string httpClient *http.Client registrationInfo api.OpenRegisterResponsePayload + updateMetadata bool closeChan chan struct{} closeOnce sync.Once @@ -66,6 +71,7 @@ type SumologicExtension struct { const ( heartbeatUrl = "/api/v1/collector/heartbeat" + metadataUrl = "/api/v1/otCollectors/metadata" registerUrl = "/api/v1/collector/register" collectorIdField = "collector_id" @@ -74,15 +80,27 @@ const ( ) const ( + updateCollectorMetadataID = "extension.sumologic.updateCollectorMetadata" + updateCollectorMetadataStage = featuregate.StageAlpha + DefaultHeartbeatInterval = 15 * time.Second ) +func init() { + featuregate.GetRegistry().MustRegisterID( + updateCollectorMetadataID, + updateCollectorMetadataStage, + featuregate.WithRegisterDescription("When enabled, the collector will update its Sumo Logic metadata on startup."), + featuregate.WithRegisterReferenceURL("https://github.com/SumoLogic/sumologic-otel-collector/pull/858"), + ) +} + var errGRPCNotSupported = fmt.Errorf("gRPC is not supported by sumologicextension") // SumologicExtension implements ClientAuthenticator var _ auth.Client = (*SumologicExtension)(nil) -func newSumologicExtension(conf *Config, logger *zap.Logger, id component.ID) (*SumologicExtension, error) { +func newSumologicExtension(conf *Config, logger *zap.Logger, id component.ID, buildVersion string) (*SumologicExtension, error) { if conf.Credentials.InstallToken == "" { return nil, errors.New("access credentials not provided: need install_token") } @@ -128,12 +146,14 @@ func newSumologicExtension(conf *Config, logger *zap.Logger, id component.ID) (* return &SumologicExtension{ collectorName: collectorName, + buildVersion: buildVersion, baseUrl: strings.TrimSuffix(conf.ApiBaseUrl, "/"), conf: conf, origLogger: logger, logger: logger, hashKey: hashKey, credentialsStore: credentialsStore, + updateMetadata: featuregate.GetRegistry().IsEnabled(updateCollectorMetadataID), closeChan: make(chan struct{}), backOff: backOff, id: id, @@ -166,6 +186,13 @@ func (se *SumologicExtension) Start(ctx context.Context, host component.Host) er zap.String(collectorIdField, colCreds.Credentials.CollectorId), ) + if se.updateMetadata { + err = se.updateMetadataWithBackoff(ctx) + if err != nil { + return err + } + } + go se.heartbeatLoop() return nil @@ -539,6 +566,7 @@ func (se *SumologicExtension) heartbeatLoop() { } var errUnauthorizedHeartbeat = errors.New("heartbeat unauthorized") +var errUnauthorizedMetadata = errors.New("metadata update unauthorized") type ErrorAPI struct { status int @@ -592,6 +620,134 @@ func (se *SumologicExtension) sendHeartbeatWithHTTPClient(ctx context.Context, h return nil } +func getHostIpAddress() (string, error) { + // This doesn't connect, we just need the connection object. + c, err := net.Dial("udp", "255.255.255.255:53") + if err != nil { + return "", err + } + + defer c.Close() + a := c.LocalAddr().(*net.UDPAddr) + h, _, err := net.SplitHostPort(a.String()) + if err != nil { + return "", err + } + + return h, nil +} + +func (se *SumologicExtension) updateMetadataWithHTTPClient(ctx context.Context, httpClient *http.Client) error { + u, err := url.Parse(se.BaseUrl() + metadataUrl) + if err != nil { + return fmt.Errorf("unable to parse metadata URL %w", err) + } + + info, err := host.Info() + if err != nil { + return err + } + + ip, err := getHostIpAddress() + if err != nil { + return err + } + + td := se.conf.CollectorFields + if td == nil { + td = map[string]interface{}{} + } + + var buff bytes.Buffer + if err = json.NewEncoder(&buff).Encode(api.OpenMetadataRequestPayload{ + HostDetails: api.OpenMetadataHostDetails{ + Name: info.Hostname, + OsName: info.OS, + OsVersion: info.PlatformVersion, + Environment: se.conf.CollectorEnvironment, + }, + CollectorDetails: api.OpenMetadataCollectorDetails{ + RunningVersion: se.buildVersion, + }, + NetworkDetails: api.OpenMetadataNetworkDetails{ + HostIpAddress: ip, + }, + TagDetails: td, + }); err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), &buff) + if err != nil { + return fmt.Errorf("unable to create HTTP request %w", err) + } + + addJSONHeaders(req) + + se.logger.Info("Updating collector metadata", + zap.String("URL", u.String()), + zap.String("body", buff.String())) + + res, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("unable to send HTTP request: %w", err) + } + defer res.Body.Close() + + switch res.StatusCode { + default: + var buff bytes.Buffer + if _, err := io.Copy(&buff, res.Body); err != nil { + return fmt.Errorf( + "failed to copy collector metadata response body, status code: %d, err: %w", + res.StatusCode, err, + ) + } + + se.logger.Warn("Metadata API error response", + zap.Int("status", res.StatusCode), + zap.String("body", buff.String())) + + return fmt.Errorf("collector metadata request failed: %w", + ErrorAPI{ + status: res.StatusCode, + body: buff.String(), + }, + ) + + case http.StatusUnauthorized: + return errUnauthorizedMetadata + case http.StatusOK: + } + + return nil +} + +func (se *SumologicExtension) updateMetadataWithBackoff(ctx context.Context) error { + se.backOff.Reset() + for { + err := se.updateMetadataWithHTTPClient(ctx, se.httpClient) + if err == nil { + return nil + } + + nbo := se.backOff.NextBackOff() + // Return error if backoff reaches the limit or uncoverable error is spotted + if _, ok := err.(*backoff.PermanentError); nbo == se.backOff.Stop || ok { + return fmt.Errorf("collector metadata update failed: %w", err) + } + + t := time.NewTimer(nbo) + defer t.Stop() + + select { + case <-t.C: + case <-ctx.Done(): + return fmt.Errorf("collector metadata update cancelled: %w", ctx.Err()) + } + } +} + func (se *SumologicExtension) ComponentID() component.ID { return se.id } diff --git a/pkg/extension/sumologicextension/extension_test.go b/pkg/extension/sumologicextension/extension_test.go index 912a20e92e..4fd3227088 100644 --- a/pkg/extension/sumologicextension/extension_test.go +++ b/pkg/extension/sumologicextension/extension_test.go @@ -34,6 +34,7 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/featuregate" "go.uber.org/zap" "github.com/SumoLogic/sumologic-otel-collector/pkg/extension/sumologicextension/api" @@ -44,6 +45,20 @@ const ( uuidRegex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" ) +func TestMain(m *testing.M) { + // Enable the feature gates before all tests to avoid flaky tests. + err := featuregate.GetRegistry().Apply(map[string]bool{ + updateCollectorMetadataID: true, + }) + + if err != nil { + panic("unable to set feature gates") + } + + code := m.Run() + os.Exit(code) +} + func TestBasicExtensionConstruction(t *testing.T) { t.Parallel() @@ -79,7 +94,7 @@ func TestBasicExtensionConstruction(t *testing.T) { for _, tc := range testcases { t.Run(tc.Name, func(t *testing.T) { - se, err := newSumologicExtension(tc.Config, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(tc.Config, zap.NewNop(), component.NewID("sumologic"), "1.0.0") if tc.WantErr { assert.Error(t, err) } else { @@ -114,8 +129,13 @@ func TestBasicStart(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } - // heartbeat + // metadata case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 3: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) @@ -137,7 +157,7 @@ func TestBasicStart(t *testing.T) { cfg.Credentials.InstallToken = "dummy_install_token" cfg.CollectorCredentialsDirectory = dir - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) assert.NotEmpty(t, se.registrationInfo.CollectorCredentialId) @@ -171,8 +191,13 @@ func TestStoreCredentials(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } - // heartbeat + // metadata case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 3: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) @@ -208,7 +233,7 @@ func TestStoreCredentials(t *testing.T) { // Ensure the directory doesn't exist before running the extension require.NoError(t, os.RemoveAll(dir)) - se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) key := createHashKey(cfg) fileName, err := credentials.HashKeyToFilename(key) @@ -234,7 +259,7 @@ func TestStoreCredentials(t *testing.T) { // Ensure the directory has 600 permissions require.NoError(t, os.Chmod(dir, 0600)) - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) key := createHashKey(cfg) fileName, err := credentials.HashKeyToFilename(key) @@ -259,7 +284,7 @@ func TestStoreCredentials(t *testing.T) { // Ensure the directory has 700 permissions require.NoError(t, os.Chmod(dir, 0700)) - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) key := createHashKey(cfg) fileName, err := credentials.HashKeyToFilename(key) @@ -282,7 +307,7 @@ func TestStoreCredentials(t *testing.T) { cfg := getConfig(srv.URL) cfg.CollectorCredentialsDirectory = dir - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) key := createHashKey(cfg) fileName, err := credentials.HashKeyToFilename(key) @@ -320,6 +345,11 @@ func TestStoreCredentials_PreexistingCredentialsAreUsed(t *testing.T) { require.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) + // metadata + case 2: + require.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + // should not produce any more requests default: w.WriteHeader(http.StatusInternalServerError) @@ -368,7 +398,7 @@ func TestStoreCredentials_PreexistingCredentialsAreUsed(t *testing.T) { }), ) - se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) fileName, err := credentials.HashKeyToFilename(hashKey) @@ -388,7 +418,7 @@ func TestStoreCredentials_PreexistingCredentialsAreUsed(t *testing.T) { credsPathMd5 := path.Join(dir, fileNameMd5) require.NoFileExists(t, credsPathMd5) - require.EqualValues(t, atomic.LoadInt32(&reqCount), 1) + require.EqualValues(t, atomic.LoadInt32(&reqCount), 2) } func TestLocalFSCredentialsStore_WorkCorrectlyForMultipleExtensions(t *testing.T) { @@ -416,8 +446,13 @@ func TestLocalFSCredentialsStore_WorkCorrectlyForMultipleExtensions(t *testing.T w.WriteHeader(http.StatusInternalServerError) } - // heartbeat + // metadata case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 3: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) @@ -464,7 +499,7 @@ func TestLocalFSCredentialsStore_WorkCorrectlyForMultipleExtensions(t *testing.T logger2, err := zap.NewDevelopment(zap.Fields(zap.Int("#", 2))) require.NoError(t, err) - se1, err := newSumologicExtension(cfg1, logger1, component.NewID("sumologic")) + se1, err := newSumologicExtension(cfg1, logger1, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, se1.Shutdown(context.Background())) }) fileName1, err := credentials.HashKeyToFilename(createHashKey(cfg1)) @@ -474,7 +509,7 @@ func TestLocalFSCredentialsStore_WorkCorrectlyForMultipleExtensions(t *testing.T require.NoError(t, se1.Start(context.Background(), componenttest.NewNopHost())) require.FileExists(t, credsPath1) - se2, err := newSumologicExtension(cfg2, logger2, component.NewID("sumologic")) + se2, err := newSumologicExtension(cfg2, logger2, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, se2.Shutdown(context.Background())) }) fileName2, err := credentials.HashKeyToFilename(createHashKey(cfg2)) @@ -520,8 +555,13 @@ func TestRegisterEmptyCollectorName(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } - // heartbeat + // metadata case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 3: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) @@ -545,7 +585,7 @@ func TestRegisterEmptyCollectorName(t *testing.T) { cfg.Credentials.InstallToken = "dummy_install_token" cfg.CollectorCredentialsDirectory = dir - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) regexPattern := fmt.Sprintf("%s-%s", hostname, uuidRegex) @@ -586,8 +626,13 @@ func TestRegisterEmptyCollectorNameForceRegistration(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } - // register again because force registration was set + // metadata case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // register again because force registration was set + case 3: require.Equal(t, registerUrl, req.URL.Path) authHeader := req.Header.Get("Authorization") @@ -604,6 +649,11 @@ func TestRegisterEmptyCollectorNameForceRegistration(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } + // metadata + case 4: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + // should not produce any more requests default: w.WriteHeader(http.StatusInternalServerError) @@ -625,7 +675,7 @@ func TestRegisterEmptyCollectorNameForceRegistration(t *testing.T) { cfg.CollectorCredentialsDirectory = dir cfg.ForceRegistration = true - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) require.NoError(t, se.Shutdown(context.Background())) @@ -637,7 +687,7 @@ func TestRegisterEmptyCollectorNameForceRegistration(t *testing.T) { colCreds, err := se.credentialsStore.Get(se.hashKey) require.NoError(t, err) colName := colCreds.CollectorName - se, err = newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err = newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) assert.Equal(t, se.collectorName, colName) @@ -672,8 +722,13 @@ func TestCollectorSendsBasicAuthHeadersOnRegistration(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } - // heartbeat + // metadata case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 3: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) @@ -696,7 +751,7 @@ func TestCollectorSendsBasicAuthHeadersOnRegistration(t *testing.T) { cfg.Credentials.InstallToken = "dummy_install_token" cfg.CollectorCredentialsDirectory = dir - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) require.NoError(t, se.Shutdown(context.Background())) @@ -745,24 +800,40 @@ func TestCollectorCheckingCredentialsFoundInLocalStorage(t *testing.T) { }{ { name: "collector checks found credentials via heartbeat call - no registration is done", - expectedReqCount: 2, + expectedReqCount: 3, srvFn: func() (*httptest.Server, *int32) { var reqCount int32 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - atomic.AddInt32(&reqCount, 1) - - require.NotEqual(t, registerUrl, req.URL.Path, - "collector shouldn't call the register API when credentials locally retrieved") - require.Equal(t, heartbeatUrl, req.URL.Path) - w.WriteHeader(204) - - authHeader := req.Header.Get("Authorization") - token := base64.StdEncoding.EncodeToString( - []byte("test-credential-id:test-credential-key"), - ) - assert.Equal(t, "Basic "+token, authHeader, - "collector didn't send correct Authorization header with heartbeat request") + reqNum := atomic.AddInt32(&reqCount, 1) + + switch reqNum { + + // heatbeat + case 1: + require.NotEqual(t, registerUrl, req.URL.Path, + "collector shouldn't call the register API when credentials locally retrieved") + + assert.Equal(t, heartbeatUrl, req.URL.Path) + + authHeader := req.Header.Get("Authorization") + token := base64.StdEncoding.EncodeToString( + []byte("test-credential-id:test-credential-key"), + ) + assert.Equal(t, "Basic "+token, authHeader, + "collector didn't send correct Authorization header with heartbeat request") + + w.WriteHeader(204) + + // metadata + case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // should not produce any more requests + default: + w.WriteHeader(http.StatusInternalServerError) + } })), &reqCount }, @@ -777,7 +848,7 @@ func TestCollectorCheckingCredentialsFoundInLocalStorage(t *testing.T) { }, { name: "collector registers when no matching credentials are found in local storage", - expectedReqCount: 2, + expectedReqCount: 3, srvFn: func() (*httptest.Server, *int32) { var reqCount int32 @@ -803,8 +874,12 @@ func TestCollectorCheckingCredentialsFoundInLocalStorage(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } - // heartbeat + // metadata case 2: + w.WriteHeader(200) + + // heartbeat + case 3: w.WriteHeader(204) // should not produce any more requests @@ -838,7 +913,7 @@ func TestCollectorCheckingCredentialsFoundInLocalStorage(t *testing.T) { logger, err := zap.NewDevelopment() require.NoError(t, err) - se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) @@ -895,8 +970,13 @@ func TestRegisterEmptyCollectorNameWithBackoff(t *testing.T) { } } - // heartbeat + // metadata case reqNum == retriesLimit+1: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case reqNum == retriesLimit+2: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) @@ -922,7 +1002,7 @@ func TestRegisterEmptyCollectorNameWithBackoff(t *testing.T) { cfg.BackOff.InitialInterval = time.Millisecond cfg.BackOff.MaxInterval = time.Millisecond - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) regexPattern := fmt.Sprintf("%s-%s", hostname, uuidRegex) @@ -974,7 +1054,7 @@ func TestRegisterEmptyCollectorNameUnrecoverableError(t *testing.T) { cfg.BackOff.InitialInterval = time.Millisecond cfg.BackOff.MaxInterval = time.Millisecond - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.EqualError(t, se.Start(context.Background(), componenttest.NewNopHost()), "collector registration failed: failed to register the collector, got HTTP status code: 404") @@ -1010,15 +1090,35 @@ func TestRegistrationRedirect(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) } - // heartbeat, and 2 heartbeats after restart - case 2, 3, 4: + // metadata + case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 3: + assert.Equal(t, heartbeatUrl, req.URL.Path) + w.WriteHeader(204) + + // heartbeat + case 4: + assert.Equal(t, heartbeatUrl, req.URL.Path) + w.WriteHeader(204) + + // metadata + case 5: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 6: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) // should not produce any more requests default: require.Fail(t, - "extension should not make more than 2 requests to the destination server", + "extension should not make more than 5 requests to the destination server", ) } }, @@ -1062,22 +1162,22 @@ func TestRegistrationRedirect(t *testing.T) { require.NoError(t, err) t.Run("works correctly", func(t *testing.T) { - se, err := newSumologicExtension(configFn(), logger, component.NewID("sumologic")) + se, err := newSumologicExtension(configFn(), logger, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) assert.Eventually(t, func() bool { return atomic.LoadInt32(&origReqCount) == 1 }, 5*time.Second, 100*time.Millisecond, "extension should only make 1 request to the original server before redirect", ) - assert.Eventually(t, func() bool { return atomic.LoadInt32(&destReqCount) == 2 }, + assert.Eventually(t, func() bool { return atomic.LoadInt32(&destReqCount) == 3 }, 5*time.Second, 100*time.Millisecond, - "extension should make 2 requests (registration + heartbeat) to the destination server", + "extension should make 3 requests (registration + metadata + heartbeat) to the destination server", ) require.NoError(t, se.Shutdown(context.Background())) }) t.Run("credentials store retrieves credentials with redirected api url", func(t *testing.T) { - se, err := newSumologicExtension(configFn(), logger, component.NewID("sumologic")) + se, err := newSumologicExtension(configFn(), logger, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) @@ -1086,10 +1186,10 @@ func TestRegistrationRedirect(t *testing.T) { "after restarting with locally stored credentials extension shouldn't call the original server", ) - assert.Eventually(t, func() bool { return atomic.LoadInt32(&destReqCount) == 4 }, + assert.Eventually(t, func() bool { return atomic.LoadInt32(&destReqCount) == 6 }, 5*time.Second, 100*time.Millisecond, - "extension should make 4 requests (registration + heartbeat, after restart "+ - "heartbeat to validate credentials and then the first heartbeat on "+ + "extension should make 6 requests (registration + metadata + heartbeat, after restart "+ + "heartbeat to validate credentials, metadata update, and then the first heartbeat on "+ "which we wait here) to the destination server", ) @@ -1130,19 +1230,24 @@ func TestCollectorReregistersAfterHTTPUnathorizedFromHeartbeat(t *testing.T) { assert.Equal(t, registerUrl, req.URL.Path) handlerRegister() - // heartbeat + // metadata case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + + // heartbeat + case 3: assert.Equal(t, heartbeatUrl, req.URL.Path) w.WriteHeader(204) // heartbeat - case 3: + case 4: assert.Equal(t, heartbeatUrl, req.URL.Path) // return unauthorized to mimic collector being removed from API w.WriteHeader(http.StatusUnauthorized) // register - case 4: + case 5: assert.Equal(t, registerUrl, req.URL.Path) handlerRegister() @@ -1169,7 +1274,7 @@ func TestCollectorReregistersAfterHTTPUnathorizedFromHeartbeat(t *testing.T) { logger, err := zap.NewDevelopment() require.NoError(t, err) - se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, logger, component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) @@ -1193,36 +1298,48 @@ func TestRegistrationRequestPayload(t *testing.T) { hostname, err := os.Hostname() require.NoError(t, err) + var reqCount int32 srv := httptest.NewServer(func() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - require.Equal(t, registerUrl, req.URL.Path) + reqNum := atomic.AddInt32(&reqCount, 1) - var reqPayload api.OpenRegisterRequestPayload - require.NoError(t, json.NewDecoder(req.Body).Decode(&reqPayload)) - require.True(t, reqPayload.Clobber) - require.Equal(t, hostname, reqPayload.Hostname) - require.Equal(t, "my description", reqPayload.Description) - require.Equal(t, "my category/", reqPayload.Category) - require.EqualValues(t, - map[string]interface{}{ - "field1": "value1", - "field2": "value2", - }, - reqPayload.Fields, - ) - require.Equal(t, "PST", reqPayload.TimeZone) + switch reqNum { + // register + case 1: + require.Equal(t, registerUrl, req.URL.Path) - authHeader := req.Header.Get("Authorization") - assert.Equal(t, "Bearer dummy_install_token", authHeader, - "collector didn't send correct Authorization header with registration request") + var reqPayload api.OpenRegisterRequestPayload + require.NoError(t, json.NewDecoder(req.Body).Decode(&reqPayload)) + require.True(t, reqPayload.Clobber) + require.Equal(t, hostname, reqPayload.Hostname) + require.Equal(t, "my description", reqPayload.Description) + require.Equal(t, "my category/", reqPayload.Category) + require.EqualValues(t, + map[string]interface{}{ + "field1": "value1", + "field2": "value2", + }, + reqPayload.Fields, + ) + require.Equal(t, "PST", reqPayload.TimeZone) + + authHeader := req.Header.Get("Authorization") + assert.Equal(t, "Bearer dummy_install_token", authHeader, + "collector didn't send correct Authorization header with registration request") + + _, err = w.Write([]byte(`{ + "collectorCredentialId": "mycredentialID", + "collectorCredentialKey": "mycredentialKey", + "collectorId": "0000000001231231", + "collectorName": "otc-test-123456123123" + }`)) + require.NoError(t, err) + // metadata + case 2: + assert.Equal(t, metadataUrl, req.URL.Path) + w.WriteHeader(200) + } - _, err = w.Write([]byte(`{ - "collectorCredentialId": "mycredentialID", - "collectorCredentialKey": "mycredentialKey", - "collectorId": "0000000001231231", - "collectorName": "otc-test-123456123123" - }`)) - require.NoError(t, err) }) }()) @@ -1249,7 +1366,7 @@ func TestRegistrationRequestPayload(t *testing.T) { } cfg.TimeZone = "PST" - se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic")) + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") require.NoError(t, err) require.NoError(t, se.Start(context.Background(), componenttest.NewNopHost())) regexPattern := fmt.Sprintf("%s-%s", hostname, uuidRegex) @@ -1259,3 +1376,64 @@ func TestRegistrationRequestPayload(t *testing.T) { require.NoError(t, se.Shutdown(context.Background())) } + +func TestGetHostIpAddress(t *testing.T) { + ip, err := getHostIpAddress() + require.NoError(t, err) + require.NotEmpty(t, ip) +} + +func TestUpdateMetadataRequestPayload(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(func() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + require.Equal(t, metadataUrl, req.URL.Path) + + var reqPayload api.OpenMetadataRequestPayload + require.NoError(t, json.NewDecoder(req.Body).Decode(&reqPayload)) + require.NotEmpty(t, reqPayload.HostDetails.Name) + require.NotEmpty(t, reqPayload.HostDetails.OsName) + require.NotEmpty(t, reqPayload.HostDetails.OsVersion) + require.NotEmpty(t, reqPayload.NetworkDetails.HostIpAddress) + require.EqualValues(t, reqPayload.HostDetails.Environment, "EKS-1.20.2") + require.EqualValues(t, reqPayload.CollectorDetails.RunningVersion, "1.0.0") + require.EqualValues(t, + map[string]interface{}{ + "team": "A", + "app": "linux", + }, + reqPayload.TagDetails, + ) + + _, err := w.Write([]byte(``)) + + require.NoError(t, err) + }) + }()) + + cfg := createDefaultConfig().(*Config) + cfg.CollectorName = "" + cfg.ApiBaseUrl = srv.URL + cfg.Credentials.InstallToken = "dummy_install_token" + cfg.BackOff.InitialInterval = time.Millisecond + cfg.BackOff.MaxInterval = time.Millisecond + cfg.Clobber = true + cfg.CollectorEnvironment = "EKS-1.20.2" + cfg.CollectorDescription = "my description" + cfg.CollectorCategory = "my category/" + cfg.CollectorFields = map[string]interface{}{ + "team": "A", + "app": "linux", + } + cfg.TimeZone = "PST" + + se, err := newSumologicExtension(cfg, zap.NewNop(), component.NewID("sumologic"), "1.0.0") + require.NoError(t, err) + + httpClient, err := se.getHTTPClient(se.conf.HTTPClientSettings, api.OpenRegisterResponsePayload{}) + require.NoError(t, err) + + err = se.updateMetadataWithHTTPClient(context.TODO(), httpClient) + require.NoError(t, err) +} diff --git a/pkg/extension/sumologicextension/factory.go b/pkg/extension/sumologicextension/factory.go index 4da875f95f..ac1a672122 100644 --- a/pkg/extension/sumologicextension/factory.go +++ b/pkg/extension/sumologicextension/factory.go @@ -64,5 +64,5 @@ func createDefaultConfig() component.Config { func createExtension(_ context.Context, params extension.CreateSettings, cfg component.Config) (extension.Extension, error) { config := cfg.(*Config) - return newSumologicExtension(config, params.Logger, params.ID) + return newSumologicExtension(config, params.Logger, params.ID, params.BuildInfo.Version) } diff --git a/pkg/extension/sumologicextension/go.mod b/pkg/extension/sumologicextension/go.mod index a19badf575..4ece735fbf 100644 --- a/pkg/extension/sumologicextension/go.mod +++ b/pkg/extension/sumologicextension/go.mod @@ -6,6 +6,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 + github.com/shirou/gopsutil/v3 v3.22.11 github.com/stretchr/testify v1.8.1 go.opentelemetry.io/collector v0.68.0 go.opentelemetry.io/collector/component v0.68.0 @@ -18,6 +19,7 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -25,13 +27,18 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.13 // indirect github.com/knadh/koanf v1.4.4 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rs/cors v1.8.2 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect go.opentelemetry.io/collector/confmap v0.68.0 // indirect go.opentelemetry.io/collector/consumer v0.68.0 // indirect go.opentelemetry.io/collector/featuregate v0.68.0 // indirect diff --git a/pkg/extension/sumologicextension/go.sum b/pkg/extension/sumologicextension/go.sum index b6a54efeed..0cf3577aa4 100644 --- a/pkg/extension/sumologicextension/go.sum +++ b/pkg/extension/sumologicextension/go.sum @@ -69,6 +69,8 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -106,6 +108,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -178,6 +181,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -229,6 +234,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -252,6 +259,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil/v3 v3.22.11 h1:kxsPKS+Eeo+VnEQ2XCaGJepeP6KY53QoRTETx3+1ndM= +github.com/shirou/gopsutil/v3 v3.22.11/go.mod h1:xl0EeL4vXJ+hQMAGN8B9VFpxukEMA0XdevQOe5MZ1oY= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -271,9 +280,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= @@ -363,6 +378,7 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -377,12 +393,14 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=