Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sumologicextension): add implementation of Sumo Logic Extension #31031

Merged
merged 10 commits into from
Feb 28, 2024
27 changes: 27 additions & 0 deletions .chloggen/drosiek-sumologicextension-2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: new_component

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: sumologicextension

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: add implementation of Sumo Logic Extension

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [29601]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
1 change: 1 addition & 0 deletions extension/sumologicextension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ and can be used as an authenticator for the
- `initial_interval` - initial interval of backoff (default: `500ms`)
- `max_interval` - maximum interval of backoff (default: `1m`)
- `max_elapsed_time` - time after which registration fails definitely (default: `15m`)
- `sticky_session_enabled` - enable sticky session support (default: `false`)

[credentials_help]: https://help.sumologic.com/docs/manage/security/installation-tokens
[fields_help]: https://help.sumologic.com/docs/manage/fields
Expand Down
14 changes: 14 additions & 0 deletions extension/sumologicextension/api/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package api // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/sumologicextension/api"

type ErrorResponsePayload struct {
ID string `json:"id"`
Errors []Error `json:"errors"`
}

type Error struct {
Code string `json:"code"`
Message string `json:"message"`
}
26 changes: 26 additions & 0 deletions extension/sumologicextension/api/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package api // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/sumologicextension/api"

type OpenMetadataHostDetails struct {
Name string `json:"name"`
OsName string `json:"osName"`
OsVersion string `json:"osVersion"`
Environment string `json:"environment"`
}

type OpenMetadataCollectorDetails struct {
RunningVersion string `json:"runningVersion"`
}

type OpenMetadataNetworkDetails struct {
HostIPAddress string `json:"hostIpAddress"`
}

type OpenMetadataRequestPayload struct {
HostDetails OpenMetadataHostDetails `json:"hostDetails"`
CollectorDetails OpenMetadataCollectorDetails `json:"collectorDetails"`
NetworkDetails OpenMetadataNetworkDetails `json:"networkDetails"`
TagDetails map[string]any `json:"tagDetails"`
}
22 changes: 22 additions & 0 deletions extension/sumologicextension/api/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package api // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/sumologicextension/api"

type OpenRegisterRequestPayload struct {
CollectorName string `json:"collectorName"`
Ephemeral bool `json:"ephemeral,omitempty"`
Description string `json:"description,omitempty"`
Hostname string `json:"hostname,omitempty"`
Category string `json:"category,omitempty"`
TimeZone string `json:"timeZone,omitempty"`
Clobber bool `json:"clobber,omitempty"`
Fields map[string]any `json:"fields,omitempty"`
}

type OpenRegisterResponsePayload struct {
CollectorCredentialID string `json:"collectorCredentialID"`
CollectorCredentialKey string `json:"collectorCredentialKey"`
CollectorID string `json:"collectorId"`
CollectorName string `json:"collectorName"`
}
4 changes: 4 additions & 0 deletions extension/sumologicextension/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ type Config struct {
// Exponential algorithm is being used.
// Please see following link for details: https://github.com/cenkalti/backoff
BackOff backOffConfig `mapstructure:"backoff"`

// StickySessionEnabled defines if sticky session support is enable.
// By default this is false.
StickySessionEnabled bool `mapstructure:"sticky_session_enabled"`
}

type accessCredentials struct {
Expand Down
257 changes: 257 additions & 0 deletions extension/sumologicextension/credentials/credentialsstore_localfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package credentials // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/sumologicextension/credentials"

import (
"encoding/json"
"fmt"
"io"
"os"
"path"

"go.uber.org/zap"
)

const (
DefaultCollectorDataDirectory = ".sumologic-otel-collector/"
)

func GetDefaultCollectorCredentialsDirectory() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}

return path.Join(home, DefaultCollectorDataDirectory), nil
}

// LocalFsStore implements Store interface and can be used to store and retrieve
// collector credentials from local file system.
//
// Files are stored locally in collectorCredentialsDirectory.
type LocalFsStore struct {
collectorCredentialsDirectory string
logger *zap.Logger
}

type LocalFsStoreOpt func(*LocalFsStore)

func WithLogger(l *zap.Logger) LocalFsStoreOpt {
return func(s *LocalFsStore) {
s.logger = l
}
}

func WithCredentialsDirectory(dir string) LocalFsStoreOpt {
return func(s *LocalFsStore) {
s.collectorCredentialsDirectory = dir
}
}

func NewLocalFsStore(opts ...LocalFsStoreOpt) (Store, error) {
defaultDir, err := GetDefaultCollectorCredentialsDirectory()
if err != nil {
return nil, err
}

logger, err := zap.NewDevelopment()
if err != nil {
return nil, err
}

store := LocalFsStore{
collectorCredentialsDirectory: defaultDir,
logger: logger,
}
for _, opt := range opts {
opt(&store)
}

return store, err
}

// Check checks if collector credentials can be found under a name being a hash
// of provided key inside collectorCredentialsDirectory.
func (cr LocalFsStore) Check(key string) bool {
f := func(_ Hasher, key string) bool {
filenameHash, err := HashKeyToFilename(key)
if err != nil {
return false
}
path := path.Join(cr.collectorCredentialsDirectory, filenameHash)
if _, err := os.Stat(path); err != nil {
return false
}
return true
}

return f(_getHasher(), key)
}

// Get retrieves collector credentials stored in local file system and then
// decrypts it using a hash of provided key.
func (cr LocalFsStore) Get(key string) (CollectorCredentials, error) {
f := func(_ Hasher, key string) (CollectorCredentials, error) {
filenameHash, err := HashKeyToFilename(key)
if err != nil {
return CollectorCredentials{}, err
}

path := path.Join(cr.collectorCredentialsDirectory, filenameHash)
creds, err := os.Open(path)
if err != nil {
return CollectorCredentials{}, err
}
defer creds.Close()

encryptedCreds, err := io.ReadAll(creds)
if err != nil {
return CollectorCredentials{}, err
}

encKey, err := HashKeyToEncryptionKey(key)
if err != nil {
return CollectorCredentials{}, err
}

collectorCreds, err := decrypt(encryptedCreds, encKey)
if err != nil {
return CollectorCredentials{}, err
}

var credentialsInfo CollectorCredentials
if err = json.Unmarshal(collectorCreds, &credentialsInfo); err != nil {
return CollectorCredentials{}, err
}

cr.logger.Info("Collector registration credentials retrieved from local fs",
zap.String("path", path),
)

return credentialsInfo, nil
}

creds, err := f(_getHasher(), key)

if err != nil {
return CollectorCredentials{}, err
}

return creds, nil
}

// Store stores collector credentials in a file in directory as specified
// in CollectorCredentialsDirectory.
// The credentials are encrypted using the provided key.
func (cr LocalFsStore) Store(key string, creds CollectorCredentials) error {
if err := ensureDir(cr.collectorCredentialsDirectory); err != nil {
return err
}

f := func(_ Hasher, key string, creds CollectorCredentials) error {
filenameHash, err := HashKeyToFilename(key)
if err != nil {
return err
}
path := path.Join(cr.collectorCredentialsDirectory, filenameHash)
collectorCreds, err := json.Marshal(creds)
if err != nil {
return fmt.Errorf("failed marshaling collector credentials: %w", err)
}

encKey, err := HashKeyToEncryptionKey(key)
if err != nil {
return err
}

encryptedCreds, err := encrypt(collectorCreds, encKey)
if err != nil {
return err
}

if err = os.WriteFile(path, encryptedCreds, 0600); err != nil {
return fmt.Errorf("failed to save credentials file '%s': %w",
path, err,
)
}

cr.logger.Info("Collector registration credentials stored locally",
zap.String("path", path),
)

return nil
}

err := f(_getHasher(), key, creds)
if err != nil {
return err
}

return nil
}

func (cr LocalFsStore) Delete(key string) error {
f := func(hasher Hasher, key string) error {
filenameHash, err := HashKeyToFilenameWith(hasher, key)
if err != nil {
return err
}

path := path.Join(cr.collectorCredentialsDirectory, filenameHash)

if _, err := os.Stat(path); err != nil {
return nil
}
if err := os.Remove(path); err != nil {
return fmt.Errorf("failed to remove credentials file '%s': %w",
path, err,
)
}

cr.logger.Debug("Collector registration credentials removed",
zap.String("path", path),
)

return nil
}

err := f(_getHasher(), key)
if err != nil {
return err
}

return nil
}

// Validate checks if the store is operating correctly
// This mostly means file permissions and the like
func (cr LocalFsStore) Validate() error {
if err := ensureDir(cr.collectorCredentialsDirectory); err != nil {
return err
}

return nil
}

// ensureDir checks if the specified directory exists and has the right permissions
// if it doesn't then it tries to create it.
func ensureDir(path string) error {
fi, err := os.Stat(path)
if err != nil {
if err := os.Mkdir(path, 0700); err != nil {
return err
}
return nil
}

// If the directory doesn't have the execution bit then
// set it so that we can 'exec' into it.
if fi.Mode().Perm() != 0700 {
if err := os.Chmod(path, 0700); err != nil {
return err
}
}

return nil
}
Loading
Loading