Skip to content

Commit

Permalink
feat(sumologicextension): add implementation of Sumo Logic Extension (#…
Browse files Browse the repository at this point in the history
…31031)

**Description:**

Add functionality based on Sumo Logic Repo code

**Link to tracking Issue:**

#29601

**Testing:**

* Unit tests
* manual tests
* in use by customers for long time already

**Documentation:**

* code comments
* README.md

---------

Signed-off-by: Dominik Rosiek <[email protected]>
Co-authored-by: Andrzej Stencel <[email protected]>
Co-authored-by: Mikołaj Świątek <[email protected]>
  • Loading branch information
3 people committed Feb 28, 2024
1 parent cea1de2 commit c7fbf38
Show file tree
Hide file tree
Showing 19 changed files with 3,324 additions and 20 deletions.
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

0 comments on commit c7fbf38

Please sign in to comment.