├── 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 | | Instance ID |
14 |
15 | {{ range . }}
16 |
17 | | {{ .InstanceId }} |
18 |
19 | {{ end }}
20 |
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 |
14 |
15 |
16 |
17 |
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 |
21 | Agent
22 |
23 |
24 | | Instance ID: | {{ .InstanceId }} |
25 |
26 | {{if .Status.Health }}
27 |
28 | | Up: | {{ .Status.Health.Up }} |
29 |
30 | {{if .Status.Health.LastError }}
31 |
32 | | {{ .Status.Health.LastError }} |
33 |
34 | {{end}}
35 | {{if .Status.Health.Up }}
36 |
37 | | Up since: | {{ .StartedAt }} |
38 |
39 | {{end}}
40 | {{end}}
41 |
42 | |
43 |
44 | Attributes
45 |
46 | {{ range .Status.AgentDescription.IdentifyingAttributes }}
47 |
48 | | {{ .Key }} | {{ .Value }} |
49 |
50 | {{ end }}
51 | {{ range .Status.AgentDescription.NonIdentifyingAttributes }}
52 |
53 | {{ .Key }} | {{ .Value }} |
54 |
55 | {{ end }}
56 |
57 | |
58 |
59 |
60 |
61 |
62 |
63 | Configuration
64 |
65 |
66 |
67 | Current Effective Configuration:
68 | {{ .EffectiveConfig }}
69 | |
70 |
71 | Additional Configuration:
72 |
82 | |
83 |
84 |
85 |
86 |