diff --git a/CHANGELOG.md b/CHANGELOG.md index 5800cf7598..c233d8f6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ Main (unreleased) - Add `otelcol.receiver.solace` component to receive traces from a Solace broker. (@wildum) +- Add `otelcol.exporter.syslog` component to export logs in syslog format (@dehaansa) + ### Enhancements - Add second metrics sample to the support bundle to provide delta information (@dehaansa) diff --git a/docs/sources/reference/compatibility/_index.md b/docs/sources/reference/compatibility/_index.md index 84830dabf3..1272b7e199 100644 --- a/docs/sources/reference/compatibility/_index.md +++ b/docs/sources/reference/compatibility/_index.md @@ -299,6 +299,7 @@ The following components, grouped by namespace, _export_ OpenTelemetry `otelcol. - [otelcol.exporter.otlphttp](../components/otelcol/otelcol.exporter.otlphttp) - [otelcol.exporter.prometheus](../components/otelcol/otelcol.exporter.prometheus) - [otelcol.exporter.splunkhec](../components/otelcol/otelcol.exporter.splunkhec) +- [otelcol.exporter.syslog](../components/otelcol/otelcol.exporter.syslog) - [otelcol.processor.attributes](../components/otelcol/otelcol.processor.attributes) - [otelcol.processor.batch](../components/otelcol/otelcol.processor.batch) - [otelcol.processor.deltatocumulative](../components/otelcol/otelcol.processor.deltatocumulative) diff --git a/docs/sources/reference/components/otelcol/otelcol.exporter.syslog.md b/docs/sources/reference/components/otelcol/otelcol.exporter.syslog.md new file mode 100644 index 0000000000..848db5b18d --- /dev/null +++ b/docs/sources/reference/components/otelcol/otelcol.exporter.syslog.md @@ -0,0 +1,244 @@ +--- +canonical: https://grafana.com/docs/alloy/latest/reference/components/otelcol/otelcol.exporter.syslog/ +description: Learn about otelcol.exporter.syslog +title: otelcol.exporter.syslog +--- + +# otelcol.exporter.syslog + +{{< docs/shared lookup="stability/public_preview.md" source="alloy" version="" >}} + +`otelcol.exporter.syslog` accepts logs from other `otelcol` components and writes them over the network using the syslog protocol. +It supports syslog protocols [RFC5424][] and [RFC3164][] and can send data over `TCP` or `UDP`. + +{{< admonition type="note" >}} +`otelcol.exporter.syslog` is a wrapper over the upstream OpenTelemetry Collector `syslog` exporter. +Bug reports or feature requests will be redirected to the upstream repository, if necessary. +{{< /admonition >}} + +You can specify multiple `otelcol.exporter.syslog` components by giving them different labels. + +[RFC5424]: https://www.rfc-editor.org/rfc/rfc5424 +[RFC3164]: https://www.rfc-editor.org/rfc/rfc3164 + +## Usage + +```alloy +otelcol.exporter.syslog "LABEL" { + endpoint = "HOST" +} +``` + +### Supported Attributes + +The exporter creates one syslog message for each log record based on the following attributes of the log record. +If an attribute is missing, the default value is used. The log's timestamp field is used for the syslog message's time. +RFC3164 only supports a subset of the attributes supported by RFC5424, and the default values are not the same between +the two protocols. Refer to the [OpenTelemetry documentation][upstream_readme] for the exporter for more details. + +| Attribute name | Type | RFC5424 Default value | RFC3164 supported | RFC3164 Default value +| ----------------- | ------ | ---------------------- |------------------ | ---------------------- +| `appname` | string | `-` | yes | empty string +| `hostname` | string | `-` | yes | `-` +| `message` | string | empty string | yes | empty string +| `msg_id` | string | `-` | no | +| `priority` | int | `165` | yes | `165` +| `proc_id` | string | `-` | no | +| `structured_data` | map | `-` | no | +| `version` | int | `1` | no | + +[upstream_readme]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree//exporter/syslogexporter + +## Arguments + +`otelcol.exporter.syslog` supports the following arguments: + +| Name | Type | Description | Default | Required | +|------------------------|-----------|---------------------------------------------------------------------------|-----------------------------------|----------| +| `endpoint` | `string` | The endpoint to send syslog formatted logs to. | | yes | +| `network` | `string` | The type of network connection to use to send logs. | tcp | no | +| `port` | `int` | The port where the syslog server accepts connections. | 514 | no | +| `protocol` | `string` | The syslog protocol that the syslog server supports. | rfc5424 | no | +| `enable_octet_counting`| `bool` | Whether to enable rfc6587 octet counting. | false | no | +| `timeout` | `duration`| Time to wait before marking a request as failed. | 5s | no | + +The `network` argument specifies if the syslog endpoint is using the TCP or UDP protocol. +`network` must be one of `tcp`, `udp` + +The `protocol` argument specifies the syslog format supported by the endpoint. +`protocol` must be one of `rfc5424`, `rfc3164` + +## Blocks + +The following blocks are supported inside the definition of `otelcol.exporter.syslog`: + +| Hierarchy | Block | Description | Required | +|------------------|----------------------|----------------------------------------------------------------------------|----------| +| tls | [tls][] | Configures TLS for a TCP connection. | no | +| sending_queue | [sending_queue][] | Configures batching of data before sending. | no | +| retry_on_failure | [retry_on_failure][] | Configures retry mechanism for failed requests. | no | +| debug_metrics | [debug_metrics][] | Configures the metrics that this component generates to monitor its state. | no | + +[tls]: #tls-block +[sending_queue]: #sending_queue-block +[retry_on_failure]: #retry_on_failure-block +[debug_metrics]: #debug_metrics-block + +### tls block + +The `tls` block configures TLS settings used for a connection to a TCP syslog server. + +{{< docs/shared lookup="reference/components/otelcol-tls-client-block.md" source="alloy" version="" >}} + +### sending_queue block + +The `sending_queue` block configures an in-memory buffer of batches before data is sent to the syslog server. + +{{< docs/shared lookup="reference/components/otelcol-queue-block.md" source="alloy" version="" >}} + +### retry_on_failure block + +The `retry_on_failure` block configures how failed requests to the syslog server are retried. + +{{< docs/shared lookup="reference/components/otelcol-retry-block.md" source="alloy" version="" >}} + +### debug_metrics block + +{{< docs/shared lookup="reference/components/otelcol-debug-metrics-block.md" source="alloy" version="" >}} + +## Exported fields + +The following fields are exported and can be referenced by other components: + +| Name | Type | Description +|--------|--------------------|----------------------------------------------------------------- +|`input` | `otelcol.Consumer` | A value that other components can use to send telemetry data to. + +`input` accepts `otelcol.Consumer` data for logs. Other telemetry signals are ignored. + +## Component health + +`otelcol.exporter.syslog` is only reported as unhealthy if given an invalid configuration. + +## Debug information + +`otelcol.exporter.syslog` doesn't expose any component-specific debug information. + +## Examples + +### TCP endpoint without TLS + +This example creates an exporter to send data to a syslog server expecting RFC5424-compliant messages over TCP without TLS: + +```alloy +otelcol.exporter.syslog "default" { + endpoint = "localhost" + tls { + insecure = true + insecure_skip_verify = true + } +} +``` + +### Use the `otelcol.processor.transform` component to format logs from `loki.source.syslog` + +This example shows one of the methods for annotating your loki messages into the format expected +by the exporter using a `otelcol.receiver.loki` component in addition to the `otelcol.processor.transform` +component. This example assumes that the log messages being parsed have come from a `loki.source.syslog` +component. This is just an example of some of the techniques that can be applied, and not a fully functioning +example for a specific incoming log. + +```alloy +otelcol.receiver.loki "default" { + output { + logs = [otelcol.processor.transform.syslog.input] + } +} + +otelcol.processor.transform "syslog" { + error_mode = "ignore" + + log_statements { + context = "log" + + statements = [ + `set(attributes["message"], attributes["__syslog_message"])`, + `set(attributes["appname"], attributes["__syslog_appname"])`, + `set(attributes["hostname"], attributes["__syslog_hostname"])`, + + // To set structured data you can chain index ([]) operations. + `set(attributes["structured_data"]["auth@32473"]["user"], attributes["__syslog_message_sd_auth_32473_user"])`, + `set(attributes["structured_data"]["auth@32473"]["user_host"], attributes["__syslog_message_sd_auth_32473_user_host"])`, + `set(attributes["structured_data"]["auth@32473"]["valid"], attributes["__syslog_message_sd_auth_32473_authenticated"])`, + ] + } + + output { + metrics = [] + logs = [otelcol.exporter.syslog.default.input] + traces = [] + } +} +``` + +### Use the `otelcol.processor.transform` component to format OpenTelemetry logs + +This example shows one of the methods for annotating your messages in the OpenTelemetry log format into the format expected +by the exporter using an `otelcol.processor.transform` component. This example assumes that the log messages being +parsed have come from another OpenTelemetry receiver in JSON format (or have been transformed to OpenTelemetry logs using +an `otelcol.receiver.loki` component). This is just an example of some of the techniques that can be applied, and not a +fully functioning example for a specific incoming log format. + +```alloy +otelcol.processor.transform "syslog" { + error_mode = "ignore" + + log_statements { + context = "log" + + statements = [ + // Parse body as JSON and merge the resulting map with the cache map, ignoring non-json bodies. + // cache is a field exposed by OTTL that is a temporary storage place for complex operations. + `merge_maps(cache, ParseJSON(body), "upsert") where IsMatch(body, "^\\{")`, + + // Set some example syslog attributes using the values from a JSON message body + // If the attribute doesn't exist in cache then nothing happens. + `set(attributes["message"], cache["log"])`, + `set(attributes["appname"], cache["application"])`, + `set(attributes["hostname"], cache["source"])`, + + // To set structured data you can chain index ([]) operations. + `set(attributes["structured_data"]["auth@32473"]["user"], attributes["user"])`, + `set(attributes["structured_data"]["auth@32473"]["user_host"], cache["source"])`, + `set(attributes["structured_data"]["auth@32473"]["valid"], cache["authenticated"])`, + + // Example priority setting, using facility 1 (user messages) and default to Info + `set(attributes["priority"], 14)`, + `set(attributes["priority"], 12) where severity_number == SEVERITY_NUMBER_WARN`, + `set(attributes["priority"], 11) where severity_number == SEVERITY_NUMBER_ERROR`, + `set(attributes["priority"], 10) where severity_number == SEVERITY_NUMBER_FATAL`, + ] + } + + output { + metrics = [] + logs = [otelcol.exporter.syslog.default.input] + traces = [] + } +} +``` + + + +## Compatible components + +`otelcol.exporter.syslog` has exports that can be consumed by the following components: + +- Components that consume [OpenTelemetry `otelcol.Consumer`](../../../compatibility/#opentelemetry-otelcolconsumer-consumers) + +{{< admonition type="note" >}} +Connecting some components may not be sensible or components may require further configuration to make the connection work correctly. +Refer to the linked documentation for more details. +{{< /admonition >}} + + diff --git a/go.mod b/go.mod index 5ab95d477b..67bc17844e 100644 --- a/go.mod +++ b/go.mod @@ -830,6 +830,7 @@ require ( github.com/ebitengine/purego v0.8.0 // indirect github.com/elastic/lunes v0.1.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/exporter/syslogexporter v0.112.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/kafka/topic v0.112.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect go.opentelemetry.io/collector/connector/connectorprofiles v0.112.0 // indirect diff --git a/go.sum b/go.sum index 409391ecb1..88bf96d1d0 100644 --- a/go.sum +++ b/go.sum @@ -1941,6 +1941,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexp github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter v0.112.0/go.mod h1:QwYTlmQDuLeaxS0HkIG9K9x45vQhHzL0SvI8inxzMeU= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/splunkhecexporter v0.112.0 h1:bIoCW8VYBEGnvpNYlamlvkPyeoQHCtfGgEuuELJYWYE= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/splunkhecexporter v0.112.0/go.mod h1:7usJQKG52/DDvzJ7Vm5+QEBE1eAYrVhEYbzYFzfkn2Q= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/syslogexporter v0.112.0 h1:p48hoUvtg9lWOlTFbaG9DfxKg15KK3V6cMXpyfCfoT4= +github.com/open-telemetry/opentelemetry-collector-contrib/exporter/syslogexporter v0.112.0/go.mod h1:CIFj32FwT/eauhbQgxUs53LObCOzoRhjLzZgDjOJB4Y= github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension v0.112.0 h1:RY0/7LTffj76403QxSlEjb0gnF788Qyfpxc+y32Rd6c= github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension v0.112.0/go.mod h1:1Z84oB3hwUH1B3IsL46csEtu7WA1qQJ/p6USTulGJf4= github.com/open-telemetry/opentelemetry-collector-contrib/extension/bearertokenauthextension v0.112.0 h1:GLh1rnXcY4P2hkMwuMLYCZMjZxze1KnciJXJTOFXOJ8= diff --git a/internal/component/all/all.go b/internal/component/all/all.go index caa0107764..f1c7323684 100644 --- a/internal/component/all/all.go +++ b/internal/component/all/all.go @@ -78,6 +78,7 @@ import ( _ "github.com/grafana/alloy/internal/component/otelcol/exporter/otlphttp" // Import otelcol.exporter.otlphttp _ "github.com/grafana/alloy/internal/component/otelcol/exporter/prometheus" // Import otelcol.exporter.prometheus _ "github.com/grafana/alloy/internal/component/otelcol/exporter/splunkhec" // Import otelcol.exporter.splunkhec + _ "github.com/grafana/alloy/internal/component/otelcol/exporter/syslog" // Import otelcol.exporter.syslog _ "github.com/grafana/alloy/internal/component/otelcol/extension/jaeger_remote_sampling" // Import otelcol.extension.jaeger_remote_sampling _ "github.com/grafana/alloy/internal/component/otelcol/processor/attributes" // Import otelcol.processor.attributes _ "github.com/grafana/alloy/internal/component/otelcol/processor/batch" // Import otelcol.processor.batch diff --git a/internal/component/common/config/types.go b/internal/component/common/config/types.go index 07d734288b..e35a25ab26 100644 --- a/internal/component/common/config/types.go +++ b/internal/component/common/config/types.go @@ -408,3 +408,32 @@ func (o *OAuth2Config) Validate() error { return o.ProxyConfig.Validate() } + +type SysLogFormat string + +const ( + // A modern Syslog RFC + SyslogFormatRFC5424 SysLogFormat = "rfc5424" + // A legacy Syslog RFC also known as BSD-syslog + SyslogFormatRFC3164 SysLogFormat = "rfc3164" +) + +// MarshalText implements encoding.TextMarshaler +func (s SysLogFormat) MarshalText() (text []byte, err error) { + return []byte(s), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler +func (s *SysLogFormat) UnmarshalText(text []byte) error { + str := string(text) + switch str { + case "rfc5424": + *s = SyslogFormatRFC5424 + case "rfc3164": + *s = SyslogFormatRFC3164 + default: + return fmt.Errorf("unknown syslog format: %s", str) + } + + return nil +} diff --git a/internal/component/loki/source/syslog/types.go b/internal/component/loki/source/syslog/types.go index 9fbe304160..a995fadca8 100644 --- a/internal/component/loki/source/syslog/types.go +++ b/internal/component/loki/source/syslog/types.go @@ -11,25 +11,18 @@ import ( st "github.com/grafana/alloy/internal/component/loki/source/syslog/internal/syslogtarget" ) -const ( - // A modern Syslog RFC - SyslogFormatRFC5424 = "rfc5424" - // A legacy Syslog RFC also known as BSD-syslog - SyslogFormatRFC3164 = "rfc3164" -) - // ListenerConfig defines a syslog listener. type ListenerConfig struct { - ListenAddress string `alloy:"address,attr"` - ListenProtocol string `alloy:"protocol,attr,optional"` - IdleTimeout time.Duration `alloy:"idle_timeout,attr,optional"` - LabelStructuredData bool `alloy:"label_structured_data,attr,optional"` - Labels map[string]string `alloy:"labels,attr,optional"` - UseIncomingTimestamp bool `alloy:"use_incoming_timestamp,attr,optional"` - UseRFC5424Message bool `alloy:"use_rfc5424_message,attr,optional"` - MaxMessageLength int `alloy:"max_message_length,attr,optional"` - TLSConfig config.TLSConfig `alloy:"tls_config,block,optional"` - SyslogFormat string `alloy:"syslog_format,attr,optional"` + ListenAddress string `alloy:"address,attr"` + ListenProtocol string `alloy:"protocol,attr,optional"` + IdleTimeout time.Duration `alloy:"idle_timeout,attr,optional"` + LabelStructuredData bool `alloy:"label_structured_data,attr,optional"` + Labels map[string]string `alloy:"labels,attr,optional"` + UseIncomingTimestamp bool `alloy:"use_incoming_timestamp,attr,optional"` + UseRFC5424Message bool `alloy:"use_rfc5424_message,attr,optional"` + MaxMessageLength int `alloy:"max_message_length,attr,optional"` + TLSConfig config.TLSConfig `alloy:"tls_config,block,optional"` + SyslogFormat config.SysLogFormat `alloy:"syslog_format,attr,optional"` } // DefaultListenerConfig provides the default arguments for a syslog listener. @@ -37,7 +30,7 @@ var DefaultListenerConfig = ListenerConfig{ ListenProtocol: st.DefaultProtocol, IdleTimeout: st.DefaultIdleTimeout, MaxMessageLength: st.DefaultMaxMessageLength, - SyslogFormat: SyslogFormatRFC5424, + SyslogFormat: config.SyslogFormatRFC5424, } // SetToDefault implements syntax.Defaulter. @@ -85,11 +78,11 @@ func (sc ListenerConfig) Convert() (*scrapeconfig.SyslogTargetConfig, error) { }, nil } -func convertSyslogFormat(format string) (scrapeconfig.SyslogFormat, error) { +func convertSyslogFormat(format config.SysLogFormat) (scrapeconfig.SyslogFormat, error) { switch format { - case SyslogFormatRFC3164: + case config.SyslogFormatRFC3164: return scrapeconfig.SyslogFormatRFC3164, nil - case SyslogFormatRFC5424: + case config.SyslogFormatRFC5424: return scrapeconfig.SyslogFormatRFC5424, nil default: return "", fmt.Errorf("unknown syslog format %q", format) diff --git a/internal/component/otelcol/exporter/syslog/syslog.go b/internal/component/otelcol/exporter/syslog/syslog.go new file mode 100644 index 0000000000..ded5330c83 --- /dev/null +++ b/internal/component/otelcol/exporter/syslog/syslog.go @@ -0,0 +1,104 @@ +// Package syslog provides an otelcol.exporter.syslog component. +package syslog + +import ( + "time" + + "github.com/grafana/alloy/internal/component" + "github.com/grafana/alloy/internal/component/common/config" + "github.com/grafana/alloy/internal/component/otelcol" + otelcolCfg "github.com/grafana/alloy/internal/component/otelcol/config" + "github.com/grafana/alloy/internal/component/otelcol/exporter" + "github.com/grafana/alloy/internal/featuregate" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/syslogexporter" + otelcomponent "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/confignet" + otelpexporterhelper "go.opentelemetry.io/collector/exporter/exporterhelper" + otelextension "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/pipeline" +) + +func init() { + component.Register(component.Registration{ + Name: "otelcol.exporter.syslog", + Stability: featuregate.StabilityPublicPreview, + Args: Arguments{}, + Exports: otelcol.ConsumerExports{}, + + Build: func(opts component.Options, args component.Arguments) (component.Component, error) { + fact := syslogexporter.NewFactory() + return exporter.New(opts, fact, args.(Arguments), exporter.TypeLogs) + }, + }) +} + +// Arguments configures the otelcol.exporter.syslog component. +type Arguments struct { + Timeout time.Duration `alloy:"timeout,attr,optional"` + + Queue otelcol.QueueArguments `alloy:"sending_queue,block,optional"` + Retry otelcol.RetryArguments `alloy:"retry_on_failure,block,optional"` + + // DebugMetrics configures component internal metrics. Optional. + DebugMetrics otelcolCfg.DebugMetricsArguments `alloy:"debug_metrics,block,optional"` + + TLS otelcol.TLSClientArguments `alloy:"tls,block,optional"` + + Endpoint string `alloy:"endpoint,attr"` + Port int `alloy:"port,attr,optional"` // default: 514 + Network string `alloy:"network,attr,optional"` // default: "tcp", also supported "udp" + Protocol config.SysLogFormat `alloy:"protocol,attr,optional"` // default: "rfc5424", also supported "rfc3164" + + // Whether or not to enable RFC 6587 Octet Counting. + EnableOctetCounting bool `alloy:"enable_octet_counting,attr,optional"` +} + +var _ exporter.Arguments = Arguments{} + +// SetToDefault implements syntax.Defaulter. +func (args *Arguments) SetToDefault() { + *args = Arguments{ + Timeout: otelcol.DefaultTimeout, + Port: 514, + Network: string(confignet.TransportTypeTCP), + Protocol: config.SyslogFormatRFC5424, + } + + args.Queue.SetToDefault() + args.Queue.Enabled = false // Upstream has this disabled by default + args.Retry.SetToDefault() + args.DebugMetrics.SetToDefault() + +} + +// Convert implements exporter.Arguments. +func (args Arguments) Convert() (otelcomponent.Config, error) { + return &syslogexporter.Config{ + TimeoutSettings: otelpexporterhelper.TimeoutConfig{ + Timeout: args.Timeout, + }, + QueueSettings: *args.Queue.Convert(), + BackOffConfig: *args.Retry.Convert(), + Endpoint: args.Endpoint, + Port: args.Port, + Network: args.Network, + Protocol: string(args.Protocol), + TLSSetting: *args.TLS.Convert(), + EnableOctetCounting: args.EnableOctetCounting, + }, nil +} + +// Extensions implements exporter.Arguments. +func (args Arguments) Extensions() map[otelcomponent.ID]otelextension.Extension { + return nil +} + +// Exporters implements exporter.Arguments. +func (args Arguments) Exporters() map[pipeline.Signal]map[otelcomponent.ID]otelcomponent.Component { + return nil +} + +// DebugMetricsConfig implements exporter.Arguments. +func (args Arguments) DebugMetricsConfig() otelcolCfg.DebugMetricsArguments { + return args.DebugMetrics +} diff --git a/internal/component/otelcol/exporter/syslog/syslog_test.go b/internal/component/otelcol/exporter/syslog/syslog_test.go new file mode 100644 index 0000000000..c44053ec65 --- /dev/null +++ b/internal/component/otelcol/exporter/syslog/syslog_test.go @@ -0,0 +1,139 @@ +package syslog_test + +import ( + "fmt" + "io" + "net" + "testing" + "time" + + "github.com/grafana/alloy/internal/component/otelcol" + "github.com/grafana/alloy/internal/component/otelcol/exporter/syslog" + "github.com/grafana/alloy/internal/runtime/componenttest" + "github.com/grafana/alloy/internal/runtime/logging/level" + "github.com/grafana/alloy/internal/util" + "github.com/grafana/alloy/syntax" + "github.com/grafana/dskit/backoff" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" +) + +// Test performs a basic integration test which runs the otelcol.exporter.syslog +// component and ensures that it can pass data to a syslog receiver. +func Test(t *testing.T) { + ch := make(chan string, 1) + h, p := makeSyslogServer(t, ch) + + ctx := componenttest.TestContext(t) + l := util.TestLogger(t) + + ctrl, err := componenttest.NewControllerFromID(l, "otelcol.exporter.syslog") + require.NoError(t, err) + + cfg := fmt.Sprintf(` + timeout = "250ms" + endpoint = "%s" + port = %s + + tls { + insecure = true + insecure_skip_verify = true + } + + debug_metrics { + disable_high_cardinality_metrics = true + } + `, h, p) + var args syslog.Arguments + require.NoError(t, syntax.Unmarshal([]byte(cfg), &args)) + require.Equal(t, args.DebugMetricsConfig().DisableHighCardinalityMetrics, true) + + go func() { + err := ctrl.Run(ctx, args) + require.NoError(t, err) + }() + + require.NoError(t, ctrl.WaitRunning(time.Second), "component never started") + require.NoError(t, ctrl.WaitExports(time.Second), "component never exported anything") + + // Truncating to second to make the syslog representation consistent, as the OTLP + // representation doesn't show the sub-second precision consistently. + timestamp := time.Now().Truncate(time.Second) + + // Send logs in the background to our exporter. + go func() { + exports := ctrl.Exports().(otelcol.ConsumerExports) + + bo := backoff.New(ctx, backoff.Config{ + MinBackoff: 10 * time.Millisecond, + MaxBackoff: 100 * time.Millisecond, + }) + for bo.Ongoing() { + err := exports.Input.ConsumeLogs(ctx, createTestLogs(timestamp)) + if err != nil { + level.Error(l).Log("msg", "failed to send logs", "err", err) + bo.Wait() + continue + } + + return + } + }() + + // Wait for our exporter to finish and pass data to our HTTP server. + select { + case <-time.After(time.Second): + require.FailNow(t, "failed waiting for logs") + case log := <-ch: + // Order of the structured data is not guaranteed, so we need to check for both possible orders. + expected := []string{ + fmt.Sprintf("<165>1 %s test-host Application 12345 - [Auth Realm=\"ADMIN\" User=\"root\"] This is a test log\n", timestamp.UTC().Format("2006-01-02T15:04:05Z")), + fmt.Sprintf("<165>1 %s test-host Application 12345 - [Auth User=\"root\" Realm=\"ADMIN\"] This is a test log\n", timestamp.UTC().Format("2006-01-02T15:04:05Z")), + } + require.Contains(t, expected, log) + } +} + +func makeSyslogServer(t *testing.T, ch chan string) (string, string) { + t.Helper() + + // Create a TCP listener on port 514 + conn, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + go func() { + c, err := conn.Accept() + require.NoError(t, err) + + msg, err := io.ReadAll(c) + require.NoError(t, err) + + err = c.Close() + require.NoError(t, err) + + ch <- string(msg) + }() + + t.Cleanup(func() { conn.Close() }) + + h, p, err := net.SplitHostPort(conn.Addr().String()) + require.NoError(t, err) + + return h, p +} + +func createTestLogs(t time.Time) plog.Logs { + logs := plog.NewLogs() + l := logs.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty() + l.SetTimestamp(pcommon.NewTimestampFromTime(t)) + l.Attributes().PutStr("appname", "Application") + l.Attributes().PutStr("hostname", "test-host") + l.Attributes().PutStr("message", "This is a test log") + l.Attributes().PutStr("proc_id", "12345") + struc := map[string]interface{}{ + "Auth": map[string]interface{}{"Realm": "ADMIN", "User": "root"}, + } + l.Attributes().PutEmptyMap("structured_data").FromRaw(struc) + return logs +} diff --git a/internal/converter/internal/otelcolconvert/converter_syslogexporter.go b/internal/converter/internal/otelcolconvert/converter_syslogexporter.go new file mode 100644 index 0000000000..2aecdf7040 --- /dev/null +++ b/internal/converter/internal/otelcolconvert/converter_syslogexporter.go @@ -0,0 +1,59 @@ +package otelcolconvert + +import ( + "fmt" + + "github.com/grafana/alloy/internal/component/common/config" + "github.com/grafana/alloy/internal/component/otelcol/exporter/syslog" + "github.com/grafana/alloy/internal/converter/diag" + "github.com/grafana/alloy/internal/converter/internal/common" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/syslogexporter" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componentstatus" +) + +func init() { + converters = append(converters, syslogExporterConverter{}) +} + +type syslogExporterConverter struct{} + +func (syslogExporterConverter) Factory() component.Factory { + return syslogexporter.NewFactory() +} + +func (syslogExporterConverter) InputComponentName() string { + return "otelcol.exporter.syslog" +} + +func (syslogExporterConverter) ConvertAndAppend(state *State, id componentstatus.InstanceID, cfg component.Config) diag.Diagnostics { + var diags diag.Diagnostics + + label := state.AlloyComponentLabel() + + args := toOtelcolExportersyslog(cfg.(*syslogexporter.Config)) + block := common.NewBlockWithOverride([]string{"otelcol", "exporter", "syslog"}, label, args) + + diags.Add( + diag.SeverityLevelInfo, + fmt.Sprintf("Converted %s into %s", StringifyInstanceID(id), StringifyBlock(block)), + ) + + state.Body().AppendBlock(block) + return diags +} + +func toOtelcolExportersyslog(cfg *syslogexporter.Config) *syslog.Arguments { + return &syslog.Arguments{ + Queue: toQueueArguments(cfg.QueueSettings), + Retry: toRetryArguments(cfg.BackOffConfig), + DebugMetrics: common.DefaultValue[syslog.Arguments]().DebugMetrics, + TLS: toTLSClientArguments(cfg.TLSSetting), + Endpoint: cfg.Endpoint, + Port: cfg.Port, + Network: cfg.Network, + Protocol: config.SysLogFormat(cfg.Protocol), + Timeout: cfg.TimeoutSettings.Timeout, + EnableOctetCounting: cfg.EnableOctetCounting, + } +} diff --git a/internal/converter/internal/promtailconvert/internal/build/syslog.go b/internal/converter/internal/promtailconvert/internal/build/syslog.go index f45291ab80..652e8d84d9 100644 --- a/internal/converter/internal/promtailconvert/internal/build/syslog.go +++ b/internal/converter/internal/promtailconvert/internal/build/syslog.go @@ -3,6 +3,7 @@ package build import ( "fmt" + "github.com/grafana/alloy/internal/component/common/config" "github.com/grafana/alloy/internal/component/common/relabel" "github.com/grafana/alloy/internal/component/loki/source/syslog" "github.com/grafana/alloy/internal/converter/diag" @@ -36,7 +37,7 @@ func (s *ScrapeConfigBuilder) AppendSyslogConfig() { // If the syslog format is not set, use the default. if listenerConfig.SyslogFormat == "" { - listenerConfig.SyslogFormat = string(syslog.DefaultListenerConfig.SyslogFormat) + listenerConfig.SyslogFormat = syslog.DefaultListenerConfig.SyslogFormat } args := syslog.Arguments{ @@ -64,14 +65,14 @@ func (s *ScrapeConfigBuilder) AppendSyslogConfig() { )) } -func convertSyslogFormat(format scrapeconfig.SyslogFormat) (string, error) { +func convertSyslogFormat(format scrapeconfig.SyslogFormat) (config.SysLogFormat, error) { switch format { case "": return syslog.DefaultListenerConfig.SyslogFormat, nil case scrapeconfig.SyslogFormatRFC3164: - return syslog.SyslogFormatRFC3164, nil + return config.SyslogFormatRFC3164, nil case scrapeconfig.SyslogFormatRFC5424: - return syslog.SyslogFormatRFC5424, nil + return config.SyslogFormatRFC5424, nil default: return "", fmt.Errorf("unknown syslog format %q", format) }