Skip to content

Commit

Permalink
Add Chronicle Forwarder
Browse files Browse the repository at this point in the history
  • Loading branch information
Miguel Rodriguez committed Dec 14, 2023
1 parent c931b99 commit e215cc1
Show file tree
Hide file tree
Showing 16 changed files with 1,216 additions and 0 deletions.
55 changes: 55 additions & 0 deletions exporter/chronicleforwarderexporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Chronicle Forwarder Exporter

The Chronicle Forwarder Exporter is designed for forwarding logs to a Chronicle endpoint using either Syslog or File-based methods. This exporter supports customization of data export types and various configuration options to tailor the connection and data handling to specific needs.

## Minimum Agent Versions

- Introduced: [v1.42.0](https://github.com/observIQ/bindplane-agent/releases/tag/v1.42.0)

## Supported Pipelines

- Logs

## How It Works

1. For Syslog, it establishes a network connection to the specified Chronicle forwarder endpoint.
2. For File, it writes logs to a specified file path.

## Configuration

| Field | Type | Default Value | Required | Description |
| -------------------- | ------------ | ------------- | -------- | ------------------------------------------------- |
| export_type | string | `syslog` | `true` | Type of export, either `syslog` or `file`. |
| syslog | SyslogConfig | | `true` | Configuration for Syslog connection. |
| file | File | | `true` | Configuration for File connection. |
| raw_log_field | string | | `false` | The field name to send raw logs to Chronicle. |
| syslog.host | string | `127.0.0.1` | `false` | The Chronicle forwarder endpoint host. |
| syslog.port | int | `10514` | `false` | The port to send logs to. |
| syslog.network | string | `tcp` | `false` | The network protocol to use (e.g., `tcp`, `udp`). |
| syslog.tls.key_file | string | | `false` | Configure the receiver to use TLS. |
| syslog.tls.cert_file | string | | `false` | Configure the receiver to use TLS. |
| file.path | string | | `false` | The path to the file for storing logs. |

## Example Configurations

### Syslog Configuration Example

```yaml
chronicleforwarderexporter:
export_type: "syslog"
syslog:
host: "syslog.example.com"
port: 10514
network: "tcp"
```
### File Configuration Example
```yaml
chronicleforwarderexporter:
export_type: "file"
file:
path: "/path/to/logfile"
```
---
100 changes: 100 additions & 0 deletions exporter/chronicleforwarderexporter/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package chronicleforwarderexporter

import (
"fmt"

"github.com/observiq/bindplane-agent/expr"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/exporter/exporterhelper"
"go.uber.org/zap"
)

const (
// ExportTypeSyslog is the syslog export type.
ExportTypeSyslog = "syslog"

// ExportTypeFile is the file export type.
ExportTypeFile = "file"
)

// Config defines configuration for the Chronicle exporter.
type Config struct {
exporterhelper.TimeoutSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct.
exporterhelper.QueueSettings `mapstructure:"sending_queue"`
exporterhelper.RetrySettings `mapstructure:"retry_on_failure"`

// ExportType is the type of export to use.
ExportType string `mapstructure:"export_type"`

// Syslog is the configuration for the connection to the Chronicle forwarder.
Syslog SyslogConfig `mapstructure:"syslog"`

// File is the configuration for the connection to the Chronicle forwarder.
File File `mapstructure:"file"`

// RawLogField is the field name that will be used to send raw logs to Chronicle.
RawLogField string `mapstructure:"raw_log_field"`
}

// SyslogConfig defines configuration for the Chronicle forwarder connection.
type SyslogConfig struct {
// Host is the Chronicle forwarder endpoint to send logs to.
Host string `mapstructure:"host"`

// port is the port to send logs to.
Port int `mapstructure:"port"`

// Network is the network protocol to use.
Network string `mapstructure:"network"`

confighttp.HTTPServerSettings `mapstructure:",squash"`
}

// File defines configuration for sending to.
type File struct {
// Path is the path to the file to send to Chronicle.
Path string `mapstructure:"path"`
}

func (cfg *Config) Validate() error {

Check warning on line 74 in exporter/chronicleforwarderexporter/config.go

View workflow job for this annotation

GitHub Actions / lint

exported method Config.Validate should have comment or be unexported
if cfg.ExportType != ExportTypeSyslog && cfg.ExportType != ExportTypeFile {
return fmt.Errorf("export_type must be either 'syslog' or 'file'")
}

if cfg.ExportType == ExportTypeSyslog {
if cfg.Syslog.Host == "" || cfg.Syslog.Port <= 0 || cfg.Syslog.Network == "" {
return fmt.Errorf("incomplete syslog configuration: host, port, and network are required")
}
}

if cfg.ExportType == ExportTypeFile {
if cfg.File.Path == "" {
return fmt.Errorf("file path is required for file export type")
}
}

if cfg.RawLogField != "" {
_, err := expr.NewOTTLLogRecordExpression(cfg.RawLogField, component.TelemetrySettings{
Logger: zap.NewNop(),
})
if err != nil {
return fmt.Errorf("raw_log_field is invalid: %s", err)
}
}
return nil
}
82 changes: 82 additions & 0 deletions exporter/chronicleforwarderexporter/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package chronicleforwarderexporter

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
cfg Config
wantErr bool
}{
{
name: "Valid syslog config",
cfg: Config{
ExportType: ExportTypeSyslog,
Syslog: SyslogConfig{
Host: "localhost",
Port: 514,
Network: "tcp",
},
},
wantErr: false,
},
{
name: "Invalid syslog config - missing host",
cfg: Config{
ExportType: ExportTypeSyslog,
Syslog: SyslogConfig{
Port: 514,
Network: "tcp",
},
},
wantErr: true,
},
{
name: "Valid file config",
cfg: Config{
ExportType: ExportTypeFile,
File: File{
Path: "/path/to/file",
},
},
wantErr: false,
},
{
name: "Invalid file config - missing path",
cfg: Config{
ExportType: ExportTypeFile,
File: File{},
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
18 changes: 18 additions & 0 deletions exporter/chronicleforwarderexporter/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:generate mdatagen metadata.yaml

// Package chronicleforwarderexporter exports OpenTelemetry data to Chronicle.
package chronicleforwarderexporter // import "github.com/observiq/bindplane-agent/exporter/azureblobexporter"
117 changes: 117 additions & 0 deletions exporter/chronicleforwarderexporter/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright observIQ, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package chronicleforwarderexporter

import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"os"
"strings"

"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/pdata/plog"
"go.uber.org/zap"
)

type chronicleForwarderExporter struct {
cfg *Config
logger *zap.Logger
writer io.Writer
marshaler logMarshaler
endpoint string
}

func newExporter(cfg *Config, params exporter.CreateSettings) (*chronicleForwarderExporter, error) {
exporter := &chronicleForwarderExporter{
cfg: cfg,
logger: params.Logger,
marshaler: newMarshaler(*cfg, params.TelemetrySettings),
}

switch cfg.ExportType {
case ExportTypeSyslog:
endpoint := buildEndpoint(cfg)

var conn net.Conn
var err error
if cfg.Syslog.TLSSetting != nil {
tlsConfig, err := cfg.Syslog.TLSSetting.LoadTLSConfig()
if err != nil {
return nil, fmt.Errorf("load TLS config: %w", err)
}
conn, err = tls.Dial(cfg.Syslog.Network, endpoint, tlsConfig)
} else {
conn, err = net.Dial(cfg.Syslog.Network, endpoint)
}

if err != nil {
return nil, fmt.Errorf("dial: %w", err)
}
exporter.writer = conn

case ExportTypeFile:
var err error
exporter.writer, err = os.OpenFile(cfg.File.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return nil, fmt.Errorf("open file: %w", err)
}
}

return exporter, nil
}

func buildEndpoint(cfg *Config) string {
return fmt.Sprintf("%s:%d", cfg.Syslog.Host, cfg.Syslog.Port)
}

func (ce *chronicleForwarderExporter) Capabilities() consumer.Capabilities {
return consumer.Capabilities{MutatesData: false}
}

func (ce *chronicleForwarderExporter) logsDataPusher(ctx context.Context, ld plog.Logs) error {
payloads, err := ce.marshaler.MarshalRawLogs(ctx, ld)
if err != nil {
return fmt.Errorf("marshal logs: %w", err)
}

for _, payload := range payloads {
if err := ce.send(payload); err != nil {
return fmt.Errorf("upload to Chronicle forwarder: %w", err)
}
}

return nil
}

func (s *chronicleForwarderExporter) send(msg string) error {

Check warning on line 102 in exporter/chronicleforwarderexporter/exporter.go

View workflow job for this annotation

GitHub Actions / lint

receiver name s should be consistent with previous receiver name ce for chronicleForwarderExporter
if !strings.HasSuffix(msg, "\n") {
msg = fmt.Sprintf("%s%s", msg, "\n")
}
_, err := fmt.Fprint(s.writer, msg)
return err
}

func (s *chronicleForwarderExporter) Shutdown(ctx context.Context) error {

Check warning on line 110 in exporter/chronicleforwarderexporter/exporter.go

View workflow job for this annotation

GitHub Actions / lint

receiver name s should be consistent with previous receiver name ce for chronicleForwarderExporter

Check warning on line 110 in exporter/chronicleforwarderexporter/exporter.go

View workflow job for this annotation

GitHub Actions / lint

parameter 'ctx' seems to be unused, consider removing or renaming it as _
if s.writer != nil {
if err := s.writer.(io.Closer).Close(); err != nil {
return fmt.Errorf("close writer: %w", err)
}
}
return nil
}
Loading

0 comments on commit e215cc1

Please sign in to comment.