├── internal ├── tools │ ├── empty.go │ ├── tools.go │ └── go.mod ├── examples │ ├── agent │ │ ├── .gitignore │ │ ├── agent │ │ │ ├── logger.go │ │ │ ├── metricreporter.go │ │ │ └── agent.go │ │ └── main.go │ ├── server │ │ ├── .gitignore │ │ ├── data │ │ │ ├── instanceid.go │ │ │ ├── anyvalue.go │ │ │ └── agents.go │ │ ├── uisrv │ │ │ ├── html │ │ │ │ ├── header.html │ │ │ │ ├── root.html │ │ │ │ └── agent.html │ │ │ └── ui.go │ │ ├── opampsrv │ │ │ ├── logger.go │ │ │ └── opampsrv.go │ │ └── main.go │ ├── supervisor │ │ ├── bin │ │ │ ├── .gitignore │ │ │ └── supervisor.yaml │ │ ├── supervisor │ │ │ ├── config │ │ │ │ └── config.go │ │ │ ├── logger.go │ │ │ └── commander │ │ │ │ └── commander.go │ │ └── main.go │ └── go.mod ├── noplogger.go ├── retryafter.go ├── testhelpers │ └── nethelpers.go └── proto │ └── anyvalue.proto ├── client ├── types │ ├── logger.go │ ├── startsettings.go │ ├── packagessyncer.go │ └── callbacks.go ├── internal │ ├── httpsender_test.go │ ├── nextmessage.go │ ├── wsreceiver.go │ ├── wssender.go │ ├── tcpproxy.go │ ├── inmempackagestore.go │ ├── sender.go │ ├── wsreceiver_test.go │ ├── clientstate.go │ ├── mockserver.go │ ├── receivedprocessor.go │ ├── httpsender.go │ ├── packagessyncer.go │ └── clientcommon.go ├── httpclient_test.go ├── httpclient.go ├── client.go ├── wsclient_test.go └── wsclient.go ├── CONTRIBUTING.md ├── .gitignore ├── .github ├── CODEOWNERS └── workflows │ └── build-and-test.yml ├── go.mod ├── server ├── httpconnectioncontext.go ├── types │ ├── connection.go │ └── callbacks.go ├── wsconnection.go ├── httpconnection.go ├── callbacks.go ├── server.go └── serverimpl.go ├── makefile ├── protobufshelpers └── anyvaluehelpers.go ├── README.md ├── go.sum └── LICENSE /internal/tools/empty.go: -------------------------------------------------------------------------------- 1 | package tools 2 | -------------------------------------------------------------------------------- /internal/examples/agent/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | -------------------------------------------------------------------------------- /internal/examples/server/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | -------------------------------------------------------------------------------- /internal/examples/server/data/instanceid.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | type InstanceId string 4 | -------------------------------------------------------------------------------- /internal/examples/supervisor/bin/.gitignore: -------------------------------------------------------------------------------- 1 | agent.log 2 | effective.yaml 3 | supervisor 4 | -------------------------------------------------------------------------------- /internal/examples/server/uisrv/html/header.html: -------------------------------------------------------------------------------- 1 | 2 | OpAMP Server 3 | 4 |

OpAMP Server

-------------------------------------------------------------------------------- /client/types/logger.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Logger interface { 4 | Debugf(format string, v ...interface{}) 5 | Errorf(format string, v ...interface{}) 6 | } 7 | -------------------------------------------------------------------------------- /internal/examples/supervisor/bin/supervisor.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | endpoint: ws://127.0.0.1:4320/v1/opamp 3 | 4 | agent: 5 | executable: ../../../../../opentelemetry-collector-contrib/bin/otelcontribcol_darwin_amd64 6 | -------------------------------------------------------------------------------- /internal/noplogger.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type NopLogger struct{} 4 | 5 | func (l *NopLogger) Debugf(format string, v ...interface{}) {} 6 | func (l *NopLogger) Errorf(format string, v ...interface{}) {} 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to opamp-go library 2 | 3 | ## Generate Protobuf Go Files 4 | 5 | Make sure you have Docker installed. 6 | 7 | Run `make gen-proto`. This should compile `internal/proto/*.proto` files to 8 | `internal/protobufs/*.pb.go` files. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # GoLand IDEA 2 | /.idea/ 3 | *.iml 4 | 5 | # VS Code 6 | .vscode/ 7 | .devcontainer/ 8 | 9 | # Emacs 10 | *~ 11 | \#*\# 12 | 13 | # Miscellaneous files 14 | *.sw[op] 15 | *.DS_Store 16 | 17 | # Coverage 18 | coverage.out 19 | coverage.txt 20 | coverage.html 21 | -------------------------------------------------------------------------------- /internal/examples/supervisor/supervisor/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Supervisor is the Supervisor config file format. 4 | type Supervisor struct { 5 | Server *OpAMPServer 6 | Agent *Agent 7 | } 8 | 9 | type OpAMPServer struct { 10 | Endpoint string 11 | } 12 | 13 | type Agent struct { 14 | Executable string 15 | } 16 | -------------------------------------------------------------------------------- /internal/examples/agent/agent/logger.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import "log" 4 | 5 | type Logger struct { 6 | Logger *log.Logger 7 | } 8 | 9 | func (l *Logger) Debugf(format string, v ...interface{}) { 10 | l.Logger.Printf(format, v...) 11 | } 12 | 13 | func (l *Logger) Errorf(format string, v ...interface{}) { 14 | l.Logger.Printf(format, v...) 15 | } 16 | -------------------------------------------------------------------------------- /internal/examples/server/opampsrv/logger.go: -------------------------------------------------------------------------------- 1 | package opampsrv 2 | 3 | import "log" 4 | 5 | type Logger struct { 6 | logger *log.Logger 7 | } 8 | 9 | func (l *Logger) Debugf(format string, v ...interface{}) { 10 | l.logger.Printf(format, v...) 11 | } 12 | 13 | func (l *Logger) Errorf(format string, v ...interface{}) { 14 | l.logger.Printf(format, v...) 15 | } 16 | -------------------------------------------------------------------------------- /internal/examples/supervisor/supervisor/logger.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | import "log" 4 | 5 | type Logger struct { 6 | Logger *log.Logger 7 | } 8 | 9 | func (l *Logger) Debugf(format string, v ...interface{}) { 10 | l.Logger.Printf(format, v...) 11 | } 12 | 13 | func (l *Logger) Errorf(format string, v ...interface{}) { 14 | l.Logger.Printf(format, v...) 15 | } 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | ##################################################### 2 | # 3 | # List of approvers for opamp-go 4 | # 5 | ##################################################### 6 | # 7 | # Learn about membership in OpenTelemetry community: 8 | # https://github.com/open-telemetry/community/blob/main/community-membership.md 9 | # 10 | # 11 | # Learn about CODEOWNERS file format: 12 | # https://help.github.com/en/articles/about-code-owners 13 | # 14 | 15 | * @open-telemetry/opamp-go-approvers 16 | -------------------------------------------------------------------------------- /internal/examples/server/uisrv/html/root.html: -------------------------------------------------------------------------------- 1 | {{ template "header.html" . }} 2 | 3 |

Agents

4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | {{ range . }} 16 | 17 | 18 | 19 | {{ end }} 20 |
Instance ID
{{ .InstanceId }}
21 | 22 | 23 | -------------------------------------------------------------------------------- /internal/examples/supervisor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/open-telemetry/opamp-go/internal/examples/supervisor/supervisor" 9 | ) 10 | 11 | func main() { 12 | logger := &supervisor.Logger{Logger: log.Default()} 13 | supervisor, err := supervisor.NewSupervisor(logger) 14 | if err != nil { 15 | logger.Errorf(err.Error()) 16 | os.Exit(-1) 17 | return 18 | } 19 | 20 | interrupt := make(chan os.Signal, 1) 21 | signal.Notify(interrupt, os.Interrupt) 22 | <-interrupt 23 | supervisor.Shutdown() 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-telemetry/opamp-go 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/cenkalti/backoff/v4 v4.1.2 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/oklog/ulid/v2 v2.0.2 9 | github.com/stretchr/testify v1.7.0 10 | google.golang.org/protobuf v1.27.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/google/go-cmp v0.5.6 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /internal/examples/agent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/open-telemetry/opamp-go/internal/examples/agent/agent" 10 | ) 11 | 12 | func main() { 13 | var agentType string 14 | flag.StringVar(&agentType, "t", "io.opentelemetry.collector", "Agent Type String") 15 | 16 | var agentVersion string 17 | flag.StringVar(&agentVersion, "v", "1.0.0", "Agent Version String") 18 | 19 | flag.Parse() 20 | 21 | agent := agent.NewAgent(&agent.Logger{log.Default()}, agentType, agentVersion) 22 | 23 | interrupt := make(chan os.Signal, 1) 24 | signal.Notify(interrupt, os.Interrupt) 25 | <-interrupt 26 | agent.Shutdown() 27 | } 28 | -------------------------------------------------------------------------------- /server/httpconnectioncontext.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | ) 8 | 9 | type connContextKeyType struct { 10 | key string 11 | } 12 | 13 | var connContextKey = connContextKeyType{key: "httpconn"} 14 | 15 | func contextWithConn(ctx context.Context, c net.Conn) context.Context { 16 | // Create a new context that stores the net.Conn. For use as ConnContext func 17 | // of http.Server to remember the connection in the context. 18 | return context.WithValue(ctx, connContextKey, c) 19 | } 20 | func connFromRequest(r *http.Request) net.Conn { 21 | // Extract the net.Conn from the context of the specified http.Request. 22 | return r.Context().Value(connContextKey).(net.Conn) 23 | } 24 | -------------------------------------------------------------------------------- /server/types/connection.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/open-telemetry/opamp-go/protobufs" 8 | ) 9 | 10 | // Connection represents one OpAMP connection. 11 | // The implementation MUST be a comparable type so that it can be used as a map key. 12 | type Connection interface { 13 | // RemoteAddr returns the remote network address of the connection. 14 | RemoteAddr() net.Addr 15 | 16 | // Send a message. Should not be called concurrently for the same Connection instance. 17 | // Can be called only for WebSocket connections. Will return an error for plain HTTP 18 | // connections. 19 | // Blocks until the message is sent. 20 | // Should return as soon as possible if the ctx is cancelled. 21 | Send(ctx context.Context, message *protobufs.ServerToAgent) error 22 | } 23 | -------------------------------------------------------------------------------- /server/wsconnection.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/gorilla/websocket" 8 | "google.golang.org/protobuf/proto" 9 | 10 | "github.com/open-telemetry/opamp-go/protobufs" 11 | "github.com/open-telemetry/opamp-go/server/types" 12 | ) 13 | 14 | // wsConnection represents a persistent OpAMP connection over a WebSocket. 15 | type wsConnection struct { 16 | wsConn *websocket.Conn 17 | } 18 | 19 | var _ types.Connection = (*wsConnection)(nil) 20 | 21 | func (c wsConnection) RemoteAddr() net.Addr { 22 | return c.wsConn.RemoteAddr() 23 | } 24 | 25 | func (c wsConnection) Send(_ context.Context, message *protobufs.ServerToAgent) error { 26 | bytes, err := proto.Marshal(message) 27 | if err != nil { 28 | return err 29 | } 30 | return c.wsConn.WriteMessage(websocket.BinaryMessage, bytes) 31 | } 32 | -------------------------------------------------------------------------------- /internal/examples/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/open-telemetry/opamp-go/internal/examples/server/data" 9 | "github.com/open-telemetry/opamp-go/internal/examples/server/opampsrv" 10 | "github.com/open-telemetry/opamp-go/internal/examples/server/uisrv" 11 | ) 12 | 13 | var logger = log.New(log.Default().Writer(), "[MAIN] ", log.Default().Flags()|log.Lmsgprefix|log.Lmicroseconds) 14 | 15 | func main() { 16 | curDir, err := os.Getwd() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | logger.Println("OpAMP Server starting...") 22 | 23 | uisrv.Start(curDir) 24 | opampSrv := opampsrv.NewServer(&data.AllAgents) 25 | opampSrv.Start() 26 | 27 | logger.Println("OpAMP Server running...") 28 | 29 | interrupt := make(chan os.Signal, 1) 30 | signal.Notify(interrupt, os.Interrupt) 31 | <-interrupt 32 | 33 | logger.Println("OpAMP Server shutting down...") 34 | uisrv.Shutdown() 35 | opampSrv.Stop() 36 | } 37 | -------------------------------------------------------------------------------- /server/httpconnection.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | 8 | "github.com/open-telemetry/opamp-go/protobufs" 9 | "github.com/open-telemetry/opamp-go/server/types" 10 | ) 11 | 12 | var errInvalidHTTPConnection = errors.New("cannot Send() over HTTP connection") 13 | 14 | // httpConnection represents an OpAMP connection over a plain HTTP connection. 15 | // Only one response is possible to send when using plain HTTP connection 16 | // and that response will be sent by OpAMP Server's HTTP request handler after the 17 | // onMessage callback returns. 18 | type httpConnection struct { 19 | conn net.Conn 20 | } 21 | 22 | func (c httpConnection) RemoteAddr() net.Addr { 23 | return c.conn.RemoteAddr() 24 | } 25 | 26 | var _ types.Connection = (*httpConnection)(nil) 27 | 28 | func (c httpConnection) Send(_ context.Context, _ *protobufs.ServerToAgent) error { 29 | // Send() should not be called for plain HTTP connection. Instead, the response will 30 | // be sent after the onMessage callback returns. 31 | return errInvalidHTTPConnection 32 | } 33 | -------------------------------------------------------------------------------- /internal/retryafter.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const retryAfterHTTPHeader = "Retry-After" 11 | 12 | type OptionalDuration struct { 13 | Duration time.Duration 14 | // true if duration field is defined. 15 | Defined bool 16 | } 17 | 18 | // ExtractRetryAfterHeader extracts Retry-After response header if the status 19 | // is 503 or 429. Returns 0 duration if the header is not found or the status 20 | // is different. 21 | func ExtractRetryAfterHeader(resp *http.Response) OptionalDuration { 22 | if resp.StatusCode == http.StatusServiceUnavailable || 23 | resp.StatusCode == http.StatusTooManyRequests { 24 | retryAfter := strings.TrimSpace(resp.Header.Get(retryAfterHTTPHeader)) 25 | if retryAfter != "" { 26 | retryIntervalSec, err := strconv.Atoi(retryAfter) 27 | if err == nil { 28 | retryInterval := time.Duration(retryIntervalSec) * time.Second 29 | return OptionalDuration{Defined: true, Duration: retryInterval} 30 | } 31 | } 32 | } 33 | return OptionalDuration{Defined: false} 34 | } 35 | -------------------------------------------------------------------------------- /internal/tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright The OpenTelemetry Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build tools 16 | // +build tools 17 | 18 | package tools 19 | 20 | // This file follows the recommendation at 21 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 22 | // on how to pin tooling dependencies to a go.mod file. 23 | // This ensures that all systems use the same version of tools in addition to regular dependencies. 24 | 25 | import ( 26 | _ "github.com/ory/go-acc" 27 | ) 28 | -------------------------------------------------------------------------------- /internal/tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-telemetry/opamp-go/internal/tools 2 | 3 | go 1.17 4 | 5 | require github.com/ory/go-acc v0.2.8 6 | 7 | require ( 8 | github.com/cespare/xxhash v1.1.0 // indirect 9 | github.com/dgraph-io/ristretto v0.0.2 // indirect 10 | github.com/fsnotify/fsnotify v1.4.9 // indirect 11 | github.com/google/uuid v1.1.1 // indirect 12 | github.com/hashicorp/hcl v1.0.0 // indirect 13 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 14 | github.com/magiconair/properties v1.8.1 // indirect 15 | github.com/mitchellh/mapstructure v1.3.2 // indirect 16 | github.com/ory/viper v1.7.5 // indirect 17 | github.com/pborman/uuid v1.2.0 // indirect 18 | github.com/pelletier/go-toml v1.8.0 // indirect 19 | github.com/spf13/afero v1.2.2 // indirect 20 | github.com/spf13/cast v1.3.1 // indirect 21 | github.com/spf13/cobra v1.0.0 // indirect 22 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 23 | github.com/spf13/pflag v1.0.5 // indirect 24 | github.com/subosito/gotenv v1.2.0 // indirect 25 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect 26 | golang.org/x/text v0.3.2 // indirect 27 | gopkg.in/ini.v1 v1.57.0 // indirect 28 | gopkg.in/yaml.v2 v2.3.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /server/callbacks.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/open-telemetry/opamp-go/protobufs" 7 | "github.com/open-telemetry/opamp-go/server/types" 8 | ) 9 | 10 | type CallbacksStruct struct { 11 | OnConnectingFunc func(request *http.Request) types.ConnectionResponse 12 | OnConnectedFunc func(conn types.Connection) 13 | OnMessageFunc func(conn types.Connection, message *protobufs.AgentToServer) *protobufs.ServerToAgent 14 | OnConnectionCloseFunc func(conn types.Connection) 15 | } 16 | 17 | var _ types.Callbacks = (*CallbacksStruct)(nil) 18 | 19 | func (c CallbacksStruct) OnConnecting(request *http.Request) types.ConnectionResponse { 20 | if c.OnConnectingFunc != nil { 21 | return c.OnConnectingFunc(request) 22 | } 23 | return types.ConnectionResponse{Accept: true} 24 | } 25 | 26 | func (c CallbacksStruct) OnConnected(conn types.Connection) { 27 | if c.OnConnectedFunc != nil { 28 | c.OnConnectedFunc(conn) 29 | } 30 | } 31 | 32 | func (c CallbacksStruct) OnMessage(conn types.Connection, message *protobufs.AgentToServer) *protobufs.ServerToAgent { 33 | if c.OnMessageFunc != nil { 34 | return c.OnMessageFunc(conn, message) 35 | } else { 36 | // We will send an empty response since there is no user-defined callback to handle it. 37 | return &protobufs.ServerToAgent{ 38 | InstanceUid: message.InstanceUid, 39 | } 40 | } 41 | } 42 | 43 | func (c CallbacksStruct) OnConnectionClose(conn types.Connection) { 44 | if c.OnConnectionCloseFunc != nil { 45 | c.OnConnectionCloseFunc(conn) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/types/startsettings.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | 7 | "github.com/open-telemetry/opamp-go/protobufs" 8 | ) 9 | 10 | type StartSettings struct { 11 | // Connection parameters. 12 | 13 | // Server URL. MUST be set. 14 | OpAMPServerURL string 15 | 16 | // Optional additional HTTP headers to send with all HTTP requests. 17 | Header http.Header 18 | 19 | // Optional TLS config for HTTP connection. 20 | TLSConfig *tls.Config 21 | 22 | // Agent information. 23 | InstanceUid string 24 | 25 | // Callbacks that the client will call after Start() returns nil. 26 | Callbacks Callbacks 27 | 28 | // Previously saved state. These will be reported to the Server immediately 29 | // after the connection is established. 30 | 31 | // The remote config status. If nil is passed it will force 32 | // the Server to send a remote config back. 33 | RemoteConfigStatus *protobufs.RemoteConfigStatus 34 | 35 | LastConnectionSettingsHash []byte 36 | 37 | // PackagesStateProvider provides access to the local state of packages. 38 | // If nil then ReportsPackageStatuses and AcceptsPackages capabilities will be disabled, 39 | // i.e. package status reporting and syncing from the Server will be disabled. 40 | PackagesStateProvider PackagesStateProvider 41 | 42 | // Defines the capabilities of the Agent. AgentCapabilities_ReportsStatus bit does not need to 43 | // be set in this field, it will be set automatically since it is required by OpAMP protocol. 44 | Capabilities protobufs.AgentCapabilities 45 | 46 | // EnableCompression can be set to true to enable the compression. Note that for WebSocket transport 47 | // the compression is only effectively enabled if the Server also supports compression. 48 | // The data will be compressed in both directions. 49 | EnableCompression bool 50 | } 51 | -------------------------------------------------------------------------------- /client/internal/httpsender_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | "github.com/open-telemetry/opamp-go/client/types" 11 | sharedinternal "github.com/open-telemetry/opamp-go/internal" 12 | "github.com/open-telemetry/opamp-go/protobufs" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestHTTPSenderRetryForStatusTooManyRequests(t *testing.T) { 17 | 18 | var connectionAttempts int64 19 | srv := StartMockServer(t) 20 | srv.OnRequest = func(w http.ResponseWriter, r *http.Request) { 21 | attempt := atomic.AddInt64(&connectionAttempts, 1) 22 | // Return a Retry-After header with a value of 1 second for first attempt. 23 | if attempt == 1 { 24 | w.Header().Set("Retry-After", "1") 25 | w.WriteHeader(http.StatusTooManyRequests) 26 | } else { 27 | w.WriteHeader(http.StatusOK) 28 | } 29 | } 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | url := "http://" + srv.Endpoint 32 | sender := NewHTTPSender(&sharedinternal.NopLogger{}) 33 | sender.NextMessage().Update(func(msg *protobufs.AgentToServer) { 34 | msg.AgentDescription = &protobufs.AgentDescription{ 35 | IdentifyingAttributes: []*protobufs.KeyValue{{ 36 | Key: "service.name", 37 | Value: &protobufs.AnyValue{ 38 | Value: &protobufs.AnyValue_StringValue{StringValue: "test-service"}, 39 | }, 40 | }}, 41 | } 42 | }) 43 | sender.callbacks = types.CallbacksStruct{ 44 | OnConnectFunc: func() { 45 | }, 46 | OnConnectFailedFunc: func(_ error) { 47 | }, 48 | } 49 | sender.url = url 50 | start := time.Now() 51 | resp, err := sender.sendRequestWithRetries(ctx) 52 | assert.NoError(t, err) 53 | assert.Equal(t, http.StatusOK, resp.StatusCode) 54 | assert.True(t, time.Since(start) > time.Second) 55 | cancel() 56 | srv.Close() 57 | } 58 | -------------------------------------------------------------------------------- /internal/examples/server/opampsrv/opampsrv.go: -------------------------------------------------------------------------------- 1 | package opampsrv 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/open-telemetry/opamp-go/internal/examples/server/data" 8 | "github.com/open-telemetry/opamp-go/protobufs" 9 | "github.com/open-telemetry/opamp-go/server" 10 | "github.com/open-telemetry/opamp-go/server/types" 11 | ) 12 | 13 | type Server struct { 14 | opampSrv server.OpAMPServer 15 | agents *data.Agents 16 | } 17 | 18 | func NewServer(agents *data.Agents) *Server { 19 | srv := &Server{ 20 | agents: agents, 21 | } 22 | 23 | logger := log.New( 24 | log.Default().Writer(), 25 | "[OPAMP] ", 26 | log.Default().Flags()|log.Lmsgprefix|log.Lmicroseconds, 27 | ) 28 | 29 | srv.opampSrv = server.New(&Logger{logger}) 30 | 31 | return srv 32 | } 33 | 34 | func (srv *Server) Start() { 35 | settings := server.StartSettings{ 36 | Settings: server.Settings{ 37 | Callbacks: server.CallbacksStruct{ 38 | OnMessageFunc: srv.onMessage, 39 | OnConnectionCloseFunc: srv.onDisconnect, 40 | }, 41 | }, 42 | ListenEndpoint: "127.0.0.1:4320", 43 | } 44 | 45 | srv.opampSrv.Start(settings) 46 | } 47 | 48 | func (srv *Server) Stop() { 49 | srv.opampSrv.Stop(context.Background()) 50 | } 51 | 52 | func (srv *Server) onDisconnect(conn types.Connection) { 53 | srv.agents.RemoveConnection(conn) 54 | } 55 | 56 | func (srv *Server) onMessage(conn types.Connection, msg *protobufs.AgentToServer) *protobufs.ServerToAgent { 57 | instanceId := data.InstanceId(msg.InstanceUid) 58 | 59 | agent := srv.agents.FindOrCreateAgent(instanceId, conn) 60 | 61 | // Start building the response. 62 | response := &protobufs.ServerToAgent{} 63 | 64 | // Process the status report and continue building the response. 65 | agent.UpdateStatus(msg, response) 66 | 67 | // Send the response back to the Agent. 68 | return response 69 | } 70 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | 8 | "github.com/open-telemetry/opamp-go/server/types" 9 | ) 10 | 11 | type Settings struct { 12 | // Callbacks that the Server will call after successful Attach/Start. 13 | Callbacks types.Callbacks 14 | } 15 | 16 | type StartSettings struct { 17 | Settings 18 | 19 | // ListenEndpoint specifies the endpoint to listen on, e.g. "127.0.0.1:4320" 20 | ListenEndpoint string 21 | 22 | // ListenPath specifies the URL path on which to accept the OpAMP connections 23 | // If this is empty string then Start() will use the default "/v1/opamp" path. 24 | ListenPath string 25 | 26 | // Server's TLS configuration. 27 | TLSConfig *tls.Config 28 | } 29 | 30 | type HTTPHandlerFunc func(http.ResponseWriter, *http.Request) 31 | 32 | type OpAMPServer interface { 33 | // Attach prepares the OpAMP Server to begin handling requests from an existing 34 | // http.Server. The returned HTTPHandlerFunc should be added as a handler to the 35 | // desired http.Server by the caller and the http.Server should be started by 36 | // the caller after that. 37 | // For example: 38 | // handler, _ := Server.Attach() 39 | // mux := http.NewServeMux() 40 | // mux.HandleFunc("/opamp", handler) 41 | // httpSrv := &http.Server{Handler:mux,Addr:"127.0.0.1:4320"} 42 | // httpSrv.ListenAndServe() 43 | Attach(settings Settings) (HTTPHandlerFunc, error) 44 | 45 | // Start an OpAMP Server and begin accepting connections. Starts its own http.Server 46 | // using provided settings. This should block until the http.Server is ready to 47 | // accept connections. 48 | Start(settings StartSettings) error 49 | 50 | // Stop accepting new connections and close all current connections. This should 51 | // block until all connections are closed. 52 | Stop(ctx context.Context) error 53 | } 54 | -------------------------------------------------------------------------------- /server/types/callbacks.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/open-telemetry/opamp-go/protobufs" 7 | ) 8 | 9 | // ConnectionResponse is the return type of the OnConnecting callback. 10 | type ConnectionResponse struct { 11 | Accept bool 12 | HTTPStatusCode int 13 | HTTPResponseHeader map[string]string 14 | } 15 | 16 | type Callbacks interface { 17 | // The following callbacks will never be called concurrently for the same 18 | // connection. They may be called concurrently for different connections. 19 | 20 | // OnConnecting is called when there is a new incoming connection. 21 | // The handler can examine the request and either accept or reject the connection. 22 | // To accept: 23 | // Return ConnectionResponse with Accept=true. 24 | // HTTPStatusCode and HTTPResponseHeader are ignored. 25 | // 26 | // To reject: 27 | // Return ConnectionResponse with Accept=false. HTTPStatusCode MUST be set to 28 | // non-zero value to indicate the rejection reason (typically 401, 429 or 503). 29 | // HTTPResponseHeader may be optionally set (e.g. "Retry-After: 30"). 30 | OnConnecting(request *http.Request) ConnectionResponse 31 | 32 | // OnConnected is called when and incoming OpAMP connection is successfully 33 | // established after OnConnecting() returns. 34 | OnConnected(conn Connection) 35 | 36 | // OnMessage is called when a message is received from the connection. Can happen 37 | // only after OnConnected(). Must return a ServerToAgent message that will be sent 38 | // as a response to the Agent. 39 | // For plain HTTP requests once OnMessage returns and the response is sent 40 | // to the Agent the OnConnectionClose message will be called immediately. 41 | OnMessage(conn Connection, message *protobufs.AgentToServer) *protobufs.ServerToAgent 42 | 43 | // OnConnectionClose is called when the OpAMP connection is closed. 44 | OnConnectionClose(conn Connection) 45 | } 46 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Function to execute a command. 2 | # Accepts command to execute as first parameter. 3 | define exec-command 4 | $(1) 5 | 6 | endef 7 | 8 | TOOLS_MOD_DIR := ./internal/tools 9 | 10 | # Find all .proto files. 11 | BASELINE_PROTO_FILES := $(wildcard internal/proto/*.proto) 12 | 13 | all: test build-examples 14 | 15 | .PHONY: test 16 | test: 17 | go test -race ./... 18 | cd internal/examples && go test -race ./... 19 | 20 | .PHONY: test-with-cover 21 | test-with-cover: 22 | go-acc --output=coverage.out --ignore=protobufs ./... 23 | 24 | show-coverage: test-with-cover 25 | # Show coverage as HTML in the default browser. 26 | go tool cover -html=coverage.out 27 | 28 | .PHONY: build-examples 29 | build-examples: build-example-agent build-example-supervisor build-example-server 30 | 31 | build-example-agent: 32 | cd internal/examples && go build -o agent/bin/agent agent/main.go 33 | 34 | build-example-supervisor: 35 | cd internal/examples && go build -o supervisor/bin/supervisor supervisor/main.go 36 | 37 | build-example-server: 38 | cd internal/examples && go build -o server/bin/server server/main.go 39 | 40 | run-examples: build-examples 41 | cd internal/examples/server && ./bin/server & 42 | @echo Server UI is running at http://localhost:4321/ 43 | cd internal/examples/agent && ./bin/agent 44 | 45 | # Generate Protobuf Go files. 46 | .PHONY: gen-proto 47 | gen-proto: 48 | $(foreach file,$(BASELINE_PROTO_FILES),$(call exec-command,docker run --rm -v${PWD}:${PWD} \ 49 | -w${PWD} otel/build-protobuf:latest --proto_path=${PWD}/internal/proto/ \ 50 | --go_out=${PWD}/internal/proto/ -I${PWD}/internal/proto/ ${PWD}/$(file))) 51 | 52 | cp -R internal/proto/github.com/open-telemetry/opamp-go/protobufs/* protobufs/ 53 | rm -rf internal/proto/github.com/ 54 | 55 | .PHONY: gomoddownload 56 | gomoddownload: 57 | go mod download 58 | 59 | .PHONY: install-tools 60 | install-tools: 61 | cd $(TOOLS_MOD_DIR) && go install github.com/ory/go-acc 62 | -------------------------------------------------------------------------------- /internal/testhelpers/nethelpers.go: -------------------------------------------------------------------------------- 1 | package testhelpers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // GetAvailableLocalAddress finds an available local port and returns an endpoint 14 | // describing it. The port is available for opening when this function returns 15 | // provided that there is no race by some other code to grab the same port 16 | // immediately. 17 | func GetAvailableLocalAddress() string { 18 | ln, err := net.Listen("tcp", "127.0.0.1:") 19 | if err != nil { 20 | log.Fatalf("failed to get a free local port: %v", err) 21 | } 22 | // There is a possible race if something else takes this same port before 23 | // the test uses it, however, that is unlikely in practice. 24 | defer ln.Close() 25 | return ln.Addr().String() 26 | } 27 | 28 | func waitForPortToListen(port int) error { 29 | totalDuration := 5 * time.Second 30 | wait := 10 * time.Millisecond 31 | address := fmt.Sprintf("127.0.0.1:%d", port) 32 | 33 | ticker := time.NewTicker(wait) 34 | defer ticker.Stop() 35 | 36 | timeout := time.After(totalDuration) 37 | 38 | for { 39 | select { 40 | case <-ticker.C: 41 | conn, err := net.Dial("tcp", address) 42 | if err == nil && conn != nil { 43 | conn.Close() 44 | return nil 45 | } 46 | 47 | case <-timeout: 48 | return fmt.Errorf("failed to wait for port %d", port) 49 | } 50 | } 51 | } 52 | 53 | // HostPortFromAddr extracts host and port from a network address 54 | func HostPortFromAddr(endpoint string) (host string, port int, err error) { 55 | sepIndex := strings.LastIndex(endpoint, ":") 56 | if sepIndex < 0 { 57 | return "", -1, errors.New("failed to parse host:port") 58 | } 59 | host, portStr := endpoint[:sepIndex], endpoint[sepIndex+1:] 60 | port, err = strconv.Atoi(portStr) 61 | return host, port, err 62 | } 63 | 64 | func WaitForEndpoint(endpoint string) { 65 | _, port, err := HostPortFromAddr(endpoint) 66 | if err != nil { 67 | log.Fatalln(err) 68 | } 69 | waitForPortToListen(port) 70 | } 71 | -------------------------------------------------------------------------------- /client/internal/nextmessage.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/open-telemetry/opamp-go/protobufs" 7 | "google.golang.org/protobuf/proto" 8 | ) 9 | 10 | // NextMessage encapsulates the next message to be sent and provides a 11 | // concurrency-safe interface to work with the message. 12 | type NextMessage struct { 13 | // The next message to send. 14 | nextMessage *protobufs.AgentToServer 15 | // Indicates that nextMessage is pending to be sent. 16 | messagePending bool 17 | // Mutex to protect the above 2 fields. 18 | messageMutex sync.Mutex 19 | } 20 | 21 | // NewNextMessage returns a new empty NextMessage. 22 | func NewNextMessage() NextMessage { 23 | return NextMessage{ 24 | nextMessage: &protobufs.AgentToServer{}, 25 | } 26 | } 27 | 28 | // Update applies the specified modifier function to the next message that 29 | // will be sent and marks the message as pending to be sent. 30 | func (s *NextMessage) Update(modifier func(msg *protobufs.AgentToServer)) { 31 | s.messageMutex.Lock() 32 | modifier(s.nextMessage) 33 | s.messagePending = true 34 | s.messageMutex.Unlock() 35 | } 36 | 37 | // PopPending returns the next message to be sent, if it is pending or nil otherwise. 38 | // Clears the "pending" flag. 39 | func (s *NextMessage) PopPending() *protobufs.AgentToServer { 40 | var msgToSend *protobufs.AgentToServer 41 | s.messageMutex.Lock() 42 | if s.messagePending { 43 | // Clone the message to have a copy for sending and avoid blocking 44 | // future updates to s.NextMessage field. 45 | msgToSend = proto.Clone(s.nextMessage).(*protobufs.AgentToServer) 46 | s.messagePending = false 47 | 48 | // Reset fields that we do not have to send unless they change before the 49 | // next report after this one. 50 | msg := &protobufs.AgentToServer{ 51 | InstanceUid: s.nextMessage.InstanceUid, 52 | // Increment the sequence number. 53 | SequenceNum: s.nextMessage.SequenceNum + 1, 54 | Capabilities: s.nextMessage.Capabilities, 55 | } 56 | 57 | s.nextMessage = msg 58 | } 59 | s.messageMutex.Unlock() 60 | return msgToSend 61 | } 62 | -------------------------------------------------------------------------------- /client/internal/wsreceiver.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gorilla/websocket" 8 | "google.golang.org/protobuf/proto" 9 | 10 | "github.com/open-telemetry/opamp-go/client/types" 11 | "github.com/open-telemetry/opamp-go/protobufs" 12 | ) 13 | 14 | // wsReceiver implements the WebSocket client's receiving portion of OpAMP protocol. 15 | type wsReceiver struct { 16 | conn *websocket.Conn 17 | logger types.Logger 18 | sender *WSSender 19 | callbacks types.Callbacks 20 | processor receivedProcessor 21 | } 22 | 23 | // NewWSReceiver creates a new Receiver that uses WebSocket to receive 24 | // messages from the server. 25 | func NewWSReceiver( 26 | logger types.Logger, 27 | callbacks types.Callbacks, 28 | conn *websocket.Conn, 29 | sender *WSSender, 30 | clientSyncedState *ClientSyncedState, 31 | packagesStateProvider types.PackagesStateProvider, 32 | capabilities protobufs.AgentCapabilities, 33 | ) *wsReceiver { 34 | w := &wsReceiver{ 35 | conn: conn, 36 | logger: logger, 37 | sender: sender, 38 | callbacks: callbacks, 39 | processor: newReceivedProcessor(logger, callbacks, sender, clientSyncedState, packagesStateProvider, capabilities), 40 | } 41 | 42 | return w 43 | } 44 | 45 | // ReceiverLoop runs the receiver loop. To stop the receiver cancel the context. 46 | func (r *wsReceiver) ReceiverLoop(ctx context.Context) { 47 | runContext, cancelFunc := context.WithCancel(ctx) 48 | 49 | out: 50 | for { 51 | var message protobufs.ServerToAgent 52 | if err := r.receiveMessage(&message); err != nil { 53 | if ctx.Err() == nil && !websocket.IsCloseError(err, websocket.CloseNormalClosure) { 54 | r.logger.Errorf("Unexpected error while receiving: %v", err) 55 | } 56 | break out 57 | } else { 58 | r.processor.ProcessReceivedMessage(runContext, &message) 59 | } 60 | } 61 | 62 | cancelFunc() 63 | } 64 | 65 | func (r *wsReceiver) receiveMessage(msg *protobufs.ServerToAgent) error { 66 | _, bytes, err := r.conn.ReadMessage() 67 | if err != nil { 68 | return err 69 | } 70 | err = proto.Unmarshal(bytes, msg) 71 | if err != nil { 72 | return fmt.Errorf("cannot decode received message: %w", err) 73 | } 74 | return err 75 | } 76 | -------------------------------------------------------------------------------- /internal/examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-telemetry/opamp-go/internal/examples 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/knadh/koanf v1.3.3 7 | github.com/oklog/ulid/v2 v2.0.2 8 | github.com/open-telemetry/opamp-go v0.1.0 9 | github.com/shirou/gopsutil v3.21.11+incompatible 10 | go.opentelemetry.io/otel v1.3.0 11 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.26.0 12 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.26.0 13 | go.opentelemetry.io/otel/metric v0.26.0 14 | go.opentelemetry.io/otel/sdk v1.3.0 15 | go.opentelemetry.io/otel/sdk/metric v0.26.0 16 | google.golang.org/protobuf v1.27.1 17 | ) 18 | 19 | require ( 20 | github.com/cenkalti/backoff/v4 v4.1.2 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/fsnotify/fsnotify v1.4.9 // indirect 23 | github.com/go-logr/logr v1.2.1 // indirect 24 | github.com/go-logr/stdr v1.2.0 // indirect 25 | github.com/go-ole/go-ole v1.2.6 // indirect 26 | github.com/golang/protobuf v1.5.2 // indirect 27 | github.com/gorilla/websocket v1.4.2 // indirect 28 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 29 | github.com/mitchellh/copystructure v1.2.0 // indirect 30 | github.com/mitchellh/mapstructure v1.4.1 // indirect 31 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | github.com/stretchr/testify v1.7.0 // indirect 34 | github.com/tklauser/go-sysconf v0.3.9 // indirect 35 | github.com/tklauser/numcpus v0.3.0 // indirect 36 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 37 | go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 // indirect 38 | go.opentelemetry.io/otel/internal/metric v0.26.0 // indirect 39 | go.opentelemetry.io/otel/sdk/export/metric v0.26.0 // indirect 40 | go.opentelemetry.io/otel/trace v1.3.0 // indirect 41 | go.opentelemetry.io/proto/otlp v0.11.0 // indirect 42 | golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect 43 | golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 // indirect 44 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db // indirect 45 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect 46 | google.golang.org/grpc v1.42.0 // indirect 47 | gopkg.in/yaml.v2 v2.4.0 // indirect 48 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 49 | ) 50 | 51 | replace github.com/open-telemetry/opamp-go => ../../ 52 | -------------------------------------------------------------------------------- /client/internal/wssender.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gorilla/websocket" 7 | "google.golang.org/protobuf/proto" 8 | 9 | "github.com/open-telemetry/opamp-go/client/types" 10 | "github.com/open-telemetry/opamp-go/protobufs" 11 | ) 12 | 13 | // WSSender implements the WebSocket client's sending portion of OpAMP protocol. 14 | type WSSender struct { 15 | SenderCommon 16 | conn *websocket.Conn 17 | logger types.Logger 18 | // Indicates that the sender has fully stopped. 19 | stopped chan struct{} 20 | } 21 | 22 | // NewSender creates a new Sender that uses WebSocket to send 23 | // messages to the server. 24 | func NewSender(logger types.Logger) *WSSender { 25 | return &WSSender{ 26 | logger: logger, 27 | SenderCommon: NewSenderCommon(), 28 | } 29 | } 30 | 31 | // Start the sender and send the first message that was set via NextMessage().Update() 32 | // earlier. To stop the WSSender cancel the ctx. 33 | func (s *WSSender) Start(ctx context.Context, conn *websocket.Conn) error { 34 | s.conn = conn 35 | err := s.sendNextMessage() 36 | 37 | // Run the sender in the background. 38 | s.stopped = make(chan struct{}) 39 | go s.run(ctx) 40 | 41 | return err 42 | } 43 | 44 | // WaitToStop blocks until the sender is stopped. To stop the sender cancel the context 45 | // that was passed to Start(). 46 | func (s *WSSender) WaitToStop() { 47 | <-s.stopped 48 | } 49 | 50 | func (s *WSSender) run(ctx context.Context) { 51 | out: 52 | for { 53 | select { 54 | case <-s.hasPendingMessage: 55 | s.sendNextMessage() 56 | 57 | case <-ctx.Done(): 58 | break out 59 | } 60 | } 61 | 62 | close(s.stopped) 63 | } 64 | 65 | func (s *WSSender) sendNextMessage() error { 66 | msgToSend := s.nextMessage.PopPending() 67 | if msgToSend != nil && !proto.Equal(msgToSend, &protobufs.AgentToServer{}) { 68 | // There is a pending message and the message has some fields populated. 69 | return s.sendMessage(msgToSend) 70 | } 71 | return nil 72 | } 73 | 74 | func (s *WSSender) sendMessage(msg *protobufs.AgentToServer) error { 75 | data, err := proto.Marshal(msg) 76 | if err != nil { 77 | s.logger.Errorf("Cannot marshal data: %v", err) 78 | return err 79 | } 80 | err = s.conn.WriteMessage(websocket.BinaryMessage, data) 81 | if err != nil { 82 | s.logger.Errorf("Cannot send: %v", err) 83 | // TODO: propagate error back to Client and reconnect. 84 | } 85 | return err 86 | } 87 | -------------------------------------------------------------------------------- /internal/examples/server/data/anyvalue.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/open-telemetry/opamp-go/protobufs" 7 | ) 8 | 9 | func isEqualAnyValue(v1, v2 *protobufs.AnyValue) bool { 10 | if v1 == v2 { 11 | return true 12 | } 13 | if v1 == nil || v2 == nil { 14 | return false 15 | } 16 | if v1.Value == v2.Value { 17 | return true 18 | } 19 | if v1.Value == nil || v2.Value == nil { 20 | return false 21 | } 22 | 23 | switch v1 := v1.Value.(type) { 24 | case *protobufs.AnyValue_StringValue: 25 | v2, ok := v2.Value.(*protobufs.AnyValue_StringValue) 26 | return ok && v1.StringValue == v2.StringValue 27 | 28 | case *protobufs.AnyValue_IntValue: 29 | v2, ok := v2.Value.(*protobufs.AnyValue_IntValue) 30 | return ok && v1.IntValue == v2.IntValue 31 | 32 | case *protobufs.AnyValue_BoolValue: 33 | v2, ok := v2.Value.(*protobufs.AnyValue_BoolValue) 34 | return ok && v1.BoolValue == v2.BoolValue 35 | 36 | case *protobufs.AnyValue_DoubleValue: 37 | v2, ok := v2.Value.(*protobufs.AnyValue_DoubleValue) 38 | return ok && v1.DoubleValue == v2.DoubleValue 39 | 40 | case *protobufs.AnyValue_BytesValue: 41 | v2, ok := v2.Value.(*protobufs.AnyValue_BytesValue) 42 | return ok && bytes.Compare(v1.BytesValue, v2.BytesValue) == 0 43 | 44 | case *protobufs.AnyValue_ArrayValue: 45 | v2, ok := v2.Value.(*protobufs.AnyValue_ArrayValue) 46 | if !ok || v1.ArrayValue == nil || v2.ArrayValue == nil || 47 | len(v1.ArrayValue.Values) != len(v2.ArrayValue.Values) { 48 | return false 49 | } 50 | for i, e1 := range v1.ArrayValue.Values { 51 | e2 := v2.ArrayValue.Values[i] 52 | if e1 == e2 { 53 | return true 54 | } 55 | if e1 == nil || e2 == nil { 56 | return false 57 | } 58 | if isEqualAnyValue(e1, e2) { 59 | return false 60 | } 61 | } 62 | return true 63 | 64 | case *protobufs.AnyValue_KvlistValue: 65 | v2, ok := v2.Value.(*protobufs.AnyValue_KvlistValue) 66 | if !ok || v1.KvlistValue == nil || v2.KvlistValue == nil || 67 | len(v1.KvlistValue.Values) != len(v2.KvlistValue.Values) { 68 | return false 69 | } 70 | for i, e1 := range v1.KvlistValue.Values { 71 | e2 := v2.KvlistValue.Values[i] 72 | if isEqualKeyValue(e1, e2) { 73 | return false 74 | } 75 | } 76 | return true 77 | } 78 | 79 | return true 80 | } 81 | 82 | func isEqualKeyValue(kv1, kv2 *protobufs.KeyValue) bool { 83 | if kv1 == kv2 { 84 | return true 85 | } 86 | if kv1 == nil || kv2 == nil { 87 | return false 88 | } 89 | 90 | return kv1.Key == kv2.Key && isEqualAnyValue(kv1.Value, kv2.Value) 91 | } 92 | -------------------------------------------------------------------------------- /protobufshelpers/anyvaluehelpers.go: -------------------------------------------------------------------------------- 1 | package protobufshelpers 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/open-telemetry/opamp-go/protobufs" 7 | ) 8 | 9 | func IsEqualAnyValue(v1, v2 *protobufs.AnyValue) bool { 10 | if v1 == v2 { 11 | return true 12 | } 13 | if v1 == nil || v2 == nil { 14 | return false 15 | } 16 | if v1.Value == v2.Value { 17 | return true 18 | } 19 | if v1.Value == nil || v2.Value == nil { 20 | return false 21 | } 22 | 23 | switch v1 := v1.Value.(type) { 24 | case *protobufs.AnyValue_StringValue: 25 | v2, ok := v2.Value.(*protobufs.AnyValue_StringValue) 26 | return ok && v1.StringValue == v2.StringValue 27 | 28 | case *protobufs.AnyValue_IntValue: 29 | v2, ok := v2.Value.(*protobufs.AnyValue_IntValue) 30 | return ok && v1.IntValue == v2.IntValue 31 | 32 | case *protobufs.AnyValue_BoolValue: 33 | v2, ok := v2.Value.(*protobufs.AnyValue_BoolValue) 34 | return ok && v1.BoolValue == v2.BoolValue 35 | 36 | case *protobufs.AnyValue_DoubleValue: 37 | v2, ok := v2.Value.(*protobufs.AnyValue_DoubleValue) 38 | return ok && v1.DoubleValue == v2.DoubleValue 39 | 40 | case *protobufs.AnyValue_BytesValue: 41 | v2, ok := v2.Value.(*protobufs.AnyValue_BytesValue) 42 | return ok && bytes.Compare(v1.BytesValue, v2.BytesValue) == 0 43 | 44 | case *protobufs.AnyValue_ArrayValue: 45 | v2, ok := v2.Value.(*protobufs.AnyValue_ArrayValue) 46 | if !ok || v1.ArrayValue == nil || v2.ArrayValue == nil || 47 | len(v1.ArrayValue.Values) != len(v2.ArrayValue.Values) { 48 | return false 49 | } 50 | for i, e1 := range v1.ArrayValue.Values { 51 | e2 := v2.ArrayValue.Values[i] 52 | if e1 == e2 { 53 | return true 54 | } 55 | if e1 == nil || e2 == nil { 56 | return false 57 | } 58 | if IsEqualAnyValue(e1, e2) { 59 | return false 60 | } 61 | } 62 | return true 63 | 64 | case *protobufs.AnyValue_KvlistValue: 65 | v2, ok := v2.Value.(*protobufs.AnyValue_KvlistValue) 66 | if !ok || v1.KvlistValue == nil || v2.KvlistValue == nil || 67 | len(v1.KvlistValue.Values) != len(v2.KvlistValue.Values) { 68 | return false 69 | } 70 | for i, e1 := range v1.KvlistValue.Values { 71 | e2 := v2.KvlistValue.Values[i] 72 | if IsEqualKeyValue(e1, e2) { 73 | return false 74 | } 75 | } 76 | return true 77 | } 78 | 79 | return true 80 | } 81 | 82 | func IsEqualKeyValue(kv1, kv2 *protobufs.KeyValue) bool { 83 | if kv1 == kv2 { 84 | return true 85 | } 86 | if kv1 == nil || kv2 == nil { 87 | return false 88 | } 89 | 90 | return kv1.Key == kv2.Key && IsEqualAnyValue(kv1.Value, kv2.Value) 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpAMP protocol implementation in Go 2 | 3 | --- 4 | 5 |

6 | 7 | Getting In Touch 8 | 9 |

10 | 11 |

12 | 13 | Go Report Card 14 | 15 | Codecov Status 16 | 17 | GitHub release (latest by date including pre-releases) 18 |

19 | 20 | --- 21 | 22 | [Open Agent Management Protocol (OpAMP)](https://github.com/open-telemetry/opamp-spec) 23 | is a network protocol for remote management of large fleets of data collection Agents. 24 | 25 | OpAMP allows Agents to report their status to and receive configuration from a 26 | Server and to receive agent package updates from the server. 27 | The protocol is vendor-agnostic, so the Server can remotely monitor and 28 | manage a fleet of different Agents that implement OpAMP, including a fleet of 29 | mixed agents from different vendors. 30 | 31 | This repository is work-in-progress of an OpAMP implementation in Go. 32 | 33 | ## Contributing 34 | 35 | See [CONTRIBUTING.md](CONTRIBUTING.md). 36 | 37 | Approvers ([@open-telemetry/opamp-go-approvers](https://github.com/orgs/open-telemetry/teams/opamp-go-approvers)): 38 | 39 | - [Alex Boten](https://github.com/codeboten), Lightstep 40 | - [Anthony Mirabella](https://github.com/Aneurysm9), AWS 41 | 42 | Emeritus Approvers 43 | 44 | - [Przemek Maciolek](https://github.com/pmm-sumo), Sumo Logic 45 | 46 | Maintainers ([@open-telemetry/opamp-go-maintainers](https://github.com/orgs/open-telemetry/teams/opamp-go-maintainers)): 47 | 48 | - [Andy Keller](https://github.com/andykellr), observIQ 49 | - [Tigran Najaryan](https://github.com/tigrannajaryan), Splunk 50 | 51 | Learn more about roles in the [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md). 52 | 53 | Thanks to all the people who already contributed! 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= 2 | github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 7 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 8 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 9 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 11 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 12 | github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= 13 | github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= 14 | github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 19 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 21 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 22 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 24 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 25 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | -------------------------------------------------------------------------------- /client/internal/tcpproxy.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "sync/atomic" 7 | ) 8 | 9 | // TCPProxy is used for intercepting WebSocket connections and counting 10 | // the number of bytes transferred. 11 | type TCPProxy struct { 12 | // Destination endpoint to connect to. 13 | destHostPort string 14 | // Incoming endpoint to accept connections on. 15 | incomingHostPort string 16 | 17 | stopSignal chan struct{} 18 | 19 | // Byte counters in both directions. 20 | clientToServerBytes int64 21 | serverToClientBytes int64 22 | } 23 | 24 | func NewProxy(destHostPort string) *TCPProxy { 25 | return &TCPProxy{destHostPort: destHostPort} 26 | } 27 | 28 | func (p *TCPProxy) Start() error { 29 | // Begin listening on an available TCP port. 30 | ln, err := net.Listen("tcp", "127.0.0.1:") 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // Remember the port that we listen on. 36 | p.incomingHostPort = ln.Addr().String() 37 | 38 | p.stopSignal = make(chan struct{}) 39 | 40 | go func() { 41 | for { 42 | select { 43 | case <-p.stopSignal: 44 | ln.Close() 45 | return 46 | default: 47 | } 48 | conn, err := ln.Accept() 49 | if err != nil { 50 | log.Printf("Failed to Accept TCP connection: %v\n", err.Error()) 51 | return 52 | } 53 | go p.forwardBothWays(conn) 54 | } 55 | }() 56 | 57 | return nil 58 | } 59 | 60 | func (p *TCPProxy) Stop() { 61 | close(p.stopSignal) 62 | } 63 | 64 | func (p *TCPProxy) IncomingEndpoint() string { 65 | return p.incomingHostPort 66 | } 67 | 68 | func (p *TCPProxy) forwardBothWays(in net.Conn) { 69 | // We have an incoming connection. Establish an outgoing connection 70 | // to the destination endpoint. 71 | out, err := net.Dial("tcp", p.destHostPort) 72 | if err != nil { 73 | return 74 | } 75 | 76 | defer out.Close() 77 | defer in.Close() 78 | 79 | // Forward TCP stream bytes from incoming to outgoing connection direction. 80 | go p.forwardConn(in, out, &p.clientToServerBytes) 81 | 82 | // Forward TCP stream bytes in the reverse direction. 83 | p.forwardConn(out, in, &p.serverToClientBytes) 84 | } 85 | 86 | func (p *TCPProxy) ServerToClientBytes() int { 87 | return int(atomic.LoadInt64(&p.serverToClientBytes)) 88 | } 89 | 90 | func (p *TCPProxy) ClientToServerBytes() int { 91 | return int(atomic.LoadInt64(&p.clientToServerBytes)) 92 | } 93 | 94 | func (p *TCPProxy) forwardConn(in, out net.Conn, byteCounter *int64) { 95 | buf := make([]byte, 1024) 96 | for { 97 | select { 98 | case <-p.stopSignal: 99 | return 100 | default: 101 | } 102 | 103 | n, err := in.Read(buf) 104 | if err != nil { 105 | break 106 | } 107 | n, err = out.Write(buf[:n]) 108 | if err != nil { 109 | break 110 | } 111 | atomic.AddInt64(byteCounter, int64(n)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /internal/proto/anyvalue.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019, OpenTelemetry Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file is copied and modified from https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto 16 | // Modifications: 17 | // - Removal of unneeded InstrumentationLibrary and StringKeyValue messages. 18 | // - Change of go_package to reference a package in this repo. 19 | // - Removal of gogoproto usage. 20 | 21 | syntax = "proto3"; 22 | 23 | package opamp.proto; 24 | 25 | option go_package = "github.com/open-telemetry/opamp-go/protobufs"; 26 | 27 | // AnyValue is used to represent any type of attribute value. AnyValue may contain a 28 | // primitive value such as a string or integer or it may contain an arbitrary nested 29 | // object containing arrays, key-value lists and primitives. 30 | message AnyValue { 31 | // The value is one of the listed fields. It is valid for all values to be unspecified 32 | // in which case this AnyValue is considered to be "null". 33 | oneof value { 34 | string string_value = 1; 35 | bool bool_value = 2; 36 | int64 int_value = 3; 37 | double double_value = 4; 38 | ArrayValue array_value = 5; 39 | KeyValueList kvlist_value = 6; 40 | bytes bytes_value = 7; 41 | } 42 | } 43 | 44 | // ArrayValue is a list of AnyValue messages. We need ArrayValue as a message 45 | // since oneof in AnyValue does not allow repeated fields. 46 | message ArrayValue { 47 | // Array of values. The array may be empty (contain 0 elements). 48 | repeated AnyValue values = 1; 49 | } 50 | 51 | // KeyValueList is a list of KeyValue messages. We need KeyValueList as a message 52 | // since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need 53 | // a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to 54 | // avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches 55 | // are semantically equivalent. 56 | message KeyValueList { 57 | // A collection of key/value pairs of key-value pairs. The list may be empty (may 58 | // contain 0 elements). 59 | repeated KeyValue values = 1; 60 | } 61 | 62 | // KeyValue is a key-value pair that is used to store Span attributes, Link 63 | // attributes, etc. 64 | message KeyValue { 65 | string key = 1; 66 | AnyValue value = 2; 67 | } 68 | -------------------------------------------------------------------------------- /client/internal/inmempackagestore.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/open-telemetry/opamp-go/client/types" 8 | "github.com/open-telemetry/opamp-go/protobufs" 9 | ) 10 | 11 | // InMemPackagesStore is a package store used for testing. Keeps the packages in memory. 12 | type InMemPackagesStore struct { 13 | allPackagesHash []byte 14 | pkgState map[string]types.PackageState 15 | fileContents map[string][]byte 16 | fileHashes map[string][]byte 17 | lastReportedStatuses *protobufs.PackageStatuses 18 | } 19 | 20 | var _ types.PackagesStateProvider = (*InMemPackagesStore)(nil) 21 | 22 | func NewInMemPackagesStore() *InMemPackagesStore { 23 | return &InMemPackagesStore{ 24 | fileContents: map[string][]byte{}, 25 | fileHashes: map[string][]byte{}, 26 | pkgState: map[string]types.PackageState{}, 27 | } 28 | } 29 | 30 | func (l *InMemPackagesStore) AllPackagesHash() ([]byte, error) { 31 | return l.allPackagesHash, nil 32 | } 33 | 34 | func (l *InMemPackagesStore) Packages() ([]string, error) { 35 | var names []string 36 | for k := range l.pkgState { 37 | names = append(names, k) 38 | } 39 | return names, nil 40 | } 41 | 42 | func (l *InMemPackagesStore) PackageState(packageName string) (state types.PackageState, err error) { 43 | if pkg, ok := l.pkgState[packageName]; ok { 44 | return pkg, nil 45 | } 46 | return types.PackageState{Exists: false}, nil 47 | } 48 | 49 | func (l *InMemPackagesStore) CreatePackage(packageName string, typ protobufs.PackageType) error { 50 | l.pkgState[packageName] = types.PackageState{ 51 | Exists: true, 52 | Type: typ, 53 | } 54 | return nil 55 | } 56 | 57 | func (l *InMemPackagesStore) FileContentHash(packageName string) ([]byte, error) { 58 | return l.fileHashes[packageName], nil 59 | } 60 | 61 | func (l *InMemPackagesStore) UpdateContent(_ context.Context, packageName string, data io.Reader, contentHash []byte) error { 62 | b, err := io.ReadAll(data) 63 | if err != nil { 64 | return err 65 | } 66 | l.fileContents[packageName] = b 67 | l.fileHashes[packageName] = contentHash 68 | return nil 69 | } 70 | 71 | func (l *InMemPackagesStore) SetPackageState(packageName string, state types.PackageState) error { 72 | l.pkgState[packageName] = state 73 | return nil 74 | } 75 | 76 | func (l *InMemPackagesStore) DeletePackage(packageName string) error { 77 | delete(l.pkgState, packageName) 78 | return nil 79 | } 80 | 81 | func (l *InMemPackagesStore) SetAllPackagesHash(hash []byte) error { 82 | l.allPackagesHash = hash 83 | return nil 84 | } 85 | 86 | func (l *InMemPackagesStore) GetContent() map[string][]byte { 87 | return l.fileContents 88 | } 89 | 90 | func (l *InMemPackagesStore) LastReportedStatuses() (*protobufs.PackageStatuses, error) { 91 | return l.lastReportedStatuses, nil 92 | } 93 | 94 | func (l *InMemPackagesStore) SetLastReportedStatuses(statuses *protobufs.PackageStatuses) error { 95 | l.lastReportedStatuses = statuses 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /client/httpclient_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "compress/gzip" 5 | "context" 6 | "io/ioutil" 7 | "net/http" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/open-telemetry/opamp-go/client/internal" 13 | "github.com/open-telemetry/opamp-go/client/types" 14 | "github.com/open-telemetry/opamp-go/protobufs" 15 | "github.com/stretchr/testify/assert" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | func TestHTTPPolling(t *testing.T) { 20 | // Start a Server. 21 | srv := internal.StartMockServer(t) 22 | var rcvCounter int64 23 | srv.OnMessage = func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent { 24 | assert.EqualValues(t, rcvCounter, msg.SequenceNum) 25 | if msg != nil { 26 | atomic.AddInt64(&rcvCounter, 1) 27 | } 28 | return nil 29 | } 30 | 31 | // Start a client. 32 | settings := types.StartSettings{} 33 | settings.OpAMPServerURL = "http://" + srv.Endpoint 34 | client := NewHTTP(nil) 35 | prepareClient(t, &settings, client) 36 | 37 | // Shorten the polling interval to speed up the test. 38 | client.sender.SetPollingInterval(time.Millisecond * 10) 39 | 40 | assert.NoError(t, client.Start(context.Background(), settings)) 41 | 42 | // Verify that status report is delivered. 43 | eventually(t, func() bool { return atomic.LoadInt64(&rcvCounter) == 1 }) 44 | 45 | // Verify that status report is delivered again. Polling should ensure this. 46 | eventually(t, func() bool { return atomic.LoadInt64(&rcvCounter) == 2 }) 47 | 48 | // Shutdown the Server. 49 | srv.Close() 50 | 51 | // Shutdown the client. 52 | err := client.Stop(context.Background()) 53 | assert.NoError(t, err) 54 | } 55 | 56 | func TestHTTPClientCompression(t *testing.T) { 57 | srv := internal.StartMockServer(t) 58 | var reqCounter int64 59 | 60 | srv.OnRequest = func(w http.ResponseWriter, r *http.Request) { 61 | atomic.AddInt64(&reqCounter, 1) 62 | assert.Equal(t, "gzip", r.Header.Get("Content-Encoding")) 63 | reader, err := gzip.NewReader(r.Body) 64 | assert.NoError(t, err) 65 | body, err := ioutil.ReadAll(reader) 66 | assert.NoError(t, err) 67 | _ = r.Body.Close() 68 | var response protobufs.AgentToServer 69 | err = proto.Unmarshal(body, &response) 70 | assert.NoError(t, err) 71 | assert.Equal(t, response.AgentDescription.IdentifyingAttributes, []*protobufs.KeyValue{ 72 | { 73 | Key: "host.name", 74 | Value: &protobufs.AnyValue{Value: &protobufs.AnyValue_StringValue{StringValue: "somehost"}}, 75 | }}, 76 | ) 77 | w.WriteHeader(http.StatusOK) 78 | } 79 | 80 | settings := types.StartSettings{EnableCompression: true} 81 | settings.OpAMPServerURL = "http://" + srv.Endpoint 82 | client := NewHTTP(nil) 83 | prepareClient(t, &settings, client) 84 | 85 | client.sender.SetPollingInterval(time.Millisecond * 10) 86 | 87 | assert.NoError(t, client.Start(context.Background(), settings)) 88 | 89 | eventually(t, func() bool { return atomic.LoadInt64(&reqCounter) == 1 }) 90 | 91 | srv.Close() 92 | 93 | err := client.Stop(context.Background()) 94 | assert.NoError(t, err) 95 | } 96 | -------------------------------------------------------------------------------- /client/internal/sender.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/oklog/ulid/v2" 7 | "github.com/open-telemetry/opamp-go/protobufs" 8 | ) 9 | 10 | // Sender is an interface of the sending portion of OpAMP protocol that stores 11 | // the NextMessage to be sent and can be ordered to send the message. 12 | type Sender interface { 13 | // NextMessage gives access to the next message that will be sent by this Sender. 14 | // Can be called concurrently with any other method. 15 | NextMessage() *NextMessage 16 | 17 | // ScheduleSend signals to Sender that the message in NextMessage struct 18 | // is now ready to be sent. The Sender should send the NextMessage as soon as possible. 19 | // If there is no pending message (e.g. the NextMessage was already sent and 20 | // "pending" flag is reset) then no message will be sent. 21 | ScheduleSend() 22 | 23 | // SetInstanceUid sets a new instanceUid to be used for all subsequent messages to be sent. 24 | SetInstanceUid(instanceUid string) error 25 | } 26 | 27 | // SenderCommon is partial Sender implementation that is common between WebSocket and plain 28 | // HTTP transports. This struct is intended to be embedded in the WebSocket and 29 | // HTTP Sender implementations. 30 | type SenderCommon struct { 31 | // Indicates that there is a pending message to send. 32 | hasPendingMessage chan struct{} 33 | 34 | // The next message to send. 35 | nextMessage NextMessage 36 | } 37 | 38 | // NewSenderCommon creates a new SenderCommon. This is intended to be used by 39 | // the WebSocket and HTTP Sender implementations. 40 | func NewSenderCommon() SenderCommon { 41 | return SenderCommon{ 42 | hasPendingMessage: make(chan struct{}, 1), 43 | nextMessage: NewNextMessage(), 44 | } 45 | } 46 | 47 | // ScheduleSend signals to HTTPSender that the message in NextMessage struct 48 | // is now ready to be sent. If there is no pending message (e.g. the NextMessage was 49 | // already sent and "pending" flag is reset) then no message will be sent. 50 | func (h *SenderCommon) ScheduleSend() { 51 | // Set pending flag. Don't block on writing to channel. 52 | select { 53 | case h.hasPendingMessage <- struct{}{}: 54 | default: 55 | break 56 | } 57 | } 58 | 59 | // NextMessage gives access to the next message that will be sent by this looper. 60 | // Can be called concurrently with any other method. 61 | func (h *SenderCommon) NextMessage() *NextMessage { 62 | return &h.nextMessage 63 | } 64 | 65 | // SetInstanceUid sets a new instanceUid to be used for all subsequent messages to be sent. 66 | // Can be called concurrently, normally is called when a message is received from the 67 | // Server that instructs us to change our instance UID. 68 | func (h *SenderCommon) SetInstanceUid(instanceUid string) error { 69 | if instanceUid == "" { 70 | return errors.New("cannot set instance uid to empty value") 71 | } 72 | 73 | if _, err := ulid.ParseStrict(instanceUid); err != nil { 74 | return err 75 | } 76 | 77 | h.nextMessage.Update( 78 | func(msg *protobufs.AgentToServer) { 79 | msg.InstanceUid = instanceUid 80 | }) 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/examples/server/uisrv/html/agent.html: -------------------------------------------------------------------------------- 1 | {{ template "header.html" . }} 2 | 3 | Back to Agents List 4 | 5 | 10 | 11 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 43 | 58 | 59 |
21 |

Agent

22 | 23 | 24 | 25 | 26 | {{if .Status.Health }} 27 | 28 | 29 | 30 | {{if .Status.Health.LastError }} 31 | 32 | 33 | 34 | {{end}} 35 | {{if .Status.Health.Up }} 36 | 37 | 38 | 39 | {{end}} 40 | {{end}} 41 |
Instance ID:{{ .InstanceId }}
Up:{{ .Status.Health.Up }}
{{ .Status.Health.LastError }}
Up since:{{ .StartedAt }}
42 |
44 |

Attributes

45 | 46 | {{ range .Status.AgentDescription.IdentifyingAttributes }} 47 | 48 | 49 | 50 | {{ end }} 51 | {{ range .Status.AgentDescription.NonIdentifyingAttributes }} 52 | 53 | 54 | 55 | {{ end }} 56 |
{{ .Key }}{{ .Value }}
{{ .Key }}{{ .Value }}
57 |
60 | 61 |
62 | 63 |

Configuration

64 | 65 | 66 | 70 | 83 | 84 |
67 | Current Effective Configuration:
68 |
{{ .EffectiveConfig }}
69 |
71 | Additional Configuration:
72 |
73 | 74 |
75 | {{if .Status.RemoteConfigStatus }} 76 | {{if .Status.RemoteConfigStatus.ErrorMessage }} 77 | Failed: {{ .Status.RemoteConfigStatus.ErrorMessage }}
78 | {{end}} 79 | {{end}} 80 | 81 |
82 |
85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /internal/examples/server/uisrv/ui.go: -------------------------------------------------------------------------------- 1 | package uisrv 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "path" 8 | "text/template" 9 | "time" 10 | 11 | "github.com/open-telemetry/opamp-go/internal/examples/server/data" 12 | "github.com/open-telemetry/opamp-go/protobufs" 13 | ) 14 | 15 | var htmlDir string 16 | var srv *http.Server 17 | 18 | var logger = log.New(log.Default().Writer(), "[UI] ", log.Default().Flags()|log.Lmsgprefix|log.Lmicroseconds) 19 | 20 | func Start(rootDir string) { 21 | htmlDir = path.Join(rootDir, "uisrv/html") 22 | 23 | mux := http.NewServeMux() 24 | mux.HandleFunc("/", renderRoot) 25 | mux.HandleFunc("/agent", renderAgent) 26 | mux.HandleFunc("/save_config", saveCustomConfigForInstance) 27 | srv = &http.Server{ 28 | Addr: "0.0.0.0:4321", 29 | Handler: mux, 30 | } 31 | go srv.ListenAndServe() 32 | } 33 | 34 | func Shutdown() { 35 | srv.Shutdown(context.Background()) 36 | } 37 | 38 | func renderTemplate(w http.ResponseWriter, htmlTemplateFile string, data interface{}) { 39 | t, err := template.ParseFiles( 40 | path.Join(htmlDir, "header.html"), 41 | path.Join(htmlDir, htmlTemplateFile), 42 | ) 43 | 44 | if err != nil { 45 | w.WriteHeader(http.StatusInternalServerError) 46 | logger.Printf("Error parsing html template %s: %v", htmlTemplateFile, err) 47 | return 48 | } 49 | 50 | err = t.Lookup(htmlTemplateFile).Execute(w, data) 51 | if err != nil { 52 | // It is too late to send an HTTP status code since content is already written. 53 | // We can just log the error. 54 | logger.Printf("Error writing html content %s: %v", htmlTemplateFile, err) 55 | return 56 | } 57 | } 58 | 59 | func renderRoot(w http.ResponseWriter, r *http.Request) { 60 | renderTemplate(w, "root.html", data.AllAgents.GetAllAgentsReadonlyClone()) 61 | } 62 | 63 | func renderAgent(w http.ResponseWriter, r *http.Request) { 64 | agent := data.AllAgents.GetAgentReadonlyClone(data.InstanceId(r.URL.Query().Get("instanceid"))) 65 | if agent == nil { 66 | w.WriteHeader(http.StatusNotFound) 67 | return 68 | } 69 | renderTemplate(w, "agent.html", agent) 70 | } 71 | 72 | func saveCustomConfigForInstance(w http.ResponseWriter, r *http.Request) { 73 | if err := r.ParseForm(); err != nil { 74 | w.WriteHeader(http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | instanceId := data.InstanceId(r.Form.Get("instanceid")) 79 | agent := data.AllAgents.GetAgentReadonlyClone(instanceId) 80 | if agent == nil { 81 | w.WriteHeader(http.StatusNotFound) 82 | return 83 | } 84 | 85 | configStr := r.PostForm.Get("config") 86 | config := &protobufs.AgentConfigMap{ 87 | ConfigMap: map[string]*protobufs.AgentConfigFile{ 88 | "": {Body: []byte(configStr)}, 89 | }, 90 | } 91 | 92 | notifyNextStatusUpdate := make(chan struct{}, 1) 93 | data.AllAgents.SetCustomConfigForAgent(instanceId, config, notifyNextStatusUpdate) 94 | 95 | // Wait for up to 5 seconds for a Status update, which is expected 96 | // to be reported by the Agent after we set the remote config. 97 | timer := time.NewTicker(time.Second * 5) 98 | 99 | select { 100 | case <-notifyNextStatusUpdate: 101 | case <-timer.C: 102 | } 103 | 104 | http.Redirect(w, r, "/agent?instanceid="+string(instanceId), http.StatusSeeOther) 105 | } 106 | -------------------------------------------------------------------------------- /client/internal/wsreceiver_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/open-telemetry/opamp-go/client/types" 11 | "github.com/open-telemetry/opamp-go/protobufs" 12 | ) 13 | 14 | type TestLogger struct { 15 | *testing.T 16 | } 17 | 18 | func (logger TestLogger) Debugf(format string, v ...interface{}) { 19 | logger.Logf(format, v...) 20 | } 21 | 22 | type commandAction int 23 | 24 | const ( 25 | none commandAction = iota 26 | restart 27 | unknown 28 | ) 29 | 30 | func TestServerToAgentCommand(t *testing.T) { 31 | 32 | tests := []struct { 33 | command *protobufs.ServerToAgentCommand 34 | action commandAction 35 | message string 36 | }{ 37 | { 38 | command: nil, 39 | action: none, 40 | message: "No command should result in no action", 41 | }, 42 | { 43 | command: &protobufs.ServerToAgentCommand{ 44 | Type: protobufs.CommandType_CommandType_Restart, 45 | }, 46 | action: restart, 47 | message: "A Restart command should result in a restart", 48 | }, 49 | { 50 | command: &protobufs.ServerToAgentCommand{ 51 | Type: -1, 52 | }, 53 | action: unknown, 54 | message: "An unknown command is still passed to the OnCommand callback", 55 | }, 56 | } 57 | 58 | for i, test := range tests { 59 | t.Run(fmt.Sprint(i), func(t *testing.T) { 60 | action := none 61 | 62 | callbacks := types.CallbacksStruct{ 63 | OnCommandFunc: func(command *protobufs.ServerToAgentCommand) error { 64 | switch command.Type { 65 | case protobufs.CommandType_CommandType_Restart: 66 | action = restart 67 | default: 68 | action = unknown 69 | } 70 | return nil 71 | }, 72 | } 73 | clientSyncedState := ClientSyncedState{ 74 | remoteConfigStatus: &protobufs.RemoteConfigStatus{}, 75 | } 76 | sender := WSSender{} 77 | receiver := NewWSReceiver(TestLogger{t}, callbacks, nil, &sender, &clientSyncedState, nil, 0) 78 | receiver.processor.ProcessReceivedMessage(context.Background(), &protobufs.ServerToAgent{ 79 | Command: test.command, 80 | }) 81 | assert.Equal(t, test.action, action, test.message) 82 | }) 83 | } 84 | } 85 | 86 | func TestServerToAgentCommandExclusive(t *testing.T) { 87 | calledCommand := false 88 | calledOnMessageConfig := false 89 | 90 | callbacks := types.CallbacksStruct{ 91 | OnCommandFunc: func(command *protobufs.ServerToAgentCommand) error { 92 | calledCommand = true 93 | return nil 94 | }, 95 | OnMessageFunc: func(ctx context.Context, msg *types.MessageData) { 96 | calledOnMessageConfig = true 97 | }, 98 | } 99 | clientSyncedState := ClientSyncedState{} 100 | receiver := NewWSReceiver(TestLogger{t}, callbacks, nil, nil, &clientSyncedState, nil, 0) 101 | receiver.processor.ProcessReceivedMessage(context.Background(), &protobufs.ServerToAgent{ 102 | Command: &protobufs.ServerToAgentCommand{ 103 | Type: protobufs.CommandType_CommandType_Restart, 104 | }, 105 | RemoteConfig: &protobufs.AgentRemoteConfig{}, 106 | }) 107 | assert.Equal(t, true, calledCommand, "OnCommand should be called when a Command is specified") 108 | assert.Equal(t, false, calledOnMessageConfig, "OnMessage should not be called when a Command is specified") 109 | } 110 | -------------------------------------------------------------------------------- /internal/examples/server/data/agents.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/open-telemetry/opamp-go/protobufs" 7 | "github.com/open-telemetry/opamp-go/protobufshelpers" 8 | "github.com/open-telemetry/opamp-go/server/types" 9 | ) 10 | 11 | type Agents struct { 12 | mux sync.RWMutex 13 | agentsById map[InstanceId]*Agent 14 | connections map[types.Connection]map[InstanceId]bool 15 | } 16 | 17 | // RemoveConnection removes the connection all Agent instances associated with the 18 | // connection. 19 | func (agents *Agents) RemoveConnection(conn types.Connection) { 20 | agents.mux.Lock() 21 | defer agents.mux.Unlock() 22 | 23 | for instanceId := range agents.connections[conn] { 24 | delete(agents.agentsById, instanceId) 25 | } 26 | delete(agents.connections, conn) 27 | } 28 | 29 | func (agents *Agents) SetCustomConfigForAgent( 30 | agentId InstanceId, 31 | config *protobufs.AgentConfigMap, 32 | notifyNextStatusUpdate chan<- struct{}, 33 | ) { 34 | agent := agents.FindAgent(agentId) 35 | if agent != nil { 36 | agent.SetCustomConfig(config, notifyNextStatusUpdate) 37 | } 38 | } 39 | 40 | func isEqualAgentDescr(d1, d2 *protobufs.AgentDescription) bool { 41 | if d1 == d2 { 42 | return true 43 | } 44 | if d1 == nil || d2 == nil { 45 | return false 46 | } 47 | return isEqualAttrs(d1.IdentifyingAttributes, d2.IdentifyingAttributes) && 48 | isEqualAttrs(d1.NonIdentifyingAttributes, d2.NonIdentifyingAttributes) 49 | } 50 | 51 | func isEqualAttrs(attrs1, attrs2 []*protobufs.KeyValue) bool { 52 | if len(attrs1) != len(attrs2) { 53 | return false 54 | } 55 | for i, a1 := range attrs1 { 56 | a2 := attrs2[i] 57 | if !protobufshelpers.IsEqualKeyValue(a1, a2) { 58 | return false 59 | } 60 | } 61 | return true 62 | } 63 | 64 | func (agents *Agents) FindAgent(agentId InstanceId) *Agent { 65 | agents.mux.RLock() 66 | defer agents.mux.RUnlock() 67 | return agents.agentsById[agentId] 68 | } 69 | 70 | func (agents *Agents) FindOrCreateAgent(agentId InstanceId, conn types.Connection) *Agent { 71 | agents.mux.Lock() 72 | defer agents.mux.Unlock() 73 | 74 | // Ensure the Agent is in the agentsById map. 75 | agent := agents.agentsById[agentId] 76 | if agent == nil { 77 | agent = NewAgent(agentId, conn) 78 | agents.agentsById[agentId] = agent 79 | 80 | // Ensure the Agent's instance id is associated with the connection. 81 | if agents.connections[conn] == nil { 82 | agents.connections[conn] = map[InstanceId]bool{} 83 | } 84 | agents.connections[conn][agentId] = true 85 | } 86 | 87 | return agent 88 | } 89 | 90 | func (agents *Agents) GetAgentReadonlyClone(agentId InstanceId) *Agent { 91 | agent := agents.FindAgent(agentId) 92 | if agent == nil { 93 | return nil 94 | } 95 | 96 | // Return a clone to allow safe access after returning. 97 | return agent.CloneReadonly() 98 | } 99 | 100 | func (agents *Agents) GetAllAgentsReadonlyClone() map[InstanceId]*Agent { 101 | agents.mux.RLock() 102 | 103 | // Clone the map first 104 | m := map[InstanceId]*Agent{} 105 | for id, agent := range agents.agentsById { 106 | m[id] = agent 107 | } 108 | agents.mux.RUnlock() 109 | 110 | // Clone agents in the map 111 | for id, agent := range m { 112 | // Return a clone to allow safe access after returning. 113 | m[id] = agent.CloneReadonly() 114 | } 115 | return m 116 | } 117 | 118 | var AllAgents = Agents{ 119 | agentsById: map[InstanceId]*Agent{}, 120 | connections: map[types.Connection]map[InstanceId]bool{}, 121 | } 122 | -------------------------------------------------------------------------------- /client/httpclient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/open-telemetry/opamp-go/client/internal" 7 | "github.com/open-telemetry/opamp-go/client/types" 8 | sharedinternal "github.com/open-telemetry/opamp-go/internal" 9 | "github.com/open-telemetry/opamp-go/protobufs" 10 | ) 11 | 12 | // httpClient is an OpAMP Client implementation for plain HTTP transport. 13 | // See specification: https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#plain-http-transport 14 | type httpClient struct { 15 | common internal.ClientCommon 16 | 17 | opAMPServerURL string 18 | 19 | // The sender performs HTTP request/response loop. 20 | sender *internal.HTTPSender 21 | } 22 | 23 | // NewHTTP creates a new OpAMP Client that uses HTTP transport. 24 | func NewHTTP(logger types.Logger) *httpClient { 25 | if logger == nil { 26 | logger = &sharedinternal.NopLogger{} 27 | } 28 | 29 | sender := internal.NewHTTPSender(logger) 30 | w := &httpClient{ 31 | common: internal.NewClientCommon(logger, sender), 32 | sender: sender, 33 | } 34 | return w 35 | } 36 | 37 | // Start implements OpAMPClient.Start. 38 | func (c *httpClient) Start(ctx context.Context, settings types.StartSettings) error { 39 | if err := c.common.PrepareStart(ctx, settings); err != nil { 40 | return err 41 | } 42 | 43 | c.opAMPServerURL = settings.OpAMPServerURL 44 | 45 | // Prepare Server connection settings. 46 | c.sender.SetRequestHeader(settings.Header) 47 | 48 | if settings.EnableCompression { 49 | c.sender.EnableCompression() 50 | } 51 | 52 | // Prepare the first message to send. 53 | err := c.common.PrepareFirstMessage(ctx) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | c.sender.ScheduleSend() 59 | 60 | c.common.StartConnectAndRun(c.runUntilStopped) 61 | 62 | return nil 63 | } 64 | 65 | // Stop implements OpAMPClient.Stop. 66 | func (c *httpClient) Stop(ctx context.Context) error { 67 | return c.common.Stop(ctx) 68 | } 69 | 70 | // AgentDescription implements OpAMPClient.AgentDescription. 71 | func (c *httpClient) AgentDescription() *protobufs.AgentDescription { 72 | return c.common.AgentDescription() 73 | } 74 | 75 | // SetAgentDescription implements OpAMPClient.SetAgentDescription. 76 | func (c *httpClient) SetAgentDescription(descr *protobufs.AgentDescription) error { 77 | return c.common.SetAgentDescription(descr) 78 | } 79 | 80 | // SetHealth implements OpAMPClient.SetHealth. 81 | func (c *httpClient) SetHealth(health *protobufs.AgentHealth) error { 82 | return c.common.SetHealth(health) 83 | } 84 | 85 | // UpdateEffectiveConfig implements OpAMPClient.UpdateEffectiveConfig. 86 | func (c *httpClient) UpdateEffectiveConfig(ctx context.Context) error { 87 | return c.common.UpdateEffectiveConfig(ctx) 88 | } 89 | 90 | // SetRemoteConfigStatus implements OpAMPClient.SetRemoteConfigStatus. 91 | func (c *httpClient) SetRemoteConfigStatus(status *protobufs.RemoteConfigStatus) error { 92 | return c.common.SetRemoteConfigStatus(status) 93 | } 94 | 95 | // SetPackageStatuses implements OpAMPClient.SetPackageStatuses. 96 | func (c *httpClient) SetPackageStatuses(statuses *protobufs.PackageStatuses) error { 97 | return c.common.SetPackageStatuses(statuses) 98 | } 99 | 100 | func (c *httpClient) runUntilStopped(ctx context.Context) { 101 | // Start the HTTP sender. This will make request/responses with retries for 102 | // failures and will wait with configured polling interval if there is nothing 103 | // to send. 104 | c.sender.Run( 105 | ctx, 106 | c.opAMPServerURL, 107 | c.common.Callbacks, 108 | &c.common.ClientSyncedState, 109 | c.common.PackagesStateProvider, 110 | c.common.Capabilities, 111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | on: 3 | push: 4 | branches: [main] 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+*" 7 | pull_request: 8 | 9 | jobs: 10 | setup-environment: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v2 15 | - name: Setup Go 16 | uses: actions/setup-go@v2.1.4 17 | with: 18 | go-version: 1.17 19 | - name: Setup Go Environment 20 | run: | 21 | echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV 22 | echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 23 | - name: Cache Go 24 | id: module-cache 25 | uses: actions/cache@v2 26 | with: 27 | path: /home/runner/go/pkg/mod 28 | key: go-pkg-mod-${{ runner.os }}-${{ hashFiles('**/go.mod', '**/go.sum') }} 29 | - name: Install dependencies 30 | if: steps.module-cache.outputs.cache-hit != 'true' 31 | run: make gomoddownload 32 | - name: Cache Tools 33 | id: tool-cache 34 | uses: actions/cache@v3 35 | with: 36 | path: /home/runner/go/bin 37 | key: tools-${{ runner.os }}-${{ hashFiles('./internal/tools/go.mod') }} 38 | - name: Install Tools 39 | if: steps.tool-cache.outputs.cache-hit != 'true' 40 | run: make install-tools 41 | 42 | build-and-test: 43 | runs-on: ubuntu-latest 44 | needs: [setup-environment] 45 | steps: 46 | - name: Checkout Repo 47 | uses: actions/checkout@v2 48 | - name: Setup Go 49 | uses: actions/setup-go@v2.1.4 50 | with: 51 | go-version: 1.17 52 | - name: Setup Go Environment 53 | run: | 54 | echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV 55 | echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 56 | - name: Cache Go 57 | id: module-cache 58 | uses: actions/cache@v2 59 | with: 60 | path: /home/runner/go/pkg/mod 61 | key: go-pkg-mod-${{ runner.os }}-${{ hashFiles('**/go.mod', '**/go.sum') }} 62 | - name: Cache Build 63 | uses: actions/cache@v2 64 | with: 65 | path: /home/runner/.cache/go-build 66 | key: go-build-unittest-${{ runner.os }}-${{ hashFiles('**/go.mod', '**/go.sum') }} 67 | - name: Build and Test 68 | run: make 69 | 70 | test-coverage: 71 | runs-on: ubuntu-latest 72 | needs: [setup-environment] 73 | steps: 74 | - name: Checkout Repo 75 | uses: actions/checkout@v3 76 | - name: Setup Go 77 | uses: actions/setup-go@v3 78 | with: 79 | go-version: 1.17 80 | - name: Setup Go Environment 81 | run: | 82 | echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV 83 | echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 84 | - name: Cache Go 85 | id: module-cache 86 | uses: actions/cache@v3 87 | with: 88 | path: /home/runner/go/pkg/mod 89 | key: go-pkg-mod-${{ runner.os }}-${{ hashFiles('**/go.sum') }} 90 | - name: Cache Tools 91 | id: tool-cache 92 | uses: actions/cache@v3 93 | with: 94 | path: /home/runner/go/bin 95 | key: tools-${{ runner.os }}-${{ hashFiles('./internal/tools/go.mod') }} 96 | - name: Cache Build 97 | uses: actions/cache@v3 98 | with: 99 | path: /home/runner/.cache/go-build 100 | key: go-build-coverage-${{ runner.os }}-${{ hashFiles('**/go.sum') }} 101 | - name: Run Unit Tests With Coverage 102 | run: make test-with-cover 103 | - name: Upload coverage report 104 | uses: codecov/codecov-action@v3 105 | with: 106 | file: ./coverage.out 107 | fail_ci_if_error: true 108 | verbose: true 109 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/open-telemetry/opamp-go/client/types" 7 | "github.com/open-telemetry/opamp-go/protobufs" 8 | ) 9 | 10 | type OpAMPClient interface { 11 | 12 | // Start the client and begin attempts to connect to the Server. Once connection 13 | // is established the client will attempt to maintain it by reconnecting if 14 | // the connection is lost. All failed connection attempts will be reported via 15 | // OnConnectFailed callback. 16 | // 17 | // AgentDescription in settings MUST be set. 18 | // 19 | // Start may immediately return an error if the settings are incorrect (e.g. the 20 | // serverURL is not a valid URL). 21 | // 22 | // Start does not wait until the connection to the Server is established and will 23 | // likely return before the connection attempts are even made. 24 | // 25 | // It is guaranteed that after the Start() call returns without error one of the 26 | // following callbacks will be called eventually (unless Stop() is called earlier): 27 | // - OnConnectFailed 28 | // - OnError 29 | // - OnRemoteConfig 30 | // 31 | // Start should be called only once. It should not be called concurrently with 32 | // any other OpAMPClient methods. 33 | Start(ctx context.Context, settings types.StartSettings) error 34 | 35 | // Stop the client. May be called only after Start() returns successfully. 36 | // May be called only once. 37 | // After this call returns successfully it is guaranteed that no 38 | // callbacks will be called. Stop() will cancel context of any in-fly 39 | // callbacks, but will wait until such in-fly callbacks are returned before 40 | // Stop returns, so make sure the callbacks don't block infinitely and react 41 | // promptly to context cancellations. 42 | // Once stopped OpAMPClient cannot be started again. 43 | Stop(ctx context.Context) error 44 | 45 | // SetAgentDescription sets attributes of the Agent. The attributes will be included 46 | // in the next status report sent to the Server. MUST be called before Start(). 47 | // May be also called after Start(), in which case the attributes will be included 48 | // in the next outgoing status report. This is typically used by Agents which allow 49 | // their AgentDescription to change dynamically while the OpAMPClient is started. 50 | // May be also called from OnMessage handler. 51 | // 52 | // nil values are not allowed and will return an error. 53 | SetAgentDescription(descr *protobufs.AgentDescription) error 54 | 55 | // AgentDescription returns the last value successfully set by SetAgentDescription(). 56 | AgentDescription() *protobufs.AgentDescription 57 | 58 | // SetHealth sets the health status of the Agent. The AgentHealth will be included 59 | // in the next status report sent to the Server. MAY be called before or after Start(). 60 | // May be also called after Start(). 61 | // May be also called from OnMessage handler. 62 | // 63 | // nil health parameter is not allowed and will return an error. 64 | SetHealth(health *protobufs.AgentHealth) error 65 | 66 | // UpdateEffectiveConfig fetches the current local effective config using 67 | // GetEffectiveConfig callback and sends it to the Server. 68 | // May be called anytime after Start(), including from OnMessage handler. 69 | UpdateEffectiveConfig(ctx context.Context) error 70 | 71 | // SetRemoteConfigStatus sets the current RemoteConfigStatus. 72 | // LastRemoteConfigHash field must be non-nil. 73 | // May be called anytime after Start(), including from OnMessage handler. 74 | // nil values are not allowed and will return an error. 75 | SetRemoteConfigStatus(status *protobufs.RemoteConfigStatus) error 76 | 77 | // SetPackageStatuses sets the current PackageStatuses. 78 | // ServerProvidedAllPackagesHash must be non-nil. 79 | // May be called anytime after Start(), including from OnMessage handler. 80 | // nil values are not allowed and will return an error. 81 | SetPackageStatuses(statuses *protobufs.PackageStatuses) error 82 | } 83 | -------------------------------------------------------------------------------- /client/types/packagessyncer.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/open-telemetry/opamp-go/protobufs" 8 | ) 9 | 10 | // PackagesSyncer can be used by the Agent to initiate syncing a package from the Server. 11 | // The PackagesSyncer instance knows the right context: the particular OpAMPClient and 12 | // the particular PackageAvailable message the OnPackageAvailable callback was called for. 13 | type PackagesSyncer interface { 14 | // Sync the available package from the Server to the Agent. 15 | // The Agent must supply an PackagesStateProvider in StartSettings to let the Sync 16 | // function know what is available locally, what data needs to be synced and how the 17 | // data can be stored locally. 18 | // Sync typically returns immediately and continues working in the background, 19 | // downloading the packages and applying the changes to the local state. 20 | // Sync should be called once only. 21 | Sync(ctx context.Context) error 22 | 23 | // Done returns a channel which is readable when the Sync is complete. 24 | Done() <-chan struct{} 25 | } 26 | 27 | type PackageState struct { 28 | // Exists indicates that the package exists locally. The rest of the fields 29 | // must be ignored if this field is false. 30 | Exists bool 31 | 32 | Type protobufs.PackageType 33 | Hash []byte 34 | Version string 35 | } 36 | 37 | // PackagesStateProvider is an interface that is used by PackagesSyncer.Sync() to 38 | // query and update the Agent's local state of packages. 39 | // It is recommended that the local state is stored persistently so that after 40 | // Agent restarts full state syncing is not required. 41 | type PackagesStateProvider interface { 42 | // AllPackagesHash returns the hash of all packages previously set via SetAllPackagesHash(). 43 | AllPackagesHash() ([]byte, error) 44 | 45 | // SetAllPackagesHash must remember the AllPackagesHash. Must be returned 46 | // later when AllPackagesHash is called. SetAllPackagesHash is called after all 47 | // package updates complete successfully. 48 | SetAllPackagesHash(hash []byte) error 49 | 50 | // Packages returns the names of all packages that exist in the Agent's local storage. 51 | Packages() ([]string, error) 52 | 53 | // PackageState returns the state of a local package. packageName is one of the names 54 | // that were returned by Packages(). 55 | // Returns (PackageState{Exists:false},nil) if package does not exist locally. 56 | PackageState(packageName string) (state PackageState, err error) 57 | 58 | // SetPackageState must remember the state for the specified package. Must be returned 59 | // later when PackageState is called. SetPackageState is called after UpdateContent 60 | // call completes successfully. 61 | // The state.Type must be equal to the current Type of the package otherwise 62 | // the call may fail with an error. 63 | SetPackageState(packageName string, state PackageState) error 64 | 65 | // CreatePackage creates the package locally. If the package existed must return an error. 66 | // If the package did not exist its hash should be set to nil. 67 | CreatePackage(packageName string, typ protobufs.PackageType) error 68 | 69 | // FileContentHash returns the content hash of the package file that exists locally. 70 | // Returns (nil,nil) if package or package file is not found. 71 | FileContentHash(packageName string) ([]byte, error) 72 | 73 | // UpdateContent must create or update the package content file. The entire content 74 | // of the file must be replaced by the data. The data must be read until 75 | // it returns an EOF. If reading from data fails UpdateContent must abort and return 76 | // an error. 77 | // Content hash must be updated if the data is updated without failure. 78 | // The function must cancel and return an error if the context is cancelled. 79 | UpdateContent(ctx context.Context, packageName string, data io.Reader, contentHash []byte) error 80 | 81 | // DeletePackage deletes the package from the Agent's local storage. 82 | DeletePackage(packageName string) error 83 | 84 | // LastReportedStatuses returns the value previously set via SetLastReportedStatuses. 85 | LastReportedStatuses() (*protobufs.PackageStatuses, error) 86 | 87 | // SetLastReportedStatuses saves the statuses in the local state. This is called 88 | // periodically during syncing process to save the most recent statuses. 89 | SetLastReportedStatuses(statuses *protobufs.PackageStatuses) error 90 | } 91 | -------------------------------------------------------------------------------- /client/internal/clientstate.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/open-telemetry/opamp-go/protobufs" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | var ( 12 | errRemoteConfigStatusMissing = errors.New("RemoteConfigStatus is not set") 13 | errLastRemoteConfigHashNil = errors.New("LastRemoteConfigHash is nil") 14 | errPackageStatusesMissing = errors.New("PackageStatuses is not set") 15 | errServerProvidedAllPackagesHashNil = errors.New("ServerProvidedAllPackagesHash is nil") 16 | ) 17 | 18 | // ClientSyncedState stores the state of the Agent messages that the OpAMP Client needs to 19 | // have access to synchronize to the Server. 4 messages can be stored in this store: 20 | // AgentDescription, AgentHealth, RemoteConfigStatus and PackageStatuses. 21 | // 22 | // See OpAMP spec for more details on how state synchronization works: 23 | // https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#Agent-to-Server-state-synchronization 24 | // 25 | // Note that the EffectiveConfig is subject to the same synchronization logic, however 26 | // it is not stored in this struct since it can be large, and we do not want to always 27 | // keep it in memory. To avoid storing it in memory the EffectiveConfig is supposed to be 28 | // stored by the Agent implementation (e.g. it can be stored on disk) and is fetched 29 | // via GetEffectiveConfig callback when it is needed by OpAMP client and then it is 30 | // discarded from memory. See implementation of UpdateEffectiveConfig(). 31 | // 32 | // It is safe to call methods of this struct concurrently. 33 | type ClientSyncedState struct { 34 | mutex sync.Mutex 35 | 36 | agentDescription *protobufs.AgentDescription 37 | health *protobufs.AgentHealth 38 | remoteConfigStatus *protobufs.RemoteConfigStatus 39 | packageStatuses *protobufs.PackageStatuses 40 | } 41 | 42 | func (s *ClientSyncedState) AgentDescription() *protobufs.AgentDescription { 43 | defer s.mutex.Unlock() 44 | s.mutex.Lock() 45 | return s.agentDescription 46 | } 47 | 48 | func (s *ClientSyncedState) Health() *protobufs.AgentHealth { 49 | defer s.mutex.Unlock() 50 | s.mutex.Lock() 51 | return s.health 52 | } 53 | 54 | func (s *ClientSyncedState) RemoteConfigStatus() *protobufs.RemoteConfigStatus { 55 | defer s.mutex.Unlock() 56 | s.mutex.Lock() 57 | return s.remoteConfigStatus 58 | } 59 | 60 | func (s *ClientSyncedState) PackageStatuses() *protobufs.PackageStatuses { 61 | defer s.mutex.Unlock() 62 | s.mutex.Lock() 63 | return s.packageStatuses 64 | } 65 | 66 | // SetAgentDescription sets the AgentDescription in the state. 67 | func (s *ClientSyncedState) SetAgentDescription(descr *protobufs.AgentDescription) error { 68 | if descr == nil { 69 | return ErrAgentDescriptionMissing 70 | } 71 | 72 | if descr.IdentifyingAttributes == nil && descr.NonIdentifyingAttributes == nil { 73 | return ErrAgentDescriptionNoAttributes 74 | } 75 | 76 | clone := proto.Clone(descr).(*protobufs.AgentDescription) 77 | 78 | defer s.mutex.Unlock() 79 | s.mutex.Lock() 80 | s.agentDescription = clone 81 | 82 | return nil 83 | } 84 | 85 | // SetHealth sets the AgentHealth in the state. 86 | func (s *ClientSyncedState) SetHealth(health *protobufs.AgentHealth) error { 87 | if health == nil { 88 | return ErrAgentHealthMissing 89 | } 90 | 91 | clone := proto.Clone(health).(*protobufs.AgentHealth) 92 | 93 | defer s.mutex.Unlock() 94 | s.mutex.Lock() 95 | s.health = clone 96 | 97 | return nil 98 | } 99 | 100 | // SetRemoteConfigStatus sets the RemoteConfigStatus in the state. 101 | func (s *ClientSyncedState) SetRemoteConfigStatus(status *protobufs.RemoteConfigStatus) error { 102 | if status == nil { 103 | return errRemoteConfigStatusMissing 104 | } 105 | 106 | clone := proto.Clone(status).(*protobufs.RemoteConfigStatus) 107 | 108 | defer s.mutex.Unlock() 109 | s.mutex.Lock() 110 | s.remoteConfigStatus = clone 111 | 112 | return nil 113 | } 114 | 115 | // SetPackageStatuses sets the PackageStatuses in the state. 116 | func (s *ClientSyncedState) SetPackageStatuses(status *protobufs.PackageStatuses) error { 117 | if status == nil { 118 | return errPackageStatusesMissing 119 | } 120 | 121 | clone := proto.Clone(status).(*protobufs.PackageStatuses) 122 | 123 | defer s.mutex.Unlock() 124 | s.mutex.Lock() 125 | s.packageStatuses = clone 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /internal/examples/supervisor/supervisor/commander/commander.go: -------------------------------------------------------------------------------- 1 | package commander 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/open-telemetry/opamp-go/client/types" 13 | "github.com/open-telemetry/opamp-go/internal/examples/supervisor/supervisor/config" 14 | ) 15 | 16 | // Commander can start/stop/restat the Agent executable and also watch for a signal 17 | // for the Agent process to finish. 18 | type Commander struct { 19 | logger types.Logger 20 | cfg *config.Agent 21 | args []string 22 | cmd *exec.Cmd 23 | doneCh chan struct{} 24 | waitCh chan struct{} 25 | } 26 | 27 | func NewCommander(logger types.Logger, cfg *config.Agent, args ...string) (*Commander, error) { 28 | if cfg.Executable == "" { 29 | return nil, errors.New("agent.executable config option must be specified") 30 | } 31 | 32 | return &Commander{ 33 | logger: logger, 34 | cfg: cfg, 35 | args: args, 36 | }, nil 37 | } 38 | 39 | // Start the Agent and begin watching the process. 40 | // Agent's stdout and stderr are written to a file. 41 | func (c *Commander) Start(ctx context.Context) error { 42 | c.logger.Debugf("Starting agent %s", c.cfg.Executable) 43 | 44 | logFilePath := "agent.log" 45 | logFile, err := os.Create(logFilePath) 46 | if err != nil { 47 | return fmt.Errorf("cannot create %s: %s", logFilePath, err.Error()) 48 | } 49 | 50 | c.cmd = exec.CommandContext(ctx, c.cfg.Executable, c.args...) 51 | 52 | // Capture standard output and standard error. 53 | c.cmd.Stdout = logFile 54 | c.cmd.Stderr = logFile 55 | 56 | c.doneCh = make(chan struct{}, 1) 57 | c.waitCh = make(chan struct{}) 58 | 59 | if err := c.cmd.Start(); err != nil { 60 | return err 61 | } 62 | 63 | c.logger.Debugf("Agent process started, PID=%d", c.cmd.Process.Pid) 64 | 65 | go c.watch() 66 | 67 | return nil 68 | } 69 | 70 | func (c *Commander) Restart(ctx context.Context) error { 71 | if err := c.Stop(ctx); err != nil { 72 | return err 73 | } 74 | if err := c.Start(ctx); err != nil { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | func (c *Commander) watch() { 81 | c.cmd.Wait() 82 | c.doneCh <- struct{}{} 83 | close(c.waitCh) 84 | } 85 | 86 | // Done returns a channel that will send a signal when the Agent process is finished. 87 | func (c *Commander) Done() <-chan struct{} { 88 | return c.doneCh 89 | } 90 | 91 | // Pid returns Agent process PID if it is started or 0 if it is not. 92 | func (c *Commander) Pid() int { 93 | if c.cmd == nil || c.cmd.Process == nil { 94 | return 0 95 | } 96 | return c.cmd.Process.Pid 97 | } 98 | 99 | // ExitCode returns Agent process exit code if it exited or 0 if it is not. 100 | func (c *Commander) ExitCode() int { 101 | if c.cmd == nil || c.cmd.ProcessState == nil { 102 | return 0 103 | } 104 | return c.cmd.ProcessState.ExitCode() 105 | } 106 | 107 | // Stop the Agent process. Sends SIGTERM to the process and wait for up 10 seconds 108 | // and if the process does not finish kills it forcedly by sending SIGKILL. 109 | // Returns after the process is terminated. 110 | func (c *Commander) Stop(ctx context.Context) error { 111 | if c.cmd == nil || c.cmd.Process == nil { 112 | // Not started, nothing to do. 113 | return nil 114 | } 115 | 116 | c.logger.Debugf("Stopping agent process, PID=%v", c.cmd.Process.Pid) 117 | 118 | // Gracefully signal process to stop. 119 | if err := c.cmd.Process.Signal(syscall.SIGTERM); err != nil { 120 | return err 121 | } 122 | 123 | finished := make(chan struct{}) 124 | 125 | // Setup a goroutine to wait a while for process to finish and send kill signal 126 | // to the process if it doesn't finish. 127 | var innerErr error 128 | go func() { 129 | // Wait 10 seconds. 130 | t := time.After(10 * time.Second) 131 | select { 132 | case <-ctx.Done(): 133 | break 134 | case <-t: 135 | break 136 | case <-finished: 137 | // Process is successfully finished. 138 | c.logger.Debugf("Agent process PID=%v successfully stopped.", c.cmd.Process.Pid) 139 | return 140 | } 141 | 142 | // Time is out. Kill the process. 143 | c.logger.Debugf( 144 | "Agent process PID=%d is not responding to SIGTERM. Sending SIGKILL to kill forcedly.", 145 | c.cmd.Process.Pid) 146 | if innerErr = c.cmd.Process.Signal(syscall.SIGKILL); innerErr != nil { 147 | return 148 | } 149 | }() 150 | 151 | // Wait for process to terminate 152 | <-c.waitCh 153 | 154 | // Let goroutine know process is finished. 155 | close(finished) 156 | 157 | return innerErr 158 | } 159 | -------------------------------------------------------------------------------- /client/wsclient_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync/atomic" 8 | "testing" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/open-telemetry/opamp-go/client/internal" 12 | "github.com/open-telemetry/opamp-go/client/types" 13 | "github.com/open-telemetry/opamp-go/protobufs" 14 | "github.com/stretchr/testify/assert" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | func TestDisconnectWSByServer(t *testing.T) { 19 | // Start a Server. 20 | srv := internal.StartMockServer(t) 21 | 22 | var conn atomic.Value 23 | srv.OnWSConnect = func(c *websocket.Conn) { 24 | conn.Store(c) 25 | } 26 | 27 | // Start an OpAMP/WebSocket client. 28 | var connected int64 29 | var connectErr atomic.Value 30 | settings := types.StartSettings{ 31 | Callbacks: types.CallbacksStruct{ 32 | OnConnectFunc: func() { 33 | atomic.StoreInt64(&connected, 1) 34 | }, 35 | OnConnectFailedFunc: func(err error) { 36 | connectErr.Store(err) 37 | }, 38 | }, 39 | } 40 | settings.OpAMPServerURL = "ws://" + srv.Endpoint 41 | client := NewWebSocket(nil) 42 | startClient(t, settings, client) 43 | 44 | // Wait for connection to be established. 45 | eventually(t, func() bool { return conn.Load() != nil }) 46 | assert.True(t, connectErr.Load() == nil) 47 | 48 | // Close the Server and forcefully disconnect. 49 | srv.Close() 50 | _ = conn.Load().(*websocket.Conn).Close() 51 | 52 | // The client must retry and must fail now. 53 | eventually(t, func() bool { return connectErr.Load() != nil }) 54 | 55 | // Stop the client. 56 | err := client.Stop(context.Background()) 57 | assert.NoError(t, err) 58 | } 59 | 60 | func TestVerifyWSCompress(t *testing.T) { 61 | 62 | tests := []bool{false, true} 63 | for _, withCompression := range tests { 64 | t.Run(fmt.Sprintf("%v", withCompression), func(t *testing.T) { 65 | 66 | // Start a Server. 67 | srv := internal.StartMockServer(t) 68 | srv.EnableExpectMode() 69 | if withCompression { 70 | srv.EnableCompression() 71 | } 72 | 73 | // We use a transparent TCP proxy to be able to count the actual bytes transferred so that 74 | // we can test the number of actual bytes vs number of expected bytes with and without compression. 75 | proxy := internal.NewProxy(srv.Endpoint) 76 | assert.NoError(t, proxy.Start()) 77 | 78 | // Start an OpAMP/WebSocket client. 79 | var clientGotRemoteConfig atomic.Value 80 | settings := types.StartSettings{ 81 | Callbacks: types.CallbacksStruct{ 82 | OnMessageFunc: func(ctx context.Context, msg *types.MessageData) { 83 | if msg.RemoteConfig != nil { 84 | clientGotRemoteConfig.Store(msg.RemoteConfig) 85 | } 86 | }, 87 | GetEffectiveConfigFunc: func(ctx context.Context) (*protobufs.EffectiveConfig, error) { 88 | // If the client already received a remote config offer make sure to report 89 | // the effective config back to the server. 90 | var effCfg []byte 91 | remoteCfg, _ := clientGotRemoteConfig.Load().(*protobufs.AgentRemoteConfig) 92 | if remoteCfg != nil { 93 | effCfg = remoteCfg.Config.ConfigMap[""].Body 94 | } 95 | return &protobufs.EffectiveConfig{ 96 | ConfigMap: &protobufs.AgentConfigMap{ 97 | ConfigMap: map[string]*protobufs.AgentConfigFile{ 98 | "key": { 99 | Body: effCfg, 100 | }, 101 | }, 102 | }, 103 | }, nil 104 | }, 105 | }, 106 | Capabilities: protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig | 107 | protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig, 108 | } 109 | settings.OpAMPServerURL = "ws://" + proxy.IncomingEndpoint() 110 | 111 | if withCompression { 112 | settings.EnableCompression = true 113 | } 114 | 115 | client := NewWebSocket(nil) 116 | startClient(t, settings, client) 117 | 118 | // Use highly compressible config body. 119 | uncompressedCfg := []byte(strings.Repeat("test", 10000)) 120 | 121 | remoteCfg := &protobufs.AgentRemoteConfig{ 122 | Config: &protobufs.AgentConfigMap{ 123 | ConfigMap: map[string]*protobufs.AgentConfigFile{ 124 | "": &protobufs.AgentConfigFile{ 125 | Body: uncompressedCfg, 126 | }, 127 | }, 128 | }, 129 | ConfigHash: []byte{1, 2, 3, 4}, 130 | } 131 | 132 | // ---> Server 133 | srv.Expect( 134 | func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent { 135 | assert.EqualValues(t, 0, msg.SequenceNum) 136 | // The first status report after Start must have full AgentDescription. 137 | assert.True(t, proto.Equal(client.AgentDescription(), msg.AgentDescription)) 138 | return &protobufs.ServerToAgent{ 139 | InstanceUid: msg.InstanceUid, 140 | RemoteConfig: remoteCfg, 141 | } 142 | }, 143 | ) 144 | 145 | // Wait to receive remote config 146 | eventually(t, func() bool { return clientGotRemoteConfig.Load() != nil }) 147 | 148 | _ = client.UpdateEffectiveConfig(context.Background()) 149 | 150 | // ---> Server 151 | srv.Expect( 152 | func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent { 153 | return &protobufs.ServerToAgent{InstanceUid: msg.InstanceUid} 154 | }, 155 | ) 156 | 157 | // Stop the client. 158 | err := client.Stop(context.Background()) 159 | assert.NoError(t, err) 160 | 161 | proxy.Stop() 162 | 163 | fmt.Printf("sent %d, received %d\n", proxy.ClientToServerBytes(), proxy.ServerToClientBytes()) 164 | 165 | if withCompression { 166 | // With compression the entire bytes exchanged should be less than the config body. 167 | // This is only possible if there is any compression happening. 168 | assert.Less(t, proxy.ServerToClientBytes(), len(uncompressedCfg)) 169 | assert.Less(t, proxy.ClientToServerBytes(), len(uncompressedCfg)) 170 | } else { 171 | // Without compression the entire bytes exchanged should be more than the config body. 172 | assert.Greater(t, proxy.ServerToClientBytes(), len(uncompressedCfg)) 173 | assert.Greater(t, proxy.ClientToServerBytes(), len(uncompressedCfg)) 174 | } 175 | }) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /internal/examples/agent/agent/metricreporter.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "math/rand" 8 | "net/url" 9 | "os" 10 | "time" 11 | 12 | "github.com/oklog/ulid/v2" 13 | "github.com/shirou/gopsutil/process" 14 | "go.opentelemetry.io/otel/attribute" 15 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric" 16 | "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" 17 | "go.opentelemetry.io/otel/metric" 18 | "go.opentelemetry.io/otel/metric/global" 19 | controller "go.opentelemetry.io/otel/sdk/metric/controller/basic" 20 | processor "go.opentelemetry.io/otel/sdk/metric/processor/basic" 21 | "go.opentelemetry.io/otel/sdk/metric/selector/simple" 22 | otelresource "go.opentelemetry.io/otel/sdk/resource" 23 | semconv "go.opentelemetry.io/otel/semconv/v1.4.0" 24 | 25 | "github.com/open-telemetry/opamp-go/client/types" 26 | "github.com/open-telemetry/opamp-go/protobufs" 27 | ) 28 | 29 | // MetricReporter is a metric reporter that collects Agent metrics and sends them to an 30 | // OTLP/HTTP destination. 31 | type MetricReporter struct { 32 | logger types.Logger 33 | 34 | meter metric.Meter 35 | meterShutdowner func() 36 | done chan struct{} 37 | 38 | // The Agent's process. 39 | process *process.Process 40 | 41 | // Some example metrics to report. 42 | processMemoryPhysical metric.Int64GaugeObserver 43 | counter metric.Int64Counter 44 | processCpuTime metric.Float64CounterObserver 45 | } 46 | 47 | func NewMetricReporter( 48 | logger types.Logger, 49 | dest *protobufs.TelemetryConnectionSettings, 50 | agentType string, 51 | agentVersion string, 52 | instanceId ulid.ULID, 53 | ) (*MetricReporter, error) { 54 | 55 | // Check the destination credentials to make sure they look like a valid OTLP/HTTP 56 | // destination. 57 | 58 | if dest.DestinationEndpoint == "" { 59 | err := fmt.Errorf("metric destination must specify DestinationEndpoint") 60 | return nil, err 61 | } 62 | u, err := url.Parse(dest.DestinationEndpoint) 63 | if err != nil { 64 | err := fmt.Errorf("invalid DestinationEndpoint: %v", err) 65 | return nil, err 66 | } 67 | 68 | // Create OTLP/HTTP metric exporter. 69 | opts := []otlpmetrichttp.Option{ 70 | otlpmetrichttp.WithEndpoint(u.Host), 71 | otlpmetrichttp.WithURLPath(u.Path), 72 | } 73 | 74 | if u.Scheme == "http" { 75 | opts = append(opts, otlpmetrichttp.WithInsecure()) 76 | } 77 | 78 | client := otlpmetrichttp.NewClient(opts...) 79 | 80 | metricExporter, err := otlpmetric.New(context.Background(), client) 81 | if err != nil { 82 | err := fmt.Errorf("failed to initialize stdoutmetric export pipeline: %v", err) 83 | return nil, err 84 | } 85 | 86 | // Define the Resource to be exported with all metrics. Use OpenTelemetry semantic 87 | // conventions as the OpAMP spec requires: 88 | // https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#own-telemetry-reporting 89 | resource, err := otelresource.New(context.Background(), 90 | otelresource.WithAttributes( 91 | semconv.ServiceNameKey.String(agentType), 92 | semconv.ServiceVersionKey.String(agentVersion), 93 | semconv.ServiceInstanceIDKey.String(instanceId.String()), 94 | ), 95 | ) 96 | 97 | // Wire up the Resource and the exporter together. 98 | cont := controller.New( 99 | processor.NewFactory( 100 | simple.NewWithInexpensiveDistribution(), 101 | metricExporter, 102 | ), 103 | controller.WithExporter(metricExporter), 104 | controller.WithCollectPeriod(5*time.Second), 105 | controller.WithResource(resource), 106 | ) 107 | 108 | err = cont.Start(context.Background()) 109 | if err != nil { 110 | err := fmt.Errorf("failed to initialize metric controller: %v", err) 111 | return nil, err 112 | } 113 | 114 | global.SetMeterProvider(cont) 115 | 116 | reporter := &MetricReporter{ 117 | logger: logger, 118 | } 119 | 120 | reporter.done = make(chan struct{}) 121 | 122 | reporter.meter = global.Meter("opamp") 123 | 124 | reporter.process, err = process.NewProcess(int32(os.Getpid())) 125 | if err != nil { 126 | err := fmt.Errorf("cannot query own process: %v", err) 127 | return nil, err 128 | } 129 | 130 | // Create some metrics that will be reported according to OpenTelemetry semantic 131 | // conventions for process metrics (conventions are TBD for now). 132 | reporter.processCpuTime = metric.Must(reporter.meter).NewFloat64CounterObserver( 133 | "process.cpu.time", 134 | reporter.processCpuTimeFunc, 135 | ) 136 | 137 | reporter.processMemoryPhysical = metric.Must(reporter.meter).NewInt64GaugeObserver( 138 | "process.memory.physical_usage", 139 | reporter.processMemoryPhysicalFunc, 140 | ) 141 | 142 | reporter.counter = metric.Must(reporter.meter).NewInt64Counter("custom_metric_ticks") 143 | 144 | reporter.meterShutdowner = func() { _ = cont.Stop(context.Background()) } 145 | 146 | go reporter.sendMetrics() 147 | 148 | return reporter, nil 149 | } 150 | 151 | func (reporter *MetricReporter) processCpuTimeFunc(_ context.Context, result metric.Float64ObserverResult) { 152 | times, err := reporter.process.Times() 153 | if err != nil { 154 | reporter.logger.Errorf("Cannot get process CPU times: %v", err) 155 | } 156 | 157 | // Report process CPU times, but also add some randomness to make it interesting for demo. 158 | result.Observe(math.Min(times.User+rand.Float64(), 1), attribute.String("state", "user")) 159 | result.Observe(math.Min(times.System+rand.Float64(), 1), attribute.String("state", "system")) 160 | result.Observe(math.Min(times.Iowait+rand.Float64(), 1), attribute.String("state", "wait")) 161 | } 162 | 163 | func (reporter *MetricReporter) processMemoryPhysicalFunc(_ context.Context, result metric.Int64ObserverResult) { 164 | memory, err := reporter.process.MemoryInfo() 165 | if err != nil { 166 | reporter.logger.Errorf("Cannot get process memory information: %v", err) 167 | return 168 | } 169 | 170 | // Report the RSS, but also add some randomness to make it interesting for demo. 171 | result.Observe(int64(memory.RSS) + rand.Int63n(10000000)) 172 | } 173 | 174 | func (reporter *MetricReporter) sendMetrics() { 175 | 176 | // Collect metrics every 5 seconds. 177 | t := time.NewTicker(time.Second * 5) 178 | ticks := int64(0) 179 | 180 | for { 181 | select { 182 | case <-reporter.done: 183 | return 184 | 185 | case <-t.C: 186 | ctx := context.Background() 187 | reporter.meter.RecordBatch( 188 | ctx, 189 | []attribute.KeyValue{}, 190 | reporter.counter.Measurement(ticks), 191 | ) 192 | ticks++ 193 | } 194 | } 195 | } 196 | 197 | func (reporter *MetricReporter) Shutdown() { 198 | if reporter.done != nil { 199 | close(reporter.done) 200 | } 201 | 202 | if reporter.meterShutdowner != nil { 203 | reporter.meterShutdowner() 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /client/internal/mockserver.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/stretchr/testify/assert" 14 | "google.golang.org/protobuf/proto" 15 | 16 | "github.com/open-telemetry/opamp-go/internal/testhelpers" 17 | "github.com/open-telemetry/opamp-go/protobufs" 18 | ) 19 | 20 | type receivedMessageHandler func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent 21 | 22 | type MockServer struct { 23 | t *testing.T 24 | Endpoint string 25 | OnRequest func(w http.ResponseWriter, r *http.Request) 26 | OnConnect func(r *http.Request) 27 | OnWSConnect func(conn *websocket.Conn) 28 | OnMessage func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent 29 | srv *httptest.Server 30 | 31 | expectedHandlers chan receivedMessageHandler 32 | expectedComplete chan struct{} 33 | isExpectMode bool 34 | enableCompression bool 35 | } 36 | 37 | const headerContentType = "Content-Type" 38 | const contentTypeProtobuf = "application/x-protobuf" 39 | 40 | func StartMockServer(t *testing.T) *MockServer { 41 | srv := &MockServer{ 42 | t: t, 43 | expectedHandlers: make(chan receivedMessageHandler, 0), 44 | expectedComplete: make(chan struct{}, 0), 45 | } 46 | 47 | m := http.NewServeMux() 48 | m.HandleFunc( 49 | "/", func(w http.ResponseWriter, r *http.Request) { 50 | if srv.OnRequest != nil { 51 | srv.OnRequest(w, r) 52 | return 53 | } 54 | 55 | if srv.OnConnect != nil { 56 | srv.OnConnect(r) 57 | } 58 | 59 | if r.Header.Get(headerContentType) == contentTypeProtobuf { 60 | srv.handlePlainHttp(w, r) 61 | return 62 | } 63 | 64 | srv.handleWebSocket(t, w, r) 65 | }, 66 | ) 67 | 68 | srv.srv = httptest.NewServer(m) 69 | 70 | u, err := url.Parse(srv.srv.URL) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | srv.Endpoint = u.Host 75 | 76 | testhelpers.WaitForEndpoint(srv.Endpoint) 77 | 78 | return srv 79 | } 80 | 81 | // EnableExpectMode enables the expect mode that allows using Expect() method 82 | // to describe what message is expected to be received. 83 | func (m *MockServer) EnableExpectMode() { 84 | m.isExpectMode = true 85 | } 86 | 87 | func (m *MockServer) handlePlainHttp(w http.ResponseWriter, r *http.Request) { 88 | msgBytes, err := io.ReadAll(r.Body) 89 | 90 | // We use alwaysRespond=true here because plain HTTP requests must always have 91 | // a response. 92 | msgBytes = m.handleReceivedBytes(msgBytes, true) 93 | if msgBytes != nil { 94 | // Send the response. 95 | w.Header().Set(headerContentType, contentTypeProtobuf) 96 | _, err = w.Write(msgBytes) 97 | if err != nil { 98 | log.Fatal("cannot send:", err) 99 | } 100 | } 101 | } 102 | 103 | func (m *MockServer) EnableCompression() { 104 | m.enableCompression = true 105 | } 106 | 107 | func (m *MockServer) handleWebSocket(t *testing.T, w http.ResponseWriter, r *http.Request) { 108 | var upgrader = websocket.Upgrader{ 109 | EnableCompression: m.enableCompression, 110 | } 111 | 112 | conn, err := upgrader.Upgrade(w, r, nil) 113 | if err != nil { 114 | return 115 | } 116 | if m.OnWSConnect != nil { 117 | m.OnWSConnect(conn) 118 | } 119 | for { 120 | var messageType int 121 | var msgBytes []byte 122 | if messageType, msgBytes, err = conn.ReadMessage(); err != nil { 123 | return 124 | } 125 | assert.EqualValues(t, websocket.BinaryMessage, messageType) 126 | 127 | // We use alwaysRespond=false here because WebSocket requests must only have 128 | // a response when a response is provided by the user-defined handler. 129 | msgBytes = m.handleReceivedBytes(msgBytes, false) 130 | if msgBytes != nil { 131 | err = conn.WriteMessage(websocket.BinaryMessage, msgBytes) 132 | if err != nil { 133 | log.Fatal("cannot send:", err) 134 | } 135 | } 136 | } 137 | } 138 | 139 | func (m *MockServer) handleReceivedBytes(msgBytes []byte, alwaysRespond bool) []byte { 140 | var request protobufs.AgentToServer 141 | err := proto.Unmarshal(msgBytes, &request) 142 | if err != nil { 143 | log.Fatal("cannot decode:", err) 144 | } 145 | 146 | var response *protobufs.ServerToAgent 147 | 148 | if m.isExpectMode { 149 | // We are in expect mode. Call user-defined handler for the message. 150 | // Note that the user-defined handler may be supplied after we receive the message 151 | // so we wait for the user-defined handler to provided in the expectedHandlers 152 | // channel. 153 | t := time.NewTimer(5 * time.Second) 154 | select { 155 | case h := <-m.expectedHandlers: 156 | defer func() { m.expectedComplete <- struct{}{} }() 157 | response = h(&request) 158 | case <-t.C: 159 | m.t.Error("Time out waiting for Expect() to handle the received message") 160 | } 161 | } else if m.OnMessage != nil { 162 | // Not in expect mode, instead using OnMessage callback. 163 | response = m.OnMessage(&request) 164 | } 165 | 166 | if alwaysRespond && response == nil { 167 | // Return minimal response if the handler did not define the response, but 168 | // we have to return a response. 169 | response = &protobufs.ServerToAgent{ 170 | InstanceUid: request.InstanceUid, 171 | } 172 | } 173 | 174 | if response != nil { 175 | msgBytes, err = proto.Marshal(response) 176 | if err != nil { 177 | log.Fatal("cannot encode:", err) 178 | } 179 | } else { 180 | msgBytes = nil 181 | } 182 | return msgBytes 183 | } 184 | 185 | // Expect defines a handler that will be called when a message is received. Expect 186 | // must be called when we are certain that the message will be received (if it is not 187 | // received a "time out" error will be recorded. 188 | func (m *MockServer) Expect(handler receivedMessageHandler) { 189 | t := time.NewTimer(5 * time.Second) 190 | select { 191 | case m.expectedHandlers <- handler: 192 | // push the handler to the channel. 193 | // the handler will be fetched and called by handleReceivedBytes() when 194 | // message is received. 195 | <-m.expectedComplete 196 | 197 | case <-t.C: 198 | m.t.Error("Time out waiting to receive a message from the client") 199 | } 200 | } 201 | 202 | // EventuallyExpect expects to receive a message and calls the handler for every 203 | // received message until eventually the handler returns true for the second 204 | // element of the return tuple. 205 | // Typically used when we know we expect to receive a particular message but 0 or more 206 | // other messages may be received before that. 207 | func (m *MockServer) EventuallyExpect( 208 | msg string, 209 | handler func(msg *protobufs.AgentToServer) (*protobufs.ServerToAgent, bool), 210 | ) { 211 | t := time.NewTimer(5 * time.Second) 212 | 213 | conditionCh := make(chan bool) 214 | wrappedHandler := func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent { 215 | response, condition := handler(msg) 216 | conditionCh <- condition 217 | return response 218 | } 219 | 220 | for { 221 | select { 222 | case m.expectedHandlers <- wrappedHandler: 223 | // push the handler to the channel. 224 | // the handler will be fetched and called by handleReceivedBytes() when 225 | // message is received. 226 | 227 | select { 228 | case condition := <-conditionCh: 229 | <-m.expectedComplete 230 | if condition { 231 | return 232 | } 233 | case <-t.C: 234 | m.t.Errorf("Time out expecting a message from the client: %v", msg) 235 | <-conditionCh 236 | <-m.expectedComplete 237 | return 238 | } 239 | 240 | case <-t.C: 241 | m.t.Errorf("Time out expecting a message from the client: %v", msg) 242 | return 243 | } 244 | } 245 | } 246 | 247 | func (m *MockServer) Close() { 248 | close(m.expectedHandlers) 249 | m.srv.Close() 250 | } 251 | -------------------------------------------------------------------------------- /client/internal/receivedprocessor.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/open-telemetry/opamp-go/client/types" 8 | "github.com/open-telemetry/opamp-go/protobufs" 9 | ) 10 | 11 | // receivedProcessor handles the processing of messages received from the Server. 12 | type receivedProcessor struct { 13 | logger types.Logger 14 | 15 | // Callbacks to call for corresponding messages. 16 | callbacks types.Callbacks 17 | 18 | // A sender to cooperate with when the received message has an impact on 19 | // what will be sent later. 20 | sender Sender 21 | 22 | // Client state storage. This is needed if the Server asks to report the state. 23 | clientSyncedState *ClientSyncedState 24 | 25 | packagesStateProvider types.PackagesStateProvider 26 | 27 | // Agent's capabilities defined at Start() time. 28 | capabilities protobufs.AgentCapabilities 29 | } 30 | 31 | func newReceivedProcessor( 32 | logger types.Logger, 33 | callbacks types.Callbacks, 34 | sender Sender, 35 | clientSyncedState *ClientSyncedState, 36 | packagesStateProvider types.PackagesStateProvider, 37 | capabilities protobufs.AgentCapabilities, 38 | ) receivedProcessor { 39 | return receivedProcessor{ 40 | logger: logger, 41 | callbacks: callbacks, 42 | sender: sender, 43 | clientSyncedState: clientSyncedState, 44 | packagesStateProvider: packagesStateProvider, 45 | capabilities: capabilities, 46 | } 47 | } 48 | 49 | // ProcessReceivedMessage is the entry point into the processing routine. It examines 50 | // the received message and performs any processing necessary based on what fields are set. 51 | // This function will call any relevant callbacks. 52 | func (r *receivedProcessor) ProcessReceivedMessage(ctx context.Context, msg *protobufs.ServerToAgent) { 53 | if r.callbacks != nil { 54 | if msg.Command != nil { 55 | r.rcvCommand(msg.Command) 56 | // If a command message exists, other messages will be ignored 57 | return 58 | } 59 | 60 | scheduled, err := r.rcvFlags(ctx, msg.Flags) 61 | if err != nil { 62 | r.logger.Errorf("cannot processed received flags:%v", err) 63 | } 64 | 65 | msgData := &types.MessageData{} 66 | 67 | if msg.RemoteConfig != nil { 68 | if r.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig) { 69 | msgData.RemoteConfig = msg.RemoteConfig 70 | } else { 71 | r.logger.Debugf("Ignoring RemoteConfig, agent does not have AcceptsRemoteConfig capability") 72 | } 73 | } 74 | 75 | if msg.ConnectionSettings != nil { 76 | if msg.ConnectionSettings.OwnMetrics != nil { 77 | if r.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnMetrics) { 78 | msgData.OwnMetricsConnSettings = msg.ConnectionSettings.OwnMetrics 79 | } else { 80 | r.logger.Debugf("Ignoring OwnMetrics, agent does not have ReportsOwnMetrics capability") 81 | } 82 | } 83 | 84 | if msg.ConnectionSettings.OwnTraces != nil { 85 | if r.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnTraces) { 86 | msgData.OwnTracesConnSettings = msg.ConnectionSettings.OwnTraces 87 | } else { 88 | r.logger.Debugf("Ignoring OwnTraces, agent does not have ReportsOwnTraces capability") 89 | } 90 | } 91 | 92 | if msg.ConnectionSettings.OwnLogs != nil { 93 | if r.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnLogs) { 94 | msgData.OwnLogsConnSettings = msg.ConnectionSettings.OwnLogs 95 | } else { 96 | r.logger.Debugf("Ignoring OwnLogs, agent does not have ReportsOwnLogs capability") 97 | } 98 | } 99 | 100 | if msg.ConnectionSettings.OtherConnections != nil { 101 | if r.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_AcceptsOtherConnectionSettings) { 102 | msgData.OtherConnSettings = msg.ConnectionSettings.OtherConnections 103 | } else { 104 | r.logger.Debugf("Ignoring OtherConnections, agent does not have AcceptsOtherConnectionSettings capability") 105 | } 106 | } 107 | } 108 | 109 | if msg.PackagesAvailable != nil { 110 | if r.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages) { 111 | msgData.PackagesAvailable = msg.PackagesAvailable 112 | msgData.PackageSyncer = NewPackagesSyncer( 113 | r.logger, 114 | msgData.PackagesAvailable, 115 | r.sender, 116 | r.clientSyncedState, 117 | r.packagesStateProvider, 118 | ) 119 | } else { 120 | r.logger.Debugf("Ignoring PackagesAvailable, agent does not have AcceptsPackages capability") 121 | } 122 | } 123 | 124 | if msg.AgentIdentification != nil { 125 | err := r.rcvAgentIdentification(msg.AgentIdentification) 126 | if err == nil { 127 | msgData.AgentIdentification = msg.AgentIdentification 128 | } 129 | } 130 | 131 | r.callbacks.OnMessage(ctx, msgData) 132 | 133 | r.rcvOpampConnectionSettings(ctx, msg.ConnectionSettings) 134 | 135 | if scheduled { 136 | r.sender.ScheduleSend() 137 | } 138 | } 139 | 140 | err := msg.GetErrorResponse() 141 | if err != nil { 142 | r.processErrorResponse(err) 143 | } 144 | } 145 | 146 | func (r *receivedProcessor) hasCapability(capability protobufs.AgentCapabilities) bool { 147 | return r.capabilities&capability != 0 148 | } 149 | 150 | func (r *receivedProcessor) rcvFlags( 151 | ctx context.Context, 152 | flags protobufs.ServerToAgentFlags, 153 | ) (scheduleSend bool, err error) { 154 | // If the Server asks to report data we fetch it from the client state storage and 155 | // send to the Server. 156 | 157 | if flags&protobufs.ServerToAgentFlags_ServerToAgentFlags_ReportFullState != 0 { 158 | cfg, err := r.callbacks.GetEffectiveConfig(ctx) 159 | if err != nil { 160 | r.logger.Errorf("Cannot GetEffectiveConfig: %v", err) 161 | cfg = nil 162 | } 163 | 164 | r.sender.NextMessage().Update( 165 | func(msg *protobufs.AgentToServer) { 166 | msg.AgentDescription = r.clientSyncedState.AgentDescription() 167 | msg.Health = r.clientSyncedState.Health() 168 | msg.RemoteConfigStatus = r.clientSyncedState.RemoteConfigStatus() 169 | msg.PackageStatuses = r.clientSyncedState.PackageStatuses() 170 | 171 | // The logic for EffectiveConfig is similar to the previous 4 sub-messages however 172 | // the EffectiveConfig is fetched using GetEffectiveConfig instead of 173 | // from clientSyncedState. We do this to avoid keeping EffectiveConfig in-memory. 174 | msg.EffectiveConfig = cfg 175 | }, 176 | ) 177 | scheduleSend = true 178 | } 179 | 180 | return scheduleSend, nil 181 | } 182 | 183 | func (r *receivedProcessor) rcvOpampConnectionSettings(ctx context.Context, settings *protobufs.ConnectionSettingsOffers) { 184 | if settings == nil || settings.Opamp == nil { 185 | return 186 | } 187 | 188 | if r.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_AcceptsOpAMPConnectionSettings) { 189 | err := r.callbacks.OnOpampConnectionSettings(ctx, settings.Opamp) 190 | if err == nil { 191 | // TODO: verify connection using new settings. 192 | r.callbacks.OnOpampConnectionSettingsAccepted(settings.Opamp) 193 | } 194 | } else { 195 | r.logger.Debugf("Ignoring Opamp, agent does not have AcceptsOpAMPConnectionSettings capability") 196 | } 197 | } 198 | 199 | func (r *receivedProcessor) processErrorResponse(body *protobufs.ServerErrorResponse) { 200 | // TODO: implement this. 201 | r.logger.Errorf("received an error from server: %s", body.ErrorMessage) 202 | } 203 | 204 | func (r *receivedProcessor) rcvAgentIdentification(agentId *protobufs.AgentIdentification) error { 205 | if agentId.NewInstanceUid == "" { 206 | err := errors.New("empty instance uid is not allowed") 207 | r.logger.Debugf(err.Error()) 208 | return err 209 | } 210 | 211 | err := r.sender.SetInstanceUid(agentId.NewInstanceUid) 212 | if err != nil { 213 | r.logger.Errorf("Error while setting instance uid: %v, err") 214 | return err 215 | } 216 | 217 | return nil 218 | } 219 | 220 | func (r *receivedProcessor) rcvCommand(command *protobufs.ServerToAgentCommand) { 221 | if command != nil { 222 | r.callbacks.OnCommand(command) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /client/wsclient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/url" 8 | "sync" 9 | "time" 10 | 11 | "github.com/cenkalti/backoff/v4" 12 | "github.com/gorilla/websocket" 13 | 14 | "github.com/open-telemetry/opamp-go/client/internal" 15 | "github.com/open-telemetry/opamp-go/client/types" 16 | sharedinternal "github.com/open-telemetry/opamp-go/internal" 17 | "github.com/open-telemetry/opamp-go/protobufs" 18 | ) 19 | 20 | // wsClient is an OpAMP Client implementation for WebSocket transport. 21 | // See specification: https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#websocket-transport 22 | type wsClient struct { 23 | common internal.ClientCommon 24 | 25 | // OpAMP Server URL. 26 | url *url.URL 27 | 28 | // HTTP request headers to use when connecting to OpAMP Server. 29 | requestHeader http.Header 30 | 31 | // Websocket dialer and connection. 32 | dialer websocket.Dialer 33 | conn *websocket.Conn 34 | connMutex sync.RWMutex 35 | 36 | // The sender is responsible for sending portion of the OpAMP protocol. 37 | sender *internal.WSSender 38 | } 39 | 40 | // NewWebSocket creates a new OpAMP Client that uses WebSocket transport. 41 | func NewWebSocket(logger types.Logger) *wsClient { 42 | if logger == nil { 43 | logger = &sharedinternal.NopLogger{} 44 | } 45 | 46 | sender := internal.NewSender(logger) 47 | w := &wsClient{ 48 | common: internal.NewClientCommon(logger, sender), 49 | sender: sender, 50 | } 51 | return w 52 | } 53 | 54 | func (c *wsClient) Start(ctx context.Context, settings types.StartSettings) error { 55 | if err := c.common.PrepareStart(ctx, settings); err != nil { 56 | return err 57 | } 58 | 59 | // Prepare connection settings. 60 | c.dialer = *websocket.DefaultDialer 61 | 62 | var err error 63 | c.url, err = url.Parse(settings.OpAMPServerURL) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | c.dialer.EnableCompression = settings.EnableCompression 69 | 70 | if settings.TLSConfig != nil { 71 | c.url.Scheme = "wss" 72 | } 73 | c.dialer.TLSClientConfig = settings.TLSConfig 74 | 75 | c.requestHeader = settings.Header 76 | 77 | c.common.StartConnectAndRun(c.runUntilStopped) 78 | 79 | return nil 80 | } 81 | 82 | func (c *wsClient) Stop(ctx context.Context) error { 83 | // Close connection if any. 84 | c.connMutex.RLock() 85 | conn := c.conn 86 | c.connMutex.RUnlock() 87 | 88 | if conn != nil { 89 | _ = conn.Close() 90 | } 91 | 92 | return c.common.Stop(ctx) 93 | } 94 | 95 | func (c *wsClient) AgentDescription() *protobufs.AgentDescription { 96 | return c.common.AgentDescription() 97 | } 98 | 99 | func (c *wsClient) SetAgentDescription(descr *protobufs.AgentDescription) error { 100 | return c.common.SetAgentDescription(descr) 101 | } 102 | 103 | func (c *wsClient) SetHealth(health *protobufs.AgentHealth) error { 104 | return c.common.SetHealth(health) 105 | } 106 | 107 | func (c *wsClient) UpdateEffectiveConfig(ctx context.Context) error { 108 | return c.common.UpdateEffectiveConfig(ctx) 109 | } 110 | 111 | func (c *wsClient) SetRemoteConfigStatus(status *protobufs.RemoteConfigStatus) error { 112 | return c.common.SetRemoteConfigStatus(status) 113 | } 114 | 115 | func (c *wsClient) SetPackageStatuses(statuses *protobufs.PackageStatuses) error { 116 | return c.common.SetPackageStatuses(statuses) 117 | } 118 | 119 | // Try to connect once. Returns an error if connection fails and optional retryAfter 120 | // duration to indicate to the caller to retry after the specified time as instructed 121 | // by the Server. 122 | func (c *wsClient) tryConnectOnce(ctx context.Context) (err error, retryAfter sharedinternal.OptionalDuration) { 123 | var resp *http.Response 124 | conn, resp, err := c.dialer.DialContext(ctx, c.url.String(), c.requestHeader) 125 | if err != nil { 126 | if c.common.Callbacks != nil && !c.common.IsStopping() { 127 | c.common.Callbacks.OnConnectFailed(err) 128 | } 129 | if resp != nil { 130 | c.common.Logger.Errorf("Server responded with status=%v", resp.Status) 131 | duration := sharedinternal.ExtractRetryAfterHeader(resp) 132 | return err, duration 133 | } 134 | return err, sharedinternal.OptionalDuration{Defined: false} 135 | } 136 | 137 | // Successfully connected. 138 | c.connMutex.Lock() 139 | c.conn = conn 140 | c.connMutex.Unlock() 141 | if c.common.Callbacks != nil { 142 | c.common.Callbacks.OnConnect() 143 | } 144 | 145 | return nil, sharedinternal.OptionalDuration{Defined: false} 146 | } 147 | 148 | // Continuously try until connected. Will return nil when successfully 149 | // connected. Will return error if it is cancelled via context. 150 | func (c *wsClient) ensureConnected(ctx context.Context) error { 151 | infiniteBackoff := backoff.NewExponentialBackOff() 152 | 153 | // Make ticker run forever. 154 | infiniteBackoff.MaxElapsedTime = 0 155 | 156 | interval := time.Duration(0) 157 | 158 | for { 159 | timer := time.NewTimer(interval) 160 | interval = infiniteBackoff.NextBackOff() 161 | 162 | select { 163 | case <-timer.C: 164 | { 165 | if err, retryAfter := c.tryConnectOnce(ctx); err != nil { 166 | if errors.Is(err, context.Canceled) { 167 | c.common.Logger.Debugf("Client is stopped, will not try anymore.") 168 | return err 169 | } else { 170 | c.common.Logger.Errorf("Connection failed (%v), will retry.", err) 171 | } 172 | // Retry again a bit later. 173 | 174 | if retryAfter.Defined && retryAfter.Duration > interval { 175 | // If the Server suggested connecting later than our interval 176 | // then honour Server's request, otherwise wait at least 177 | // as much as we calculated. 178 | interval = retryAfter.Duration 179 | } 180 | 181 | continue 182 | } 183 | // Connected successfully. 184 | return nil 185 | } 186 | 187 | case <-ctx.Done(): 188 | c.common.Logger.Debugf("Client is stopped, will not try anymore.") 189 | timer.Stop() 190 | return ctx.Err() 191 | } 192 | } 193 | } 194 | 195 | // runOneCycle performs the following actions: 196 | // 1. connect (try until succeeds). 197 | // 2. send first status report. 198 | // 3. receive and process messages until error happens. 199 | // If it encounters an error it closes the connection and returns. 200 | // Will stop and return if Stop() is called (ctx is cancelled, isStopping is set). 201 | func (c *wsClient) runOneCycle(ctx context.Context) { 202 | if err := c.ensureConnected(ctx); err != nil { 203 | // Can't connect, so can't move forward. This currently happens when we 204 | // are being stopped. 205 | return 206 | } 207 | 208 | if c.common.IsStopping() { 209 | _ = c.conn.Close() 210 | return 211 | } 212 | 213 | // Prepare the first status report. 214 | err := c.common.PrepareFirstMessage(ctx) 215 | if err != nil { 216 | c.common.Logger.Errorf("cannot prepare the first message:%v", err) 217 | return 218 | } 219 | 220 | // Create a cancellable context for background processors. 221 | procCtx, procCancel := context.WithCancel(ctx) 222 | 223 | // Connected successfully. Start the sender. This will also send the first 224 | // status report. 225 | if err := c.sender.Start(procCtx, c.conn); err != nil { 226 | c.common.Logger.Errorf("Failed to send first status report: %v", err) 227 | // We could not send the report, the only thing we can do is start over. 228 | _ = c.conn.Close() 229 | procCancel() 230 | return 231 | } 232 | 233 | // First status report sent. Now loop to receive and process messages. 234 | r := internal.NewWSReceiver( 235 | c.common.Logger, 236 | c.common.Callbacks, 237 | c.conn, 238 | c.sender, 239 | &c.common.ClientSyncedState, 240 | c.common.PackagesStateProvider, 241 | c.common.Capabilities, 242 | ) 243 | r.ReceiverLoop(ctx) 244 | 245 | // Stop the background processors. 246 | procCancel() 247 | 248 | // If we exited receiverLoop it means there is a connection error, we cannot 249 | // read messages anymore. We need to start over. 250 | 251 | // Close the connection to unblock the WSSender as well. 252 | _ = c.conn.Close() 253 | 254 | // Wait for WSSender to stop. 255 | c.sender.WaitToStop() 256 | } 257 | 258 | func (c *wsClient) runUntilStopped(ctx context.Context) { 259 | // Iterates until we detect that the client is stopping. 260 | for { 261 | if c.common.IsStopping() { 262 | return 263 | } 264 | 265 | c.runOneCycle(ctx) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /client/internal/httpsender.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/cenkalti/backoff/v4" 15 | "github.com/open-telemetry/opamp-go/internal" 16 | "google.golang.org/protobuf/proto" 17 | 18 | "github.com/open-telemetry/opamp-go/client/types" 19 | "github.com/open-telemetry/opamp-go/protobufs" 20 | ) 21 | 22 | const OpAMPPlainHTTPMethod = "POST" 23 | const defaultPollingIntervalMs = 30 * 1000 // default interval is 30 seconds. 24 | 25 | const headerContentEncoding = "Content-Encoding" 26 | const encodingTypeGZip = "gzip" 27 | 28 | // HTTPSender allows scheduling messages to send. Once run, it will loop through 29 | // a request/response cycle for each message to send and will process all received 30 | // responses using a receivedProcessor. If there are no pending messages to send 31 | // the HTTPSender will wait for the configured polling interval. 32 | type HTTPSender struct { 33 | SenderCommon 34 | 35 | url string 36 | logger types.Logger 37 | client *http.Client 38 | callbacks types.Callbacks 39 | pollingIntervalMs int64 40 | compressionEnabled bool 41 | 42 | // Headers to send with all requests. 43 | requestHeader http.Header 44 | 45 | // Processor to handle received messages. 46 | receiveProcessor receivedProcessor 47 | } 48 | 49 | // NewHTTPSender creates a new Sender that uses HTTP to send messages 50 | // with default settings. 51 | func NewHTTPSender(logger types.Logger) *HTTPSender { 52 | h := &HTTPSender{ 53 | SenderCommon: NewSenderCommon(), 54 | logger: logger, 55 | client: http.DefaultClient, 56 | pollingIntervalMs: defaultPollingIntervalMs, 57 | } 58 | // initialize the headers with no additional headers 59 | h.SetRequestHeader(nil) 60 | return h 61 | } 62 | 63 | // Run starts the processing loop that will perform the HTTP request/response. 64 | // When there are no more messages to send Run will suspend until either there is 65 | // a new message to send or the polling interval elapses. 66 | // Should not be called concurrently with itself. Can be called concurrently with 67 | // modifying NextMessage(). 68 | // Run continues until ctx is cancelled. 69 | func (h *HTTPSender) Run( 70 | ctx context.Context, 71 | url string, 72 | callbacks types.Callbacks, 73 | clientSyncedState *ClientSyncedState, 74 | packagesStateProvider types.PackagesStateProvider, 75 | capabilities protobufs.AgentCapabilities, 76 | ) { 77 | h.url = url 78 | h.callbacks = callbacks 79 | h.receiveProcessor = newReceivedProcessor(h.logger, callbacks, h, clientSyncedState, packagesStateProvider, capabilities) 80 | 81 | for { 82 | pollingTimer := time.NewTimer(time.Millisecond * time.Duration(atomic.LoadInt64(&h.pollingIntervalMs))) 83 | select { 84 | case <-h.hasPendingMessage: 85 | // Have something to send. Stop the polling timer and send what we have. 86 | pollingTimer.Stop() 87 | h.makeOneRequestRoundtrip(ctx) 88 | 89 | case <-pollingTimer.C: 90 | // Polling interval has passed. Force a status update. 91 | h.NextMessage().Update(func(msg *protobufs.AgentToServer) {}) 92 | // This will make hasPendingMessage channel readable, so we will enter 93 | // the case above on the next iteration of the loop. 94 | h.ScheduleSend() 95 | break 96 | 97 | case <-ctx.Done(): 98 | return 99 | } 100 | } 101 | } 102 | 103 | // SetRequestHeader sets additional HTTP headers to send with all future requests. 104 | // Should not be called concurrently with any other method. 105 | func (h *HTTPSender) SetRequestHeader(header http.Header) { 106 | if header == nil { 107 | header = http.Header{} 108 | } 109 | h.requestHeader = header 110 | h.requestHeader.Set(headerContentType, contentTypeProtobuf) 111 | } 112 | 113 | // makeOneRequestRoundtrip sends a request and receives a response. 114 | // It will retry the request if the server responds with too many 115 | // requests or unavailable status. 116 | func (h *HTTPSender) makeOneRequestRoundtrip(ctx context.Context) { 117 | resp, err := h.sendRequestWithRetries(ctx) 118 | if err != nil { 119 | return 120 | } 121 | if resp == nil { 122 | // No request was sent and nothing to receive. 123 | return 124 | } 125 | h.receiveResponse(ctx, resp) 126 | } 127 | 128 | func (h *HTTPSender) sendRequestWithRetries(ctx context.Context) (*http.Response, error) { 129 | req, err := h.prepareRequest(ctx) 130 | if err != nil { 131 | if errors.Is(err, context.Canceled) { 132 | h.logger.Debugf("Client is stopped, will not try anymore.") 133 | } else { 134 | h.logger.Errorf("Failed prepare request (%v), will not try anymore.", err) 135 | } 136 | return nil, err 137 | } 138 | if req == nil { 139 | // Nothing to send. 140 | return nil, nil 141 | } 142 | 143 | // Repeatedly try requests with a backoff strategy. 144 | infiniteBackoff := backoff.NewExponentialBackOff() 145 | // Make backoff run forever. 146 | infiniteBackoff.MaxElapsedTime = 0 147 | 148 | interval := time.Duration(0) 149 | 150 | for { 151 | timer := time.NewTimer(interval) 152 | interval = infiniteBackoff.NextBackOff() 153 | 154 | select { 155 | case <-timer.C: 156 | { 157 | resp, err := h.client.Do(req) 158 | if err == nil { 159 | switch resp.StatusCode { 160 | case http.StatusOK: 161 | // We consider it connected if we receive 200 status from the Server. 162 | h.callbacks.OnConnect() 163 | return resp, nil 164 | 165 | case http.StatusTooManyRequests, http.StatusServiceUnavailable: 166 | interval = recalculateInterval(interval, resp) 167 | err = fmt.Errorf("server response code=%d", resp.StatusCode) 168 | 169 | default: 170 | return nil, fmt.Errorf("invalid response from server: %d", resp.StatusCode) 171 | } 172 | } else if errors.Is(err, context.Canceled) { 173 | h.logger.Debugf("Client is stopped, will not try anymore.") 174 | return nil, err 175 | } 176 | 177 | h.logger.Errorf("Failed to do HTTP request (%v), will retry", err) 178 | h.callbacks.OnConnectFailed(err) 179 | } 180 | 181 | case <-ctx.Done(): 182 | h.logger.Debugf("Client is stopped, will not try anymore.") 183 | return nil, ctx.Err() 184 | } 185 | } 186 | } 187 | 188 | func recalculateInterval(interval time.Duration, resp *http.Response) time.Duration { 189 | retryAfter := internal.ExtractRetryAfterHeader(resp) 190 | if retryAfter.Defined && retryAfter.Duration > interval { 191 | // If the Server suggested connecting later than our interval 192 | // then honour Server's request, otherwise wait at least 193 | // as much as we calculated. 194 | interval = retryAfter.Duration 195 | } 196 | return interval 197 | } 198 | 199 | func (h *HTTPSender) prepareRequest(ctx context.Context) (*http.Request, error) { 200 | msgToSend := h.nextMessage.PopPending() 201 | if msgToSend == nil || proto.Equal(msgToSend, &protobufs.AgentToServer{}) { 202 | // There is no pending message or the message is empty. 203 | // Nothing to send. 204 | return nil, nil 205 | } 206 | 207 | data, err := proto.Marshal(msgToSend) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | var body io.Reader 213 | 214 | if h.compressionEnabled { 215 | var buf bytes.Buffer 216 | g := gzip.NewWriter(&buf) 217 | if _, err = g.Write(data); err != nil { 218 | h.logger.Errorf("Failed to compress message: %v", err) 219 | return nil, err 220 | } 221 | if err = g.Close(); err != nil { 222 | h.logger.Errorf("Failed to close the writer: %v", err) 223 | return nil, err 224 | } 225 | body = &buf 226 | } else { 227 | body = bytes.NewReader(data) 228 | } 229 | req, err := http.NewRequestWithContext(ctx, OpAMPPlainHTTPMethod, h.url, body) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | req.Header = h.requestHeader 235 | return req, nil 236 | } 237 | 238 | func (h *HTTPSender) receiveResponse(ctx context.Context, resp *http.Response) { 239 | msgBytes, err := io.ReadAll(resp.Body) 240 | if err != nil { 241 | _ = resp.Body.Close() 242 | h.logger.Errorf("cannot read response body: %v", err) 243 | return 244 | } 245 | _ = resp.Body.Close() 246 | 247 | var response protobufs.ServerToAgent 248 | if err := proto.Unmarshal(msgBytes, &response); err != nil { 249 | h.logger.Errorf("cannot unmarshal response: %v", err) 250 | return 251 | } 252 | 253 | h.receiveProcessor.ProcessReceivedMessage(ctx, &response) 254 | } 255 | 256 | // SetPollingInterval sets the interval between polling. Has effect starting from the 257 | // next polling cycle. 258 | func (h *HTTPSender) SetPollingInterval(duration time.Duration) { 259 | atomic.StoreInt64(&h.pollingIntervalMs, duration.Milliseconds()) 260 | } 261 | 262 | func (h *HTTPSender) EnableCompression() { 263 | h.compressionEnabled = true 264 | h.requestHeader.Set(headerContentEncoding, encodingTypeGZip) 265 | } 266 | -------------------------------------------------------------------------------- /client/types/callbacks.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/open-telemetry/opamp-go/protobufs" 7 | ) 8 | 9 | type MessageData struct { 10 | // RemoteConfig is offered by the Server. The Agent must process it and call 11 | // OpAMPClient.SetRemoteConfigStatus to indicate success or failure. If the 12 | // effective config has changed as a result of processing the Agent must also call 13 | // OpAMPClient.UpdateEffectiveConfig. SetRemoteConfigStatus and UpdateEffectiveConfig 14 | // may be called from OnMessage handler or after OnMessage returns. 15 | RemoteConfig *protobufs.AgentRemoteConfig 16 | 17 | // Connection settings are offered by the Server. These fields should be processed 18 | // as described in the ConnectionSettingsOffers message. 19 | OwnMetricsConnSettings *protobufs.TelemetryConnectionSettings 20 | OwnTracesConnSettings *protobufs.TelemetryConnectionSettings 21 | OwnLogsConnSettings *protobufs.TelemetryConnectionSettings 22 | OtherConnSettings map[string]*protobufs.OtherConnectionSettings 23 | 24 | // PackagesAvailable offered by the Server. The Agent must process the offer. 25 | // The typical way to process is to call PackageSyncer.Sync() function, which will 26 | // take care of reporting the status to the Server as processing happens. 27 | // 28 | // If PackageSyncer.Sync() function is not called then it is the responsibility of 29 | // OnMessage handler to do the processing and call OpAMPClient.SetPackageStatuses to 30 | // reflect the processing status. SetPackageStatuses may be called from OnMessage 31 | // handler or after OnMessage returns. 32 | PackagesAvailable *protobufs.PackagesAvailable 33 | PackageSyncer PackagesSyncer 34 | 35 | // AgentIdentification indicates a new identification received from the Server. 36 | // The Agent must save this identification and use it in the future instantiations 37 | // of OpAMPClient. 38 | AgentIdentification *protobufs.AgentIdentification 39 | } 40 | 41 | type Callbacks interface { 42 | // OnConnect is called when the connection is successfully established to the Server. 43 | // May be called after Start() is called and every time a connection is established to the Server. 44 | // For WebSocket clients this is called after the handshake is completed without any error. 45 | // For HTTP clients this is called for any request if the response status is OK. 46 | OnConnect() 47 | 48 | // OnConnectFailed is called when the connection to the Server cannot be established. 49 | // May be called after Start() is called and tries to connect to the Server. 50 | // May also be called if the connection is lost and reconnection attempt fails. 51 | OnConnectFailed(err error) 52 | 53 | // OnError is called when the Server reports an error in response to some previously 54 | // sent request. Useful for logging purposes. The Agent should not attempt to process 55 | // the error by reconnecting or retrying previous operations. The client handles the 56 | // ErrorResponse_UNAVAILABLE case internally by performing retries as necessary. 57 | OnError(err *protobufs.ServerErrorResponse) 58 | 59 | // OnMessage is called when the Agent receives a message that needs processing. 60 | // See MessageData definition for the data that may be available for processing. 61 | // During OnMessage execution the OpAMPClient functions that change the status 62 | // of the client may be called, e.g. if RemoteConfig is processed then 63 | // SetRemoteConfigStatus should be called to reflect the processing result. 64 | // These functions may also be called after OnMessage returns. This is advisable 65 | // if processing can take a long time. In that case returning quickly is preferable 66 | // to avoid blocking the OpAMPClient. 67 | OnMessage(ctx context.Context, msg *MessageData) 68 | 69 | // OnOpampConnectionSettings is called when the Agent receives an OpAMP 70 | // connection settings offer from the Server. Typically, the settings can specify 71 | // authorization headers or TLS certificate, potentially also a different 72 | // OpAMP destination to work with. 73 | // 74 | // The Agent should process the offer and return an error if the Agent does not 75 | // want to accept the settings (e.g. if the TSL certificate in the settings 76 | // cannot be verified). 77 | // 78 | // If OnOpampConnectionSettings returns nil and then the caller will 79 | // attempt to reconnect to the OpAMP Server using the new settings. 80 | // If the connection fails the settings will be rejected and an error will 81 | // be reported to the Server. If the connection succeeds the new settings 82 | // will be used by the client from that moment on. 83 | // 84 | // Only one OnOpampConnectionSettings call can be active at any time. 85 | // See OnRemoteConfig for the behavior. 86 | OnOpampConnectionSettings( 87 | ctx context.Context, 88 | settings *protobufs.OpAMPConnectionSettings, 89 | ) error 90 | 91 | // OnOpampConnectionSettingsAccepted will be called after the settings are 92 | // verified and accepted (OnOpampConnectionSettingsOffer and connection using 93 | // new settings succeeds). The Agent should store the settings and use them 94 | // in the future. Old connection settings should be forgotten. 95 | OnOpampConnectionSettingsAccepted( 96 | settings *protobufs.OpAMPConnectionSettings, 97 | ) 98 | 99 | // For all methods that accept a context parameter the caller may cancel the 100 | // context if processing takes too long. In that case the method should return 101 | // as soon as possible with an error. 102 | 103 | // SaveRemoteConfigStatus is called after OnRemoteConfig returns. The status 104 | // will be set either as APPLIED or FAILED depending on whether OnRemoteConfig 105 | // returned a success or error. 106 | // The Agent must remember this RemoteConfigStatus and supply in the future 107 | // calls to Start() in StartSettings.RemoteConfigStatus. 108 | SaveRemoteConfigStatus(ctx context.Context, status *protobufs.RemoteConfigStatus) 109 | 110 | // GetEffectiveConfig returns the current effective config. Only one 111 | // GetEffectiveConfig call can be active at any time. Until GetEffectiveConfig 112 | // returns it will not be called again. 113 | GetEffectiveConfig(ctx context.Context) (*protobufs.EffectiveConfig, error) 114 | 115 | // OnCommand is called when the Server requests that the connected Agent perform a command. 116 | OnCommand(command *protobufs.ServerToAgentCommand) error 117 | } 118 | 119 | type CallbacksStruct struct { 120 | OnConnectFunc func() 121 | OnConnectFailedFunc func(err error) 122 | OnErrorFunc func(err *protobufs.ServerErrorResponse) 123 | 124 | OnMessageFunc func(ctx context.Context, msg *MessageData) 125 | 126 | OnOpampConnectionSettingsFunc func( 127 | ctx context.Context, 128 | settings *protobufs.OpAMPConnectionSettings, 129 | ) error 130 | OnOpampConnectionSettingsAcceptedFunc func( 131 | settings *protobufs.OpAMPConnectionSettings, 132 | ) 133 | 134 | OnCommandFunc func(command *protobufs.ServerToAgentCommand) error 135 | 136 | SaveRemoteConfigStatusFunc func(ctx context.Context, status *protobufs.RemoteConfigStatus) 137 | GetEffectiveConfigFunc func(ctx context.Context) (*protobufs.EffectiveConfig, error) 138 | } 139 | 140 | var _ Callbacks = (*CallbacksStruct)(nil) 141 | 142 | func (c CallbacksStruct) OnConnect() { 143 | if c.OnConnectFunc != nil { 144 | c.OnConnectFunc() 145 | } 146 | } 147 | 148 | func (c CallbacksStruct) OnConnectFailed(err error) { 149 | if c.OnConnectFailedFunc != nil { 150 | c.OnConnectFailedFunc(err) 151 | } 152 | } 153 | 154 | func (c CallbacksStruct) OnError(err *protobufs.ServerErrorResponse) { 155 | if c.OnErrorFunc != nil { 156 | c.OnErrorFunc(err) 157 | } 158 | } 159 | 160 | func (c CallbacksStruct) OnMessage(ctx context.Context, msg *MessageData) { 161 | if c.OnMessageFunc != nil { 162 | c.OnMessageFunc(ctx, msg) 163 | } 164 | } 165 | 166 | func (c CallbacksStruct) SaveRemoteConfigStatus(ctx context.Context, status *protobufs.RemoteConfigStatus) { 167 | if c.SaveRemoteConfigStatusFunc != nil { 168 | c.SaveRemoteConfigStatusFunc(ctx, status) 169 | } 170 | } 171 | 172 | func (c CallbacksStruct) GetEffectiveConfig(ctx context.Context) (*protobufs.EffectiveConfig, error) { 173 | if c.GetEffectiveConfigFunc != nil { 174 | return c.GetEffectiveConfigFunc(ctx) 175 | } 176 | return nil, nil 177 | } 178 | 179 | func (c CallbacksStruct) OnOpampConnectionSettings( 180 | ctx context.Context, settings *protobufs.OpAMPConnectionSettings, 181 | ) error { 182 | if c.OnOpampConnectionSettingsFunc != nil { 183 | return c.OnOpampConnectionSettingsFunc(ctx, settings) 184 | } 185 | return nil 186 | } 187 | 188 | func (c CallbacksStruct) OnOpampConnectionSettingsAccepted(settings *protobufs.OpAMPConnectionSettings) { 189 | if c.OnOpampConnectionSettingsAcceptedFunc != nil { 190 | c.OnOpampConnectionSettingsAcceptedFunc(settings) 191 | } 192 | } 193 | 194 | func (c CallbacksStruct) OnCommand(command *protobufs.ServerToAgentCommand) error { 195 | if c.OnCommandFunc != nil { 196 | return c.OnCommandFunc(command) 197 | } 198 | return nil 199 | } 200 | -------------------------------------------------------------------------------- /server/serverimpl.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "errors" 8 | "io" 9 | "net" 10 | "net/http" 11 | 12 | "github.com/gorilla/websocket" 13 | "google.golang.org/protobuf/proto" 14 | 15 | "github.com/open-telemetry/opamp-go/client/types" 16 | "github.com/open-telemetry/opamp-go/internal" 17 | "github.com/open-telemetry/opamp-go/protobufs" 18 | ) 19 | 20 | var ( 21 | errAlreadyStarted = errors.New("already started") 22 | ) 23 | 24 | const defaultOpAMPPath = "/v1/opamp" 25 | const headerContentType = "Content-Type" 26 | const headerContentEncoding = "Content-Encoding" 27 | const headerAcceptEncoding = "Accept-Encoding" 28 | const contentEncodingGzip = "gzip" 29 | const contentTypeProtobuf = "application/x-protobuf" 30 | 31 | type server struct { 32 | logger types.Logger 33 | settings Settings 34 | 35 | // Upgrader to use to upgrade HTTP to WebSocket. 36 | wsUpgrader websocket.Upgrader 37 | 38 | // The listening HTTP Server after successful Start() call. Nil if Start() 39 | // is not called or was not successful. 40 | httpServer *http.Server 41 | } 42 | 43 | var _ OpAMPServer = (*server)(nil) 44 | 45 | func New(logger types.Logger) *server { 46 | if logger == nil { 47 | logger = &internal.NopLogger{} 48 | } 49 | 50 | return &server{logger: logger} 51 | } 52 | 53 | func (s *server) Attach(settings Settings) (HTTPHandlerFunc, error) { 54 | s.settings = settings 55 | // TODO: Add support for compression using Upgrader.EnableCompression field. 56 | s.wsUpgrader = websocket.Upgrader{} 57 | return s.httpHandler, nil 58 | } 59 | 60 | func (s *server) Start(settings StartSettings) error { 61 | if s.httpServer != nil { 62 | return errAlreadyStarted 63 | } 64 | 65 | _, err := s.Attach(settings.Settings) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Prepare handling OpAMP incoming HTTP requests on the requests URL path. 71 | mux := http.NewServeMux() 72 | 73 | path := settings.ListenPath 74 | if path == "" { 75 | path = defaultOpAMPPath 76 | } 77 | 78 | mux.HandleFunc(path, s.httpHandler) 79 | 80 | hs := &http.Server{ 81 | Handler: mux, 82 | Addr: settings.ListenEndpoint, 83 | TLSConfig: settings.TLSConfig, 84 | ConnContext: contextWithConn, 85 | } 86 | s.httpServer = hs 87 | 88 | listenAddr := s.httpServer.Addr 89 | 90 | // Start the HTTP Server in background. 91 | if hs.TLSConfig != nil { 92 | if listenAddr == "" { 93 | listenAddr = ":https" 94 | } 95 | err = s.startHttpServer( 96 | listenAddr, 97 | func(l net.Listener) error { return hs.ServeTLS(l, "", "") }, 98 | ) 99 | } else { 100 | if listenAddr == "" { 101 | listenAddr = ":http" 102 | } 103 | err = s.startHttpServer( 104 | listenAddr, 105 | func(l net.Listener) error { return hs.Serve(l) }, 106 | ) 107 | } 108 | return err 109 | } 110 | 111 | func (s *server) startHttpServer(listenAddr string, serveFunc func(l net.Listener) error) error { 112 | // If the listen address is not specified use the default. 113 | ln, err := net.Listen("tcp", listenAddr) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | // Begin serving connections in the background. 119 | go func() { 120 | err = serveFunc(ln) 121 | 122 | // ErrServerClosed is expected after successful Stop(), so we won't log that 123 | // particular error. 124 | if err != nil && err != http.ErrServerClosed { 125 | s.logger.Errorf("Error running HTTP Server: %v", err) 126 | } 127 | }() 128 | 129 | return nil 130 | } 131 | 132 | func (s *server) Stop(ctx context.Context) error { 133 | if s.httpServer != nil { 134 | defer func() { s.httpServer = nil }() 135 | // This stops accepting new connections. TODO: close existing 136 | // connections and wait them to be terminated. 137 | return s.httpServer.Shutdown(ctx) 138 | } 139 | return nil 140 | } 141 | 142 | func (s *server) httpHandler(w http.ResponseWriter, req *http.Request) { 143 | if s.settings.Callbacks != nil { 144 | resp := s.settings.Callbacks.OnConnecting(req) 145 | if !resp.Accept { 146 | // HTTP connection is not accepted. Set the response headers. 147 | for k, v := range resp.HTTPResponseHeader { 148 | w.Header().Set(k, v) 149 | } 150 | // And write the response status code. 151 | w.WriteHeader(resp.HTTPStatusCode) 152 | return 153 | } 154 | } 155 | 156 | // HTTP connection is accepted. Check if it is a plain HTTP request. 157 | 158 | if req.Header.Get(headerContentType) == contentTypeProtobuf { 159 | // Yes, a plain HTTP request. 160 | s.handlePlainHTTPRequest(req, w) 161 | return 162 | } 163 | 164 | // No, it is a WebSocket. Upgrade it. 165 | conn, err := s.wsUpgrader.Upgrade(w, req, nil) 166 | if err != nil { 167 | s.logger.Errorf("Cannot upgrade HTTP connection to WebSocket: %v", err) 168 | return 169 | } 170 | 171 | // Return from this func to reduce memory usage. 172 | // Handle the connection on a separate goroutine. 173 | go s.handleWSConnection(conn) 174 | } 175 | 176 | func (s *server) handleWSConnection(wsConn *websocket.Conn) { 177 | agentConn := wsConnection{wsConn: wsConn} 178 | 179 | defer func() { 180 | // Close the connection when all is done. 181 | defer func() { 182 | err := wsConn.Close() 183 | if err != nil { 184 | s.logger.Errorf("error closing the WebSocket connection: %v", err) 185 | } 186 | }() 187 | 188 | if s.settings.Callbacks != nil { 189 | s.settings.Callbacks.OnConnectionClose(agentConn) 190 | } 191 | }() 192 | 193 | if s.settings.Callbacks != nil { 194 | s.settings.Callbacks.OnConnected(agentConn) 195 | } 196 | 197 | // Loop until fail to read from the WebSocket connection. 198 | for { 199 | // Block until the next message can be read. 200 | mt, bytes, err := wsConn.ReadMessage() 201 | if err != nil { 202 | if !websocket.IsUnexpectedCloseError(err) { 203 | s.logger.Errorf("Cannot read a message from WebSocket: %v", err) 204 | break 205 | } 206 | // This is a normal closing of the WebSocket connection. 207 | s.logger.Debugf("Agent disconnected: %v", err) 208 | break 209 | } 210 | if mt != websocket.BinaryMessage { 211 | s.logger.Errorf("Received unexpected message type from WebSocket: %v", mt) 212 | continue 213 | } 214 | 215 | // Decode WebSocket message as a Protobuf message. 216 | var request protobufs.AgentToServer 217 | err = proto.Unmarshal(bytes, &request) 218 | if err != nil { 219 | s.logger.Errorf("Cannot decode message from WebSocket: %v", err) 220 | continue 221 | } 222 | 223 | if s.settings.Callbacks != nil { 224 | response := s.settings.Callbacks.OnMessage(agentConn, &request) 225 | if response.InstanceUid == "" { 226 | response.InstanceUid = request.InstanceUid 227 | } 228 | err = agentConn.Send(context.Background(), response) 229 | if err != nil { 230 | s.logger.Errorf("Cannot send message to WebSocket: %v", err) 231 | } 232 | } 233 | } 234 | } 235 | 236 | func decompressGzip(data []byte) ([]byte, error) { 237 | r, err := gzip.NewReader(bytes.NewBuffer(data)) 238 | if err != nil { 239 | return nil, err 240 | } 241 | defer r.Close() 242 | return io.ReadAll(r) 243 | } 244 | 245 | func (s *server) readReqBody(req *http.Request) ([]byte, error) { 246 | data, err := io.ReadAll(req.Body) 247 | if err != nil { 248 | return nil, err 249 | } 250 | if req.Header.Get(headerContentEncoding) == contentEncodingGzip { 251 | data, err = decompressGzip(data) 252 | if err != nil { 253 | return nil, err 254 | } 255 | } 256 | return data, nil 257 | } 258 | 259 | func compressGzip(data []byte) ([]byte, error) { 260 | var buf bytes.Buffer 261 | w := gzip.NewWriter(&buf) 262 | _, err := w.Write(data) 263 | if err != nil { 264 | return nil, err 265 | } 266 | err = w.Close() 267 | if err != nil { 268 | return nil, err 269 | } 270 | return buf.Bytes(), nil 271 | } 272 | 273 | func (s *server) handlePlainHTTPRequest(req *http.Request, w http.ResponseWriter) { 274 | bytes, err := s.readReqBody(req) 275 | if err != nil { 276 | s.logger.Debugf("Cannot read HTTP body: %v", err) 277 | w.WriteHeader(http.StatusBadRequest) 278 | return 279 | } 280 | 281 | // Decode the message as a Protobuf message. 282 | var request protobufs.AgentToServer 283 | err = proto.Unmarshal(bytes, &request) 284 | if err != nil { 285 | s.logger.Debugf("Cannot decode message from HTTP Body: %v", err) 286 | w.WriteHeader(http.StatusBadRequest) 287 | return 288 | } 289 | 290 | agentConn := httpConnection{ 291 | conn: connFromRequest(req), 292 | } 293 | 294 | if s.settings.Callbacks == nil { 295 | w.WriteHeader(http.StatusInternalServerError) 296 | return 297 | } 298 | 299 | s.settings.Callbacks.OnConnected(agentConn) 300 | 301 | defer func() { 302 | // Indicate via the callback that the OpAMP Connection is closed. From OpAMP 303 | // perspective the connection represented by this http request 304 | // is closed. It is not possible to send or receive more OpAMP messages 305 | // via this agentConn. 306 | s.settings.Callbacks.OnConnectionClose(agentConn) 307 | }() 308 | 309 | response := s.settings.Callbacks.OnMessage(agentConn, &request) 310 | 311 | // Set the InstanceUid if it is not set by the callback. 312 | if response.InstanceUid == "" { 313 | response.InstanceUid = request.InstanceUid 314 | } 315 | 316 | // Marshal the response. 317 | bytes, err = proto.Marshal(response) 318 | if err != nil { 319 | w.WriteHeader(http.StatusInternalServerError) 320 | return 321 | } 322 | 323 | // Send the response. 324 | w.Header().Set(headerContentType, contentTypeProtobuf) 325 | if req.Header.Get(headerAcceptEncoding) == contentEncodingGzip { 326 | bytes, err = compressGzip(bytes) 327 | if err != nil { 328 | s.logger.Errorf("Cannot compress response: %v", err) 329 | w.WriteHeader(http.StatusInternalServerError) 330 | return 331 | } 332 | w.Header().Set(headerContentEncoding, contentEncodingGzip) 333 | } 334 | _, err = w.Write(bytes) 335 | 336 | if err != nil { 337 | s.logger.Debugf("Cannot send HTTP response: %v", err) 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /internal/examples/agent/agent/agent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "runtime" 9 | "sort" 10 | "time" 11 | 12 | "github.com/knadh/koanf" 13 | "github.com/knadh/koanf/parsers/yaml" 14 | "github.com/knadh/koanf/providers/rawbytes" 15 | "github.com/oklog/ulid/v2" 16 | 17 | "github.com/open-telemetry/opamp-go/client" 18 | "github.com/open-telemetry/opamp-go/client/types" 19 | "github.com/open-telemetry/opamp-go/protobufs" 20 | ) 21 | 22 | const localConfig = ` 23 | exporters: 24 | otlp: 25 | endpoint: localhost:1111 26 | 27 | receivers: 28 | otlp: 29 | protocols: 30 | grpc: {} 31 | http: {} 32 | 33 | service: 34 | pipelines: 35 | traces: 36 | receivers: [otlp] 37 | processors: [] 38 | exporters: [otlp] 39 | ` 40 | 41 | type Agent struct { 42 | logger types.Logger 43 | 44 | agentType string 45 | agentVersion string 46 | 47 | effectiveConfig string 48 | 49 | instanceId ulid.ULID 50 | 51 | agentDescription *protobufs.AgentDescription 52 | 53 | opampClient client.OpAMPClient 54 | 55 | remoteConfigStatus *protobufs.RemoteConfigStatus 56 | 57 | metricReporter *MetricReporter 58 | } 59 | 60 | func NewAgent(logger types.Logger, agentType string, agentVersion string) *Agent { 61 | agent := &Agent{ 62 | effectiveConfig: localConfig, 63 | logger: logger, 64 | agentType: agentType, 65 | agentVersion: agentVersion, 66 | } 67 | 68 | agent.createAgentIdentity() 69 | agent.logger.Debugf("Agent starting, id=%v, type=%s, version=%s.", 70 | agent.instanceId.String(), agentType, agentVersion) 71 | 72 | agent.loadLocalConfig() 73 | if err := agent.start(); err != nil { 74 | agent.logger.Errorf("Cannot start OpAMP client: %v", err) 75 | return nil 76 | } 77 | 78 | return agent 79 | } 80 | 81 | func (agent *Agent) start() error { 82 | agent.opampClient = client.NewWebSocket(agent.logger) 83 | 84 | settings := types.StartSettings{ 85 | OpAMPServerURL: "ws://127.0.0.1:4320/v1/opamp", 86 | InstanceUid: agent.instanceId.String(), 87 | Callbacks: types.CallbacksStruct{ 88 | OnConnectFunc: func() { 89 | agent.logger.Debugf("Connected to the server.") 90 | }, 91 | OnConnectFailedFunc: func(err error) { 92 | agent.logger.Errorf("Failed to connect to the server: %v", err) 93 | }, 94 | OnErrorFunc: func(err *protobufs.ServerErrorResponse) { 95 | agent.logger.Errorf("Server returned an error response: %v", err.ErrorMessage) 96 | }, 97 | SaveRemoteConfigStatusFunc: func(_ context.Context, status *protobufs.RemoteConfigStatus) { 98 | agent.remoteConfigStatus = status 99 | }, 100 | GetEffectiveConfigFunc: func(ctx context.Context) (*protobufs.EffectiveConfig, error) { 101 | return agent.composeEffectiveConfig(), nil 102 | }, 103 | OnMessageFunc: agent.onMessage, 104 | }, 105 | RemoteConfigStatus: agent.remoteConfigStatus, 106 | Capabilities: protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig | 107 | protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig | 108 | protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig | 109 | protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnMetrics, 110 | } 111 | err := agent.opampClient.SetAgentDescription(agent.agentDescription) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | agent.logger.Debugf("Starting OpAMP client...") 117 | 118 | err = agent.opampClient.Start(context.Background(), settings) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | agent.logger.Debugf("OpAMP Client started.") 124 | 125 | return nil 126 | } 127 | 128 | func (agent *Agent) createAgentIdentity() { 129 | // Generate instance id. 130 | entropy := ulid.Monotonic(rand.New(rand.NewSource(0)), 0) 131 | agent.instanceId = ulid.MustNew(ulid.Timestamp(time.Now()), entropy) 132 | 133 | hostname, _ := os.Hostname() 134 | 135 | // Create Agent description. 136 | agent.agentDescription = &protobufs.AgentDescription{ 137 | IdentifyingAttributes: []*protobufs.KeyValue{ 138 | { 139 | Key: "service.name", 140 | Value: &protobufs.AnyValue{ 141 | Value: &protobufs.AnyValue_StringValue{StringValue: agent.agentType}, 142 | }, 143 | }, 144 | { 145 | Key: "service.version", 146 | Value: &protobufs.AnyValue{ 147 | Value: &protobufs.AnyValue_StringValue{StringValue: agent.agentVersion}, 148 | }, 149 | }, 150 | }, 151 | NonIdentifyingAttributes: []*protobufs.KeyValue{ 152 | { 153 | Key: "os.family", 154 | Value: &protobufs.AnyValue{ 155 | Value: &protobufs.AnyValue_StringValue{ 156 | StringValue: runtime.GOOS, 157 | }, 158 | }, 159 | }, 160 | { 161 | Key: "host.name", 162 | Value: &protobufs.AnyValue{ 163 | Value: &protobufs.AnyValue_StringValue{ 164 | StringValue: hostname, 165 | }, 166 | }, 167 | }, 168 | }, 169 | } 170 | } 171 | 172 | func (agent *Agent) updateAgentIdentity(instanceId ulid.ULID) { 173 | agent.logger.Debugf("Agent identify is being changed from id=%v to id=%v", 174 | agent.instanceId.String(), 175 | instanceId.String()) 176 | agent.instanceId = instanceId 177 | 178 | if agent.metricReporter != nil { 179 | // TODO: reinit or update meter (possibly using a single function to update all own connection settings 180 | // or with having a common resource factory or so) 181 | } 182 | } 183 | 184 | func (agent *Agent) loadLocalConfig() { 185 | var k = koanf.New(".") 186 | _ = k.Load(rawbytes.Provider([]byte(localConfig)), yaml.Parser()) 187 | 188 | effectiveConfigBytes, err := k.Marshal(yaml.Parser()) 189 | if err != nil { 190 | panic(err) 191 | } 192 | 193 | agent.effectiveConfig = string(effectiveConfigBytes) 194 | } 195 | 196 | func (agent *Agent) composeEffectiveConfig() *protobufs.EffectiveConfig { 197 | return &protobufs.EffectiveConfig{ 198 | ConfigMap: &protobufs.AgentConfigMap{ 199 | ConfigMap: map[string]*protobufs.AgentConfigFile{ 200 | "": {Body: []byte(agent.effectiveConfig)}, 201 | }, 202 | }, 203 | } 204 | } 205 | 206 | func (agent *Agent) initMeter(settings *protobufs.TelemetryConnectionSettings) { 207 | reporter, err := NewMetricReporter(agent.logger, settings, agent.agentType, agent.agentVersion, agent.instanceId) 208 | if err != nil { 209 | agent.logger.Errorf("Cannot collect metrics: %v", err) 210 | return 211 | } 212 | 213 | prevReporter := agent.metricReporter 214 | 215 | agent.metricReporter = reporter 216 | 217 | if prevReporter != nil { 218 | prevReporter.Shutdown() 219 | } 220 | 221 | return 222 | } 223 | 224 | type agentConfigFileItem struct { 225 | name string 226 | file *protobufs.AgentConfigFile 227 | } 228 | 229 | type agentConfigFileSlice []agentConfigFileItem 230 | 231 | func (a agentConfigFileSlice) Less(i, j int) bool { 232 | return a[i].name < a[j].name 233 | } 234 | 235 | func (a agentConfigFileSlice) Swap(i, j int) { 236 | t := a[i] 237 | a[i] = a[j] 238 | a[j] = t 239 | } 240 | 241 | func (a agentConfigFileSlice) Len() int { 242 | return len(a) 243 | } 244 | 245 | func (agent *Agent) applyRemoteConfig(config *protobufs.AgentRemoteConfig) (configChanged bool, err error) { 246 | if config == nil { 247 | return false, nil 248 | } 249 | 250 | agent.logger.Debugf("Received remote config from server, hash=%x.", config.ConfigHash) 251 | 252 | // Begin with local config. We will later merge received configs on top of it. 253 | var k = koanf.New(".") 254 | if err := k.Load(rawbytes.Provider([]byte(localConfig)), yaml.Parser()); err != nil { 255 | return false, err 256 | } 257 | 258 | orderedConfigs := agentConfigFileSlice{} 259 | for name, file := range config.Config.ConfigMap { 260 | if name == "" { 261 | // skip instance config 262 | continue 263 | } 264 | orderedConfigs = append(orderedConfigs, agentConfigFileItem{ 265 | name: name, 266 | file: file, 267 | }) 268 | } 269 | 270 | // Sort to make sure the order of merging is stable. 271 | sort.Sort(orderedConfigs) 272 | 273 | // Append instance config as the last item. 274 | instanceConfig := config.Config.ConfigMap[""] 275 | if instanceConfig != nil { 276 | orderedConfigs = append(orderedConfigs, agentConfigFileItem{ 277 | name: "", 278 | file: instanceConfig, 279 | }) 280 | } 281 | 282 | // Merge received configs. 283 | for _, item := range orderedConfigs { 284 | var k2 = koanf.New(".") 285 | err := k2.Load(rawbytes.Provider(item.file.Body), yaml.Parser()) 286 | if err != nil { 287 | return false, fmt.Errorf("cannot parse config named %s: %v", item.name, err) 288 | } 289 | err = k.Merge(k2) 290 | if err != nil { 291 | return false, fmt.Errorf("cannot merge config named %s: %v", item.name, err) 292 | } 293 | } 294 | 295 | // The merged final result is our effective config. 296 | effectiveConfigBytes, err := k.Marshal(yaml.Parser()) 297 | if err != nil { 298 | panic(err) 299 | } 300 | 301 | newEffectiveConfig := string(effectiveConfigBytes) 302 | configChanged = false 303 | if agent.effectiveConfig != newEffectiveConfig { 304 | agent.logger.Debugf("Effective config changed. Need to report to server.") 305 | agent.effectiveConfig = newEffectiveConfig 306 | configChanged = true 307 | } 308 | 309 | return configChanged, nil 310 | } 311 | 312 | func (agent *Agent) Shutdown() { 313 | agent.logger.Debugf("Agent shutting down...") 314 | if agent.opampClient != nil { 315 | _ = agent.opampClient.Stop(context.Background()) 316 | } 317 | } 318 | 319 | func (agent *Agent) onMessage(ctx context.Context, msg *types.MessageData) { 320 | configChanged := false 321 | if msg.RemoteConfig != nil { 322 | var err error 323 | configChanged, err = agent.applyRemoteConfig(msg.RemoteConfig) 324 | if err != nil { 325 | agent.opampClient.SetRemoteConfigStatus(&protobufs.RemoteConfigStatus{ 326 | LastRemoteConfigHash: msg.RemoteConfig.ConfigHash, 327 | Status: protobufs.RemoteConfigStatuses_RemoteConfigStatuses_FAILED, 328 | ErrorMessage: err.Error(), 329 | }) 330 | } else { 331 | agent.opampClient.SetRemoteConfigStatus(&protobufs.RemoteConfigStatus{ 332 | LastRemoteConfigHash: msg.RemoteConfig.ConfigHash, 333 | Status: protobufs.RemoteConfigStatuses_RemoteConfigStatuses_APPLIED, 334 | }) 335 | } 336 | } 337 | 338 | if msg.OwnMetricsConnSettings != nil { 339 | agent.initMeter(msg.OwnMetricsConnSettings) 340 | } 341 | 342 | if msg.AgentIdentification != nil { 343 | newInstanceId, err := ulid.Parse(msg.AgentIdentification.NewInstanceUid) 344 | if err != nil { 345 | agent.logger.Errorf(err.Error()) 346 | } 347 | agent.updateAgentIdentity(newInstanceId) 348 | } 349 | 350 | if configChanged { 351 | err := agent.opampClient.UpdateEffectiveConfig(ctx) 352 | if err != nil { 353 | agent.logger.Errorf(err.Error()) 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /client/internal/packagessyncer.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/open-telemetry/opamp-go/client/types" 11 | "github.com/open-telemetry/opamp-go/protobufs" 12 | ) 13 | 14 | // packagesSyncer performs the package syncing process. 15 | type packagesSyncer struct { 16 | logger types.Logger 17 | available *protobufs.PackagesAvailable 18 | clientSyncedState *ClientSyncedState 19 | localState types.PackagesStateProvider 20 | sender Sender 21 | 22 | statuses *protobufs.PackageStatuses 23 | doneCh chan struct{} 24 | } 25 | 26 | // NewPackagesSyncer creates a new packages syncer. 27 | func NewPackagesSyncer( 28 | logger types.Logger, 29 | available *protobufs.PackagesAvailable, 30 | sender Sender, 31 | clientSyncedState *ClientSyncedState, 32 | packagesStateProvider types.PackagesStateProvider, 33 | ) *packagesSyncer { 34 | return &packagesSyncer{ 35 | logger: logger, 36 | available: available, 37 | sender: sender, 38 | clientSyncedState: clientSyncedState, 39 | localState: packagesStateProvider, 40 | doneCh: make(chan struct{}), 41 | } 42 | } 43 | 44 | // Sync performs the package syncing process. 45 | func (s *packagesSyncer) Sync(ctx context.Context) error { 46 | 47 | defer func() { 48 | close(s.doneCh) 49 | }() 50 | 51 | // Prepare package statuses. 52 | if err := s.initStatuses(); err != nil { 53 | return err 54 | } 55 | 56 | if err := s.clientSyncedState.SetPackageStatuses(s.statuses); err != nil { 57 | return err 58 | } 59 | 60 | // Now do the actual syncing in the background. 61 | go s.doSync(ctx) 62 | 63 | return nil 64 | } 65 | 66 | func (s *packagesSyncer) Done() <-chan struct{} { 67 | return s.doneCh 68 | } 69 | 70 | // initStatuses initializes the "statuses" field from the current "localState". 71 | // The "statuses" will be updated as the syncing progresses and will be 72 | // periodically reported to the Server. 73 | func (s *packagesSyncer) initStatuses() error { 74 | if s.localState == nil { 75 | return errors.New("cannot sync packages because PackagesStateProvider is not provided") 76 | } 77 | 78 | // Restore statuses that were previously stored in the local state. 79 | var err error 80 | s.statuses, err = s.localState.LastReportedStatuses() 81 | if err != nil { 82 | return err 83 | } 84 | if s.statuses == nil { 85 | // No statuses are stored locally (maybe first time), just start with empty. 86 | s.statuses = &protobufs.PackageStatuses{} 87 | } 88 | 89 | if s.statuses.Packages == nil { 90 | s.statuses.Packages = map[string]*protobufs.PackageStatus{} 91 | } 92 | 93 | // Report to the Server the "all" hash that we received from the Server so that 94 | // the Server knows we are processing the right offer. 95 | s.statuses.ServerProvidedAllPackagesHash = s.available.AllPackagesHash 96 | 97 | return nil 98 | } 99 | 100 | // doSync performs the actual syncing process. 101 | func (s *packagesSyncer) doSync(ctx context.Context) { 102 | hash, err := s.localState.AllPackagesHash() 103 | if err != nil { 104 | s.logger.Errorf("Package syncing failed: %V", err) 105 | return 106 | } 107 | if bytes.Compare(hash, s.available.AllPackagesHash) == 0 { 108 | s.logger.Debugf("All packages are already up to date.") 109 | return 110 | } 111 | 112 | failed := false 113 | if err := s.deleteUnneededLocalPackages(); err != nil { 114 | s.logger.Errorf("Cannot delete unneeded packages: %v", err) 115 | failed = true 116 | } 117 | 118 | // Iterate through offered packages and sync them all from server. 119 | for name, pkg := range s.available.Packages { 120 | err := s.syncPackage(ctx, name, pkg) 121 | if err != nil { 122 | s.logger.Errorf("Cannot sync package %s: %v", name, err) 123 | failed = true 124 | } 125 | } 126 | 127 | if !failed { 128 | // Update the "all" hash on success, so that next time Sync() does not thing, 129 | // unless a new hash is received from the Server. 130 | if err := s.localState.SetAllPackagesHash(s.available.AllPackagesHash); err != nil { 131 | s.logger.Errorf("SetAllPackagesHash failed: %v", err) 132 | } else { 133 | s.logger.Debugf("All packages are synced and up to date.") 134 | } 135 | } else { 136 | s.logger.Errorf("Package syncing was not successful.") 137 | } 138 | 139 | _ = s.reportStatuses(true) 140 | } 141 | 142 | // syncPackage downloads the package from the server and installs it. 143 | func (s *packagesSyncer) syncPackage( 144 | ctx context.Context, 145 | pkgName string, 146 | pkgAvail *protobufs.PackageAvailable, 147 | ) error { 148 | 149 | status := s.statuses.Packages[pkgName] 150 | if status == nil { 151 | // This package has no status. Create one. 152 | status = &protobufs.PackageStatus{ 153 | Name: pkgName, 154 | ServerOfferedVersion: pkgAvail.Version, 155 | ServerOfferedHash: pkgAvail.Hash, 156 | } 157 | s.statuses.Packages[pkgName] = status 158 | } 159 | 160 | pkgLocal, err := s.localState.PackageState(pkgName) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | mustCreate := !pkgLocal.Exists 166 | if pkgLocal.Exists { 167 | if bytes.Equal(pkgLocal.Hash, pkgAvail.Hash) { 168 | s.logger.Debugf("Package %s hash is unchanged, skipping", pkgName) 169 | return nil 170 | } 171 | if pkgLocal.Type != pkgAvail.Type { 172 | // Package is of wrong type. Need to re-create it. So, delete it. 173 | if err := s.localState.DeletePackage(pkgName); err != nil { 174 | err = fmt.Errorf("cannot delete existing version of package %s: %v", pkgName, err) 175 | status.Status = protobufs.PackageStatusEnum_PackageStatusEnum_InstallFailed 176 | status.ErrorMessage = err.Error() 177 | return err 178 | } 179 | // And mark that it needs to be created. 180 | mustCreate = true 181 | } 182 | } 183 | 184 | // Report that we are beginning to install it. 185 | status.Status = protobufs.PackageStatusEnum_PackageStatusEnum_Installing 186 | _ = s.reportStatuses(true) 187 | 188 | if mustCreate { 189 | // Make sure the package exists. 190 | err = s.localState.CreatePackage(pkgName, pkgAvail.Type) 191 | if err != nil { 192 | err = fmt.Errorf("cannot create package %s: %v", pkgName, err) 193 | status.Status = protobufs.PackageStatusEnum_PackageStatusEnum_InstallFailed 194 | status.ErrorMessage = err.Error() 195 | return err 196 | } 197 | } 198 | 199 | // Sync package file: ensure it exists or download it. 200 | err = s.syncPackageFile(ctx, pkgName, pkgAvail.File) 201 | if err == nil { 202 | // Only save the state on success, so that next sync does not retry this package. 203 | pkgLocal.Hash = pkgAvail.Hash 204 | pkgLocal.Version = pkgAvail.Version 205 | if err := s.localState.SetPackageState(pkgName, pkgLocal); err == nil { 206 | status.Status = protobufs.PackageStatusEnum_PackageStatusEnum_Installed 207 | status.AgentHasHash = pkgAvail.Hash 208 | status.AgentHasVersion = pkgAvail.Version 209 | } 210 | } 211 | 212 | if err != nil { 213 | status.Status = protobufs.PackageStatusEnum_PackageStatusEnum_InstallFailed 214 | status.ErrorMessage = err.Error() 215 | } 216 | _ = s.reportStatuses(true) 217 | 218 | return err 219 | } 220 | 221 | // syncPackageFile downloads the package file from the server. 222 | // If the file already exists and contents are 223 | // unchanged, it is not downloaded again. 224 | func (s *packagesSyncer) syncPackageFile( 225 | ctx context.Context, pkgName string, file *protobufs.DownloadableFile, 226 | ) error { 227 | shouldDownload, err := s.shouldDownloadFile(pkgName, file) 228 | if err == nil && shouldDownload { 229 | err = s.downloadFile(ctx, pkgName, file) 230 | } 231 | 232 | return err 233 | } 234 | 235 | // shouldDownloadFile returns true if the file should be downloaded. 236 | func (s *packagesSyncer) shouldDownloadFile( 237 | packageName string, 238 | file *protobufs.DownloadableFile, 239 | ) (bool, error) { 240 | fileContentHash, err := s.localState.FileContentHash(packageName) 241 | 242 | if err != nil { 243 | err := fmt.Errorf("cannot calculate checksum of %s: %v", packageName, err) 244 | s.logger.Errorf(err.Error()) 245 | return true, nil 246 | } else { 247 | // Compare the checksum of the file we have with what 248 | // we are offered by the server. 249 | if bytes.Compare(fileContentHash, file.ContentHash) != 0 { 250 | s.logger.Debugf("Package %s: file hash mismatch, will download.", packageName) 251 | return true, nil 252 | } 253 | } 254 | return false, nil 255 | } 256 | 257 | // downloadFile downloads the file from the server. 258 | func (s *packagesSyncer) downloadFile(ctx context.Context, pkgName string, file *protobufs.DownloadableFile) error { 259 | s.logger.Debugf("Downloading package %s file from %s", pkgName, file.DownloadUrl) 260 | 261 | req, err := http.NewRequestWithContext(ctx, "GET", file.DownloadUrl, nil) 262 | if err != nil { 263 | return fmt.Errorf("cannot download file from %s: %v", file.DownloadUrl, err) 264 | } 265 | 266 | resp, err := http.DefaultClient.Do(req) 267 | if err != nil { 268 | return fmt.Errorf("cannot download file from %s: %v", file.DownloadUrl, err) 269 | } 270 | defer func() { _ = resp.Body.Close() }() 271 | 272 | if resp.StatusCode != http.StatusOK { 273 | return fmt.Errorf("cannot download file from %s, HTTP response=%v", file.DownloadUrl, resp.StatusCode) 274 | } 275 | 276 | // TODO: either add a callback to verify file.Signature or pass the Signature 277 | // as a parameter to UpdateContent. 278 | 279 | err = s.localState.UpdateContent(ctx, pkgName, resp.Body, file.ContentHash) 280 | if err != nil { 281 | return fmt.Errorf("cannot download file from %s: %v", file.DownloadUrl, err) 282 | } 283 | return nil 284 | } 285 | 286 | // deleteUnneededLocalPackages deletes local packages that are not 287 | // needed anymore. This is done by comparing the local package state 288 | // with the server's package state. 289 | func (s *packagesSyncer) deleteUnneededLocalPackages() error { 290 | // Read the list of packages we have locally. 291 | localPackages, err := s.localState.Packages() 292 | if err != nil { 293 | return err 294 | } 295 | 296 | var lastErr error 297 | for _, localPkg := range localPackages { 298 | // Do we have a package that is not offered? 299 | if _, offered := s.available.Packages[localPkg]; !offered { 300 | s.logger.Debugf("Package %s is no longer needed, deleting.", localPkg) 301 | err := s.localState.DeletePackage(localPkg) 302 | if err != nil { 303 | lastErr = err 304 | } 305 | } 306 | } 307 | 308 | // Also remove packages that were not offered from the statuses. 309 | for name := range s.statuses.Packages { 310 | if _, offered := s.available.Packages[name]; !offered { 311 | delete(s.statuses.Packages, name) 312 | } 313 | } 314 | 315 | return lastErr 316 | } 317 | 318 | // reportStatuses saves the last reported statuses to provider and client state. 319 | // If sendImmediately is true, the statuses are scheduled to be 320 | // sent to the server. 321 | func (s *packagesSyncer) reportStatuses(sendImmediately bool) error { 322 | // Save it in the user-supplied state provider. 323 | if err := s.localState.SetLastReportedStatuses(s.statuses); err != nil { 324 | s.logger.Errorf("Cannot save last reported statuses: %v", err) 325 | return err 326 | } 327 | 328 | // Also save it in our internal state (will be needed if the Server asks for it). 329 | if err := s.clientSyncedState.SetPackageStatuses(s.statuses); err != nil { 330 | s.logger.Errorf("Cannot save client state: %v", err) 331 | return err 332 | } 333 | s.sender.NextMessage().Update( 334 | func(msg *protobufs.AgentToServer) { 335 | msg.PackageStatuses = s.clientSyncedState.PackageStatuses() 336 | }) 337 | 338 | if sendImmediately { 339 | s.sender.ScheduleSend() 340 | } 341 | return nil 342 | } 343 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /client/internal/clientcommon.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | 9 | "github.com/open-telemetry/opamp-go/client/types" 10 | "github.com/open-telemetry/opamp-go/protobufs" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | var ( 15 | ErrAgentDescriptionMissing = errors.New("AgentDescription is nil") 16 | ErrAgentDescriptionNoAttributes = errors.New("AgentDescription has no attributes defined") 17 | ErrAgentHealthMissing = errors.New("AgentHealth is nil") 18 | ErrReportsEffectiveConfigNotSet = errors.New("ReportsEffectiveConfig capability is not set") 19 | ErrReportsRemoteConfigNotSet = errors.New("ReportsRemoteConfig capability is not set") 20 | ErrPackagesStateProviderNotSet = errors.New("PackagesStateProvider must be set") 21 | ErrAcceptsPackagesNotSet = errors.New("AcceptsPackages and ReportsPackageStatuses must be set") 22 | 23 | errAlreadyStarted = errors.New("already started") 24 | errCannotStopNotStarted = errors.New("cannot stop because not started") 25 | errReportsPackageStatusesNotSet = errors.New("ReportsPackageStatuses capability is not set") 26 | ) 27 | 28 | // ClientCommon contains the OpAMP logic that is common between WebSocket and 29 | // plain HTTP transports. 30 | type ClientCommon struct { 31 | Logger types.Logger 32 | Callbacks types.Callbacks 33 | 34 | // Agent's capabilities defined at Start() time. 35 | Capabilities protobufs.AgentCapabilities 36 | 37 | // Client state storage. This is needed if the Server asks to report the state. 38 | ClientSyncedState ClientSyncedState 39 | 40 | // PackagesStateProvider provides access to the local state of packages. 41 | PackagesStateProvider types.PackagesStateProvider 42 | 43 | // The transport-specific sender. 44 | sender Sender 45 | 46 | // True if Start() is successful. 47 | isStarted bool 48 | 49 | // Cancellation func for background go routines. 50 | runCancel context.CancelFunc 51 | 52 | // True when stopping is in progress. 53 | isStoppingFlag bool 54 | isStoppingMutex sync.RWMutex 55 | 56 | // Indicates that the Client is fully stopped. 57 | stoppedSignal chan struct{} 58 | } 59 | 60 | // NewClientCommon creates a new ClientCommon. 61 | func NewClientCommon(logger types.Logger, sender Sender) ClientCommon { 62 | return ClientCommon{ 63 | Logger: logger, sender: sender, stoppedSignal: make(chan struct{}, 1), 64 | } 65 | } 66 | 67 | // PrepareStart prepares the client state for the next Start() call. 68 | // It returns an error if the client is already started, or if the settings are invalid. 69 | func (c *ClientCommon) PrepareStart( 70 | _ context.Context, settings types.StartSettings, 71 | ) error { 72 | if c.isStarted { 73 | return errAlreadyStarted 74 | } 75 | 76 | c.Capabilities = settings.Capabilities 77 | 78 | // According to OpAMP spec this capability MUST be set, since all Agents MUST report status. 79 | c.Capabilities |= protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus 80 | 81 | if c.ClientSyncedState.AgentDescription() == nil { 82 | return ErrAgentDescriptionMissing 83 | } 84 | 85 | if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth != 0 && c.ClientSyncedState.Health() == nil { 86 | return ErrAgentHealthMissing 87 | } 88 | 89 | // Prepare remote config status. 90 | if settings.RemoteConfigStatus == nil { 91 | // RemoteConfigStatus is not provided. Start with empty. 92 | settings.RemoteConfigStatus = &protobufs.RemoteConfigStatus{ 93 | Status: protobufs.RemoteConfigStatuses_RemoteConfigStatuses_UNSET, 94 | } 95 | } 96 | 97 | if err := c.ClientSyncedState.SetRemoteConfigStatus(settings.RemoteConfigStatus); err != nil { 98 | return err 99 | } 100 | 101 | // Prepare package statuses. 102 | c.PackagesStateProvider = settings.PackagesStateProvider 103 | var packageStatuses *protobufs.PackageStatuses 104 | if settings.PackagesStateProvider != nil { 105 | if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages == 0 || 106 | c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses == 0 { 107 | return ErrAcceptsPackagesNotSet 108 | } 109 | 110 | // Set package status from the value previously saved in the PackagesStateProvider. 111 | var err error 112 | packageStatuses, err = settings.PackagesStateProvider.LastReportedStatuses() 113 | if err != nil { 114 | return err 115 | } 116 | } else { 117 | if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages != 0 || 118 | c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses != 0 { 119 | return ErrPackagesStateProviderNotSet 120 | } 121 | } 122 | 123 | if packageStatuses == nil { 124 | // PackageStatuses is not provided. Start with empty. 125 | packageStatuses = &protobufs.PackageStatuses{} 126 | } 127 | if err := c.ClientSyncedState.SetPackageStatuses(packageStatuses); err != nil { 128 | return err 129 | } 130 | 131 | // Prepare callbacks. 132 | c.Callbacks = settings.Callbacks 133 | if c.Callbacks == nil { 134 | // Make sure it is always safe to call Callbacks. 135 | c.Callbacks = types.CallbacksStruct{} 136 | } 137 | 138 | if err := c.sender.SetInstanceUid(settings.InstanceUid); err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | } 144 | 145 | // Stop stops the client. It returns an error if the client is not started. 146 | func (c *ClientCommon) Stop(ctx context.Context) error { 147 | if !c.isStarted { 148 | return errCannotStopNotStarted 149 | } 150 | 151 | c.isStoppingMutex.Lock() 152 | cancelFunc := c.runCancel 153 | c.isStoppingFlag = true 154 | c.isStoppingMutex.Unlock() 155 | 156 | cancelFunc() 157 | 158 | // Wait until stopping is finished. 159 | select { 160 | case <-ctx.Done(): 161 | return ctx.Err() 162 | case <-c.stoppedSignal: 163 | } 164 | return nil 165 | } 166 | 167 | // IsStopping returns true if Stop() was called. 168 | func (c *ClientCommon) IsStopping() bool { 169 | c.isStoppingMutex.RLock() 170 | defer c.isStoppingMutex.RUnlock() 171 | return c.isStoppingFlag 172 | } 173 | 174 | // StartConnectAndRun initiates the connection with the Server and starts the 175 | // background goroutine that handles the communication unitl client is stopped. 176 | func (c *ClientCommon) StartConnectAndRun(runner func(ctx context.Context)) { 177 | // Create a cancellable context. 178 | runCtx, runCancel := context.WithCancel(context.Background()) 179 | 180 | c.isStoppingMutex.Lock() 181 | defer c.isStoppingMutex.Unlock() 182 | 183 | if c.isStoppingFlag { 184 | // Stop() was called. Don't connect. 185 | runCancel() 186 | return 187 | } 188 | c.runCancel = runCancel 189 | 190 | go func() { 191 | defer func() { 192 | // We only return from runner() when we are instructed to stop. 193 | // When returning signal that we stopped. 194 | c.stoppedSignal <- struct{}{} 195 | }() 196 | 197 | runner(runCtx) 198 | }() 199 | 200 | c.isStarted = true 201 | } 202 | 203 | // PrepareFirstMessage prepares the initial state of NextMessage struct that client 204 | // sends when it first establishes a connection with the Server. 205 | func (c *ClientCommon) PrepareFirstMessage(ctx context.Context) error { 206 | cfg, err := c.Callbacks.GetEffectiveConfig(ctx) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | c.sender.NextMessage().Update( 212 | func(msg *protobufs.AgentToServer) { 213 | msg.AgentDescription = c.ClientSyncedState.AgentDescription() 214 | msg.EffectiveConfig = cfg 215 | msg.RemoteConfigStatus = c.ClientSyncedState.RemoteConfigStatus() 216 | msg.PackageStatuses = c.ClientSyncedState.PackageStatuses() 217 | msg.Capabilities = c.Capabilities 218 | }, 219 | ) 220 | return nil 221 | } 222 | 223 | // AgentDescription returns the current state of the AgentDescription. 224 | func (c *ClientCommon) AgentDescription() *protobufs.AgentDescription { 225 | // Return a cloned copy to allow caller to do whatever they want with the result. 226 | return proto.Clone(c.ClientSyncedState.AgentDescription()).(*protobufs.AgentDescription) 227 | } 228 | 229 | // SetAgentDescription sends a status update to the Server with the new AgentDescription 230 | // and remembers the AgentDescription in the client state so that it can be sent 231 | // to the Server when the Server asks for it. 232 | func (c *ClientCommon) SetAgentDescription(descr *protobufs.AgentDescription) error { 233 | // store the Agent description to send on reconnect 234 | if err := c.ClientSyncedState.SetAgentDescription(descr); err != nil { 235 | return err 236 | } 237 | c.sender.NextMessage().Update( 238 | func(msg *protobufs.AgentToServer) { 239 | msg.AgentDescription = c.ClientSyncedState.AgentDescription() 240 | }, 241 | ) 242 | c.sender.ScheduleSend() 243 | return nil 244 | } 245 | 246 | // SetHealth sends a status update to the Server with the new AgentHealth 247 | // and remembers the AgentHealth in the client state so that it can be sent 248 | // to the Server when the Server asks for it. 249 | func (c *ClientCommon) SetHealth(health *protobufs.AgentHealth) error { 250 | // store the AgentHealth to send on reconnect 251 | if err := c.ClientSyncedState.SetHealth(health); err != nil { 252 | return err 253 | } 254 | c.sender.NextMessage().Update( 255 | func(msg *protobufs.AgentToServer) { 256 | msg.Health = c.ClientSyncedState.Health() 257 | }, 258 | ) 259 | c.sender.ScheduleSend() 260 | return nil 261 | } 262 | 263 | // UpdateEffectiveConfig fetches the current local effective config using 264 | // GetEffectiveConfig callback and sends it to the Server using provided Sender. 265 | func (c *ClientCommon) UpdateEffectiveConfig(ctx context.Context) error { 266 | if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig == 0 { 267 | return ErrReportsEffectiveConfigNotSet 268 | } 269 | 270 | // Fetch the locally stored config. 271 | cfg, err := c.Callbacks.GetEffectiveConfig(ctx) 272 | if err != nil { 273 | return fmt.Errorf("GetEffectiveConfig failed: %w", err) 274 | } 275 | 276 | // Send it to the Server. 277 | c.sender.NextMessage().Update( 278 | func(msg *protobufs.AgentToServer) { 279 | msg.EffectiveConfig = cfg 280 | }, 281 | ) 282 | // TODO: if this call is coming from OnMessage callback don't schedule the send 283 | // immediately, wait until the end of OnMessage to send one message only. 284 | c.sender.ScheduleSend() 285 | 286 | // Note that we do not store the EffectiveConfig anywhere else. It will be deleted 287 | // from NextMessage when the message is sent. This avoids storing EffectiveConfig 288 | // in memory for longer than it is needed. 289 | return nil 290 | } 291 | 292 | // SetRemoteConfigStatus sends a status update to the Server if the new RemoteConfigStatus 293 | // is different from the status we already have in the state. 294 | // It also remembers the new RemoteConfigStatus in the client state so that it can be 295 | // sent to the Server when the Server asks for it. 296 | func (c *ClientCommon) SetRemoteConfigStatus(status *protobufs.RemoteConfigStatus) error { 297 | if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig == 0 { 298 | return ErrReportsRemoteConfigNotSet 299 | } 300 | 301 | if status.LastRemoteConfigHash == nil { 302 | return errLastRemoteConfigHashNil 303 | } 304 | 305 | statusChanged := !proto.Equal(c.ClientSyncedState.RemoteConfigStatus(), status) 306 | 307 | // Remember the new status. 308 | if err := c.ClientSyncedState.SetRemoteConfigStatus(status); err != nil { 309 | return err 310 | } 311 | 312 | if statusChanged { 313 | // Let the Server know about the new status. 314 | c.sender.NextMessage().Update( 315 | func(msg *protobufs.AgentToServer) { 316 | msg.RemoteConfigStatus = c.ClientSyncedState.RemoteConfigStatus() 317 | }, 318 | ) 319 | // TODO: if this call is coming from OnMessage callback don't schedule the send 320 | // immediately, wait until the end of OnMessage to send one message only. 321 | c.sender.ScheduleSend() 322 | } 323 | 324 | return nil 325 | } 326 | 327 | // SetPackageStatuses sends a status update to the Server if the new PackageStatuses 328 | // are different from the ones we already have in the state. 329 | // It also remembers the new PackageStatuses in the client state so that it can be 330 | // sent to the Server when the Server asks for it. 331 | func (c *ClientCommon) SetPackageStatuses(statuses *protobufs.PackageStatuses) error { 332 | if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses == 0 { 333 | return errReportsPackageStatusesNotSet 334 | } 335 | 336 | if statuses.ServerProvidedAllPackagesHash == nil { 337 | return errServerProvidedAllPackagesHashNil 338 | } 339 | 340 | statusChanged := !proto.Equal(c.ClientSyncedState.PackageStatuses(), statuses) 341 | 342 | if err := c.ClientSyncedState.SetPackageStatuses(statuses); err != nil { 343 | return err 344 | } 345 | 346 | // Check if the new status is different from the previous. 347 | if statusChanged { 348 | // Let the Server know about the new status. 349 | 350 | c.sender.NextMessage().Update( 351 | func(msg *protobufs.AgentToServer) { 352 | msg.PackageStatuses = c.ClientSyncedState.PackageStatuses() 353 | }, 354 | ) 355 | // TODO: if this call is coming from OnMessage callback don't schedule the send 356 | // immediately, wait until the end of OnMessage to send one message only. 357 | c.sender.ScheduleSend() 358 | } 359 | 360 | return nil 361 | } 362 | --------------------------------------------------------------------------------