Repository: open-telemetry/opamp-go
Pull request #40 (2021-12-08 19:02:10)

Implement client.SetAgentAttributes


Patch:
diff --git a/client/client.go b/client/client.go
index 24dba03..e06a0f0 100644
--- a/client/client.go
+++ b/client/client.go
@@ -86,7 +86,7 @@ type OpAMPClient interface {
 	// included in the first start report after  connection is established.
 	// MAY also be called after Start(), in which case the attributes will be included
 	// in the next outgoing status report.
-	SetAgentAttributes(attrs map[string]protobufs.AnyValue) error
+	SetAgentAttributes(attrs map[string]*protobufs.AnyValue) error
 
 	// SetEffectiveConfig sets the effective config of the Agent. The config will be
 	// included in the next status report sent to the server. The behavior regarding
diff --git a/client/clientimpl.go b/client/clientimpl.go
index a223195..360e06f 100644
--- a/client/clientimpl.go
+++ b/client/clientimpl.go
@@ -104,10 +104,11 @@ func (w *client) Start(settings StartSettings) error {
 	// Prepare the first status report.
 	w.sender.UpdateNextStatus(
 		func(statusReport *protobufs.StatusReport) {
-			statusReport.AgentDescription = &protobufs.AgentDescription{
-				AgentType:    w.settings.AgentType,
-				AgentVersion: w.settings.AgentVersion,
+			if statusReport.AgentDescription == nil {
+				statusReport.AgentDescription = &protobufs.AgentDescription{}
 			}
+			statusReport.AgentDescription.AgentType = w.settings.AgentType
+			statusReport.AgentDescription.AgentVersion = w.settings.AgentVersion
 		},
 	)
 
@@ -161,8 +162,26 @@ func (w *client) Stop(ctx context.Context) error {
 	return nil
 }
 
-func (w *client) SetAgentAttributes(attrs map[string]protobufs.AnyValue) error {
-	panic("implement me")
+func (w *client) SetAgentAttributes(attrs map[string]*protobufs.AnyValue) error {
+	var attrsSlice []*protobufs.KeyValue
+	for k, v := range attrs {
+		attrsSlice = append(attrsSlice, &protobufs.KeyValue{
+			Key:   k,
+			Value: v,
+		})
+	}
+
+	w.sender.UpdateNextStatus(func(statusReport *protobufs.StatusReport) {
+		if statusReport.AgentDescription == nil {
+			statusReport.AgentDescription = &protobufs.AgentDescription{
+				AgentType:       w.settings.AgentType,
+				AgentVersion:    w.settings.AgentVersion,
+				AgentAttributes: attrsSlice,
+			}
+		}
+	})
+	w.sender.ScheduleSend()
+	return nil
 }
 
 func (w *client) SetEffectiveConfig(config *protobufs.EffectiveConfig) error {
diff --git a/go.mod b/go.mod
index f934e76..14ab7de 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.17
 require (
 	github.com/cenkalti/backoff/v4 v4.1.2
 	github.com/gorilla/websocket v1.4.2
+	github.com/knadh/koanf v1.3.3
 	github.com/oklog/ulid/v2 v2.0.2
 	github.com/stretchr/testify v1.7.0
 	google.golang.org/protobuf v1.27.1
@@ -12,7 +13,12 @@ require (
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
 )
diff --git a/go.sum b/go.sum
index a701798..0e3813b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,28 +1,164 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
+github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
+github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
+github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
+github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
+github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
 github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
+github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
+github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
+github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
+github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/knadh/koanf v1.3.3 h1:eNtBOzQDzkzIIPRCJCx/Ha3DeD/ZFwCAp8JxyqoVAls=
+github.com/knadh/koanf v1.3.3/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
 github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
+github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
+github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
+github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 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/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
+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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/examples/agent/.gitignore b/internal/examples/agent/.gitignore
new file mode 100644
index 0000000..ae3c172
--- /dev/null
+++ b/internal/examples/agent/.gitignore
@@ -0,0 +1,1 @@
+/bin/
diff --git a/internal/examples/agent/agent/agent.go b/internal/examples/agent/agent/agent.go
new file mode 100644
index 0000000..998f2db
--- /dev/null
+++ b/internal/examples/agent/agent/agent.go
@@ -0,0 +1,280 @@
+package agent
+
+import (
+	"context"
+	"crypto/sha256"
+	"fmt"
+	"math/rand"
+	"os"
+	"runtime"
+	"sort"
+	"time"
+
+	"github.com/knadh/koanf"
+	"github.com/knadh/koanf/parsers/yaml"
+	"github.com/knadh/koanf/providers/rawbytes"
+	"github.com/oklog/ulid/v2"
+	"github.com/open-telemetry/opamp-go/client/types"
+
+	"github.com/open-telemetry/opamp-go/client"
+	"github.com/open-telemetry/opamp-go/protobufs"
+)
+
+const localConfig = `
+exporters:
+  otlp:
+    endpoint: localhost:1111
+
+receivers:
+  otlp:
+    protocols:
+      grpc: {}
+      http: {}
+
+service:
+  pipelines:
+    traces:
+      receivers: [otlp]
+      processors: []
+      exporters: [otlp]
+`
+
+type Agent struct {
+	logger types.Logger
+
+	agentType    string
+	agentVersion string
+
+	effectiveConfig     string
+	effectiveConfigHash []byte
+
+	instanceId ulid.ULID
+
+	agentDescription *protobufs.AgentDescription
+
+	opampClient client.OpAMPClient
+
+	remoteConfigHash []byte
+}
+
+func NewAgent(logger types.Logger, agentType string, agentVersion string) *Agent {
+	agent := &Agent{
+		effectiveConfig: localConfig,
+		logger:          logger,
+		agentType:       agentType,
+		agentVersion:    agentVersion,
+	}
+
+	agent.createAgentIdentity()
+	agent.logger.Debugf("Agent starting, id=%v, type=%s, version=%s.",
+		agent.instanceId.String(), agentType, agentVersion)
+
+	agent.loadLocalConfig()
+	if err := agent.start(); err != nil {
+		agent.logger.Errorf("Cannot start OpAMP client: %v", err)
+		return nil
+	}
+
+	return agent
+}
+
+func (agent *Agent) start() error {
+	agent.opampClient = client.New(agent.logger)
+
+	settings := client.StartSettings{
+		OpAMPServerURL: "ws://127.0.0.1:4320/v1/opamp",
+		InstanceUid:    agent.instanceId.String(),
+		AgentType:      agent.agentType,
+		AgentVersion:   agent.agentVersion,
+		Callbacks: client.CallbacksStruct{
+			OnConnectFunc: func() {
+				agent.logger.Debugf("Connected to the server.")
+			},
+			OnConnectFailedFunc: func(err error) {
+				agent.logger.Errorf("Failed to connect to the server: %v", err)
+			},
+			OnErrorFunc: func(err *protobufs.ServerErrorResponse) {
+				agent.logger.Errorf("Server returned an error response: %v", err.ErrorMessage)
+			},
+			OnRemoteConfigFunc: agent.onRemoteConfig,
+		},
+		LastRemoteConfigHash: agent.remoteConfigHash,
+		LastEffectiveConfig:  agent.composeEffectiveConfig(),
+	}
+
+	agent.logger.Debugf("Starting OpAMP client...")
+
+	err := agent.opampClient.Start(settings)
+	if err != nil {
+		return err
+	}
+
+	agent.logger.Debugf("OpAMP Client started.")
+
+	return nil
+}
+
+func (agent *Agent) createAgentIdentity() {
+	// Generate instance id.
+	entropy := ulid.Monotonic(rand.New(rand.NewSource(0)), 0)
+	agent.instanceId = ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
+
+	hostname, _ := os.Hostname()
+
+	// Create agent description.
+	agent.agentDescription = &protobufs.AgentDescription{
+		AgentType:    agent.agentType,
+		AgentVersion: agent.agentVersion,
+		AgentAttributes: []*protobufs.KeyValue{
+			{
+				Key: "os.family",
+				Value: &protobufs.AnyValue{
+					Value: &protobufs.AnyValue_StringValue{
+						StringValue: runtime.GOOS,
+					},
+				},
+			},
+			{
+				Key: "host.name",
+				Value: &protobufs.AnyValue{
+					Value: &protobufs.AnyValue_StringValue{
+						StringValue: hostname,
+					},
+				},
+			},
+		},
+	}
+}
+
+func (agent *Agent) loadLocalConfig() {
+	var k = koanf.New(".")
+	k.Load(rawbytes.Provider([]byte(localConfig)), yaml.Parser())
+
+	effectiveConfigBytes, err := k.Marshal(yaml.Parser())
+	if err != nil {
+		panic(err)
+	}
+
+	agent.effectiveConfig = string(effectiveConfigBytes)
+	hash := sha256.Sum256(effectiveConfigBytes)
+	agent.effectiveConfigHash = hash[:]
+}
+
+func (agent *Agent) composeEffectiveConfig() *protobufs.EffectiveConfig {
+	return &protobufs.EffectiveConfig{
+		Hash: agent.effectiveConfigHash,
+		ConfigMap: &protobufs.AgentConfigMap{
+			ConfigMap: map[string]*protobufs.AgentConfigFile{
+				"": {Body: []byte(agent.effectiveConfig)},
+			},
+		},
+	}
+}
+
+func (agent *Agent) onRemoteConfig(
+	ctx context.Context,
+	config *protobufs.AgentRemoteConfig,
+) (*protobufs.EffectiveConfig, error) {
+	err := agent.applyRemoteConfig(config)
+	if err != nil {
+		return nil, err
+	}
+	return agent.composeEffectiveConfig(), nil
+}
+
+type agentConfigFileItem struct {
+	name string
+	file *protobufs.AgentConfigFile
+}
+
+type agentConfigFileSlice []agentConfigFileItem
+
+func (a agentConfigFileSlice) Less(i, j int) bool {
+	return a[i].name < a[j].name
+}
+
+func (a agentConfigFileSlice) Swap(i, j int) {
+	t := a[i]
+	a[i] = a[j]
+	a[j] = t
+}
+
+func (a agentConfigFileSlice) Len() int {
+	return len(a)
+}
+
+func (agent *Agent) applyRemoteConfig(config *protobufs.AgentRemoteConfig) error {
+	if config == nil {
+		return nil
+	}
+
+	agent.logger.Debugf("Received remote config from server, hash=%x.", config.ConfigHash)
+
+	// Begin with local config. We will later merge received configs on top of it.
+	var k = koanf.New(".")
+	if err := k.Load(rawbytes.Provider([]byte(localConfig)), yaml.Parser()); err != nil {
+		return err
+	}
+
+	orderedConfigs := agentConfigFileSlice{}
+	for name, file := range config.Config.ConfigMap {
+		if name == "" {
+			// skip instance config
+			continue
+		}
+		orderedConfigs = append(orderedConfigs, agentConfigFileItem{
+			name: name,
+			file: file,
+		})
+	}
+
+	// Sort to make sure the order of merging is stable.
+	sort.Sort(orderedConfigs)
+
+	// Append instance config as the last item.
+	instanceConfig := config.Config.ConfigMap[""]
+	if instanceConfig != nil {
+		orderedConfigs = append(orderedConfigs, agentConfigFileItem{
+			name: "",
+			file: instanceConfig,
+		})
+	}
+
+	// Merge received configs.
+	for _, item := range orderedConfigs {
+		var k2 = koanf.New(".")
+		err := k2.Load(rawbytes.Provider(item.file.Body), yaml.Parser())
+		if err != nil {
+			return fmt.Errorf("cannot parse config named %s: %v", item.name, err)
+		}
+		err = k.Merge(k2)
+		if err != nil {
+			return fmt.Errorf("cannot merge config named %s: %v", item.name, err)
+		}
+	}
+
+	// The merged final result is our effective config.
+	effectiveConfigBytes, err := k.Marshal(yaml.Parser())
+	if err != nil {
+		panic(err)
+	}
+
+	newEffectiveConfig := string(effectiveConfigBytes)
+	if agent.effectiveConfig != newEffectiveConfig {
+		agent.logger.Debugf("Effective config changed. Need to report to server.")
+		agent.effectiveConfig = newEffectiveConfig
+		hash := sha256.Sum256(effectiveConfigBytes)
+		agent.effectiveConfigHash = hash[:]
+	}
+
+	agent.remoteConfigHash = config.ConfigHash
+
+	return nil
+}
+
+func (agent *Agent) Shutdown() {
+	agent.logger.Debugf("Agent shutting down...")
+	if agent.opampClient != nil {
+		agent.opampClient.Stop(context.Background())
+	}
+}
diff --git a/internal/examples/agent/agent/logger.go b/internal/examples/agent/agent/logger.go
new file mode 100644
index 0000000..d161f34
--- /dev/null
+++ b/internal/examples/agent/agent/logger.go
@@ -0,0 +1,15 @@
+package agent
+
+import "log"
+
+type Logger struct {
+	Logger *log.Logger
+}
+
+func (l *Logger) Debugf(format string, v ...interface{}) {
+	l.Logger.Printf(format, v...)
+}
+
+func (l *Logger) Errorf(format string, v ...interface{}) {
+	l.Logger.Printf(format, v...)
+}
diff --git a/internal/examples/agent/main.go b/internal/examples/agent/main.go
new file mode 100644
index 0000000..7b8bc98
--- /dev/null
+++ b/internal/examples/agent/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"os"
+	"os/signal"
+
+	"github.com/open-telemetry/opamp-go/internal/examples/agent/agent"
+)
+
+func main() {
+	var agentType string
+	flag.StringVar(&agentType, "t", "io.opentelemetry.collector", "Agent Type String")
+
+	var agentVersion string
+	flag.StringVar(&agentVersion, "v", "1.0.0", "Agent Version String")
+
+	flag.Parse()
+
+	agent := agent.NewAgent(&agent.Logger{log.Default()}, agentType, agentVersion)
+
+	interrupt := make(chan os.Signal, 1)
+	signal.Notify(interrupt, os.Interrupt)
+	<-interrupt
+	agent.Shutdown()
+}
diff --git a/internal/examples/server/data/agents.go b/internal/examples/server/data/agents.go
index 6efbb36..978ac3c 100644
--- a/internal/examples/server/data/agents.go
+++ b/internal/examples/server/data/agents.go
@@ -4,6 +4,7 @@ import (
 	"sync"
 
 	"github.com/open-telemetry/opamp-go/protobufs"
+	"github.com/open-telemetry/opamp-go/protobufshelpers"
 	"github.com/open-telemetry/opamp-go/server/types"
 )
 
@@ -51,7 +52,7 @@ func isEqualAgentDescr(d1, d2 *protobufs.AgentDescription) bool {
 	}
 	for i, a1 := range d1.AgentAttributes {
 		a2 := d2.AgentAttributes[i]
-		if !isEqualKeyValue(a1, a2) {
+		if !protobufshelpers.IsEqualKeyValue(a1, a2) {
 			return false
 		}
 	}
@@ -71,7 +72,7 @@ func (agents *Agents) FindOrCreateAgent(agentId InstanceId, conn types.Connectio
 	// Ensure the agent is in the agentsById map.
 	agent := agents.agentsById[agentId]
 	if agent == nil {
-		agent := NewAgent(agentId, conn)
+		agent = NewAgent(agentId, conn)
 		agents.agentsById[agentId] = agent
 
 		// Ensure the agent's instance id is associated with the connection.
@@ -113,5 +114,6 @@ func (agents *Agents) GetAllAgentsReadonlyClone() map[InstanceId]*Agent {
 }
 
 var AllAgents = Agents{
-	agentsById: map[InstanceId]*Agent{},
+	agentsById:  map[InstanceId]*Agent{},
+	connections: map[types.Connection]map[InstanceId]bool{},
 }
diff --git a/internal/examples/server/uisrv/html/agent.html b/internal/examples/server/uisrv/html/agent.html
index 8bf5b8c..6ff3e27 100644
--- a/internal/examples/server/uisrv/html/agent.html
+++ b/internal/examples/server/uisrv/html/agent.html
@@ -57,7 +57,7 @@
             Additional Configuration:<br/>
             <form action="/save_config" method="post">
             <input type="hidden" name="instanceid" value="{{ .InstanceId }}"/>
-            <textarea cols="40" rows="20" name="config">{{ .CustomConfigForInstance }}</textarea><br/>
+            <textarea cols="40" rows="20" name="config">{{ .CustomInstanceConfig }}</textarea><br/>
              {{if .Status.RemoteConfigStatus }}
                 {{if .Status.RemoteConfigStatus.ErrorMessage }}
                 <span style="color:red">Failed: {{ .Status.RemoteConfigStatus.ErrorMessage }}</span><br/>
diff --git a/makefile b/makefile
index 2098354..63b181d 100644
--- a/makefile
+++ b/makefile
@@ -8,16 +8,26 @@ endef
 # Find all .proto files.
 BASELINE_PROTO_FILES := $(wildcard internal/proto/*.proto)
 
-all: test examples
+all: test build-examples
 
 .PHONY: test
 test:
 	go test -race ./...
 
-.PHONY: examples
-examples:
+.PHONY: build-examples
+build-examples: build-example-agent build-example-server
+
+build-example-agent:
+	go build -o internal/examples/agent/bin/agent internal/examples/agent/main.go
+
+build-example-server:
 	go build -o internal/examples/server/bin/server internal/examples/server/main.go
 
+run-examples: build-examples
+	cd internal/examples/server && ./bin/server &
+	@echo Server UI is running at http://localhost:4321/
+	cd internal/examples/agent && ./bin/agent
+
 # Generate Protobuf Go files.
 .PHONY: gen-proto
 gen-proto:
diff --git a/protobufshelpers/anyvaluehelpers.go b/protobufshelpers/anyvaluehelpers.go
new file mode 100644
index 0000000..898fd99
--- /dev/null
+++ b/protobufshelpers/anyvaluehelpers.go
@@ -0,0 +1,91 @@
+package protobufshelpers
+
+import (
+	"bytes"
+
+	"github.com/open-telemetry/opamp-go/protobufs"
+)
+
+func IsEqualAnyValue(v1, v2 *protobufs.AnyValue) bool {
+	if v1 == v2 {
+		return true
+	}
+	if v1 == nil || v2 == nil {
+		return false
+	}
+	if v1.Value == v2.Value {
+		return true
+	}
+	if v1.Value == nil || v2.Value == nil {
+		return false
+	}
+
+	switch v1 := v1.Value.(type) {
+	case *protobufs.AnyValue_StringValue:
+		v2, ok := v2.Value.(*protobufs.AnyValue_StringValue)
+		return ok && v1.StringValue == v2.StringValue
+
+	case *protobufs.AnyValue_IntValue:
+		v2, ok := v2.Value.(*protobufs.AnyValue_IntValue)
+		return ok && v1.IntValue == v2.IntValue
+
+	case *protobufs.AnyValue_BoolValue:
+		v2, ok := v2.Value.(*protobufs.AnyValue_BoolValue)
+		return ok && v1.BoolValue == v2.BoolValue
+
+	case *protobufs.AnyValue_DoubleValue:
+		v2, ok := v2.Value.(*protobufs.AnyValue_DoubleValue)
+		return ok && v1.DoubleValue == v2.DoubleValue
+
+	case *protobufs.AnyValue_BytesValue:
+		v2, ok := v2.Value.(*protobufs.AnyValue_BytesValue)
+		return ok && bytes.Compare(v1.BytesValue, v2.BytesValue) == 0
+
+	case *protobufs.AnyValue_ArrayValue:
+		v2, ok := v2.Value.(*protobufs.AnyValue_ArrayValue)
+		if !ok || v1.ArrayValue == nil || v2.ArrayValue == nil ||
+			len(v1.ArrayValue.Values) != len(v2.ArrayValue.Values) {
+			return false
+		}
+		for i, e1 := range v1.ArrayValue.Values {
+			e2 := v2.ArrayValue.Values[i]
+			if e1 == e2 {
+				return true
+			}
+			if e1 == nil || e2 == nil {
+				return false
+			}
+			if IsEqualAnyValue(e1, e2) {
+				return false
+			}
+		}
+		return true
+
+	case *protobufs.AnyValue_KvlistValue:
+		v2, ok := v2.Value.(*protobufs.AnyValue_KvlistValue)
+		if !ok || v1.KvlistValue == nil || v2.KvlistValue == nil ||
+			len(v1.KvlistValue.Values) != len(v2.KvlistValue.Values) {
+			return false
+		}
+		for i, e1 := range v1.KvlistValue.Values {
+			e2 := v2.KvlistValue.Values[i]
+			if IsEqualKeyValue(e1, e2) {
+				return false
+			}
+		}
+		return true
+	}
+
+	return true
+}
+
+func IsEqualKeyValue(kv1, kv2 *protobufs.KeyValue) bool {
+	if kv1 == kv2 {
+		return true
+	}
+	if kv1 == nil || kv2 == nil {
+		return false
+	}
+
+	return kv1.Key == kv2.Key && IsEqualAnyValue(kv1.Value, kv2.Value)
+}
diff --git a/server/callbacks.go b/server/callbacks.go
index e8dc6be..7864701 100644
--- a/server/callbacks.go
+++ b/server/callbacks.go
@@ -20,7 +20,7 @@ func (c CallbacksStruct) OnConnecting(request *http.Request) types.ConnectionRes
 	if c.OnConnectingFunc != nil {
 		return c.OnConnectingFunc(request)
 	}
-	return types.ConnectionResponse{}
+	return types.ConnectionResponse{Accept: true}
 }
 
 func (c CallbacksStruct) OnConnected(conn types.Connection) {


Test patch:
diff --git a/client/clientimpl_test.go b/client/clientimpl_test.go
index 89fef27..8d81104 100644
--- a/client/clientimpl_test.go
+++ b/client/clientimpl_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/gorilla/websocket"
 	ulid "github.com/oklog/ulid/v2"
+	"github.com/open-telemetry/opamp-go/protobufshelpers"
 	"github.com/stretchr/testify/assert"
 	"google.golang.org/protobuf/proto"
 
@@ -347,6 +348,86 @@ func TestSetEffectiveConfig(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func isEqualAgentAttrs(set map[string]*protobufs.AnyValue, received []*protobufs.KeyValue) bool {
+	if len(set) != len(received) {
+		return false
+	}
+
+	for _, kv := range received {
+		val, exists := set[kv.Key]
+		if !exists {
+			return false
+		}
+		if !protobufshelpers.IsEqualAnyValue(kv.Value, val) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func TestSetAgentAttributes(t *testing.T) {
+	// Start a server.
+	srv := internal.StartMockServer(t)
+	var rcvAgentDescr atomic.Value
+	srv.OnMessage = func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent {
+		if statusReport := msg.GetStatusReport(); statusReport != nil {
+			if statusReport.AgentDescription != nil {
+				rcvAgentDescr.Store(statusReport.AgentDescription)
+			}
+		}
+		return nil
+	}
+
+	// Start a client.
+	settings := StartSettings{}
+	settings.OpAMPServerURL = "ws://" + srv.Endpoint
+	client := prepareClient(settings)
+
+	sendAttrs := map[string]*protobufs.AnyValue{
+		"host.name": {
+			Value: &protobufs.AnyValue_StringValue{StringValue: "somehost"},
+		},
+	}
+
+	// Set before start. It should be delivered immediately after Start().
+	client.SetAgentAttributes(sendAttrs)
+
+	assert.NoError(t, client.Start(settings))
+
+	// Verify it is delivered.
+	eventually(t, func() bool {
+		agentDescr := rcvAgentDescr.Load().(*protobufs.AgentDescription)
+		if agentDescr == nil {
+			return false
+		}
+		return isEqualAgentAttrs(sendAttrs, agentDescr.AgentAttributes)
+	})
+
+	// Now change again.
+	sendAttrs["key2"] = &protobufs.AnyValue{
+		Value: &protobufs.AnyValue_StringValue{StringValue: "somehost"},
+	}
+
+	client.SetAgentAttributes(sendAttrs)
+
+	// Verify change is delivered.
+	eventually(t, func() bool {
+		agentDescr := rcvAgentDescr.Load().(*protobufs.AgentDescription)
+		if agentDescr == nil {
+			return false
+		}
+		return isEqualAgentAttrs(sendAttrs, agentDescr.AgentAttributes)
+	})
+
+	// Shutdown the server.
+	srv.Close()
+
+	// Shutdown the client.
+	err := client.Stop(context.Background())
+	assert.NoError(t, err)
+}
+
 func TestConnectionSettings(t *testing.T) {
 	hash := []byte{1, 2, 3}
 	opampSettings := &protobufs.ConnectionSettings{DestinationEndpoint: "http://opamp.com"}