Skip to content

Commit

Permalink
[exporter/loki] Refactor the Loki exporter (#13572)
Browse files Browse the repository at this point in the history
This commit brings a new implementation of the Loki exporter while
maintaining compatibility with the previous version (called "legacy" in
this PR). The new exporter has a cleaner logic, allowing the Loki labels
to be configured as part of the data points instead of being
part of the static configuration. This will be useful in the future when
migrating from the Loki exporter to a native OTLP endpoint in Loki.

Fixes #12873

Signed-off-by: Juraci Paixão Kröhling <[email protected]>
  • Loading branch information
jpkrohling authored Sep 1, 2022
1 parent 7d1f90a commit adc8fdf
Show file tree
Hide file tree
Showing 28 changed files with 1,706 additions and 839 deletions.
17 changes: 17 additions & 0 deletions exporter/lokiexporter/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Contributing to the Loki Exporter
===

In order to contribute to this exporter, it might be useful to have a local setup of Loki, or a free account
at Grafana Labs.

Local Loki
---

* Download and unpack the Loki package on a local directory, like `~/bin`
* Download a [config file](https://raw.githubusercontent.com/grafana/loki/master/cmd/loki/loki-local-config.yaml) and store as `config.yaml` on the local directory
* Start Loki with `$ loki-linux-amd64`, making sure the config file is on the current directory

Now, we need a tool to interact with Loki. Either use Grafana and configure it with a Loki data source, or use `logcli`:

* Download and unpack the `logcli` package on a local directory, like `~/bin`
* You are now ready to issue queries to Loki, such as: `logcli-linux-amd64 query '{exporter="OTLP"}'`
144 changes: 67 additions & 77 deletions exporter/lokiexporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,97 +13,87 @@ Exports data via HTTP to [Loki](https://grafana.com/docs/loki/latest/).
The following settings are required:

- `endpoint` (no default): The target URL to send Loki log streams to (e.g.: `http://loki:3100/loki/api/v1/push`).

- `labels.{attributes/resource}` (no default): Either a map of attributes or resource names to valid Loki label names
(must match "^[a-zA-Z_][a-zA-Z0-9_]*$") allowed to be added as labels to Loki log streams.
Attributes are log record attributes that describe the log message itself. Resource attributes are attributes that
belong to the infrastructure that create the log (container_name, cluster_name, etc.). At least one attribute from
attribute or resource is required
Logs that do not have at least one of these attributes will be dropped.
This is a safety net to help prevent accidentally adding dynamic labels that may significantly increase cardinality,
thus having a performance impact on your Loki instance. See the
[Loki label best practices](https://grafana.com/docs/loki/latest/best-practices/) page for
additional details on the types of labels you may want to associate with log streams.

- `labels.record` (no default): A map of record attributes to valid Loki label names (must match
"^[a-zA-Z_][a-zA-Z0-9_]*$") allowed to be added as labels to Loki log streams.
Record attributes can be: `traceID`, `spanID`, `severity`, `severityN`. These attributes will be added as log labels
and will be removed from the log body.

The following settings can be optionally configured:

- `tenant`: composed of the properties `tenant.source` and `tenant.value`.
- `tenant.source`: one of "static", "context", or "attribute".
- `tenant.value`: the semantics depend on the tenant source. See the "Tenant information" section.

- `tls`:
- `insecure` (default = false): When set to true disables verifying the server's certificate chain and host name. The
connection is still encrypted but server identity is not verified.
- `ca_file` (no default) Path to the CA cert to verify the server being connected to. Should only be used if `insecure`
is set to false.
- `cert_file` (no default) Path to the TLS cert to use for client connections when TLS client auth is required.
Should only be used if `insecure` is set to false.
- `key_file` (no default) Path to the TLS key to use for TLS required connections. Should only be used if `insecure` is
set to false.


- `timeout` (default = 30s): HTTP request time limit. For details see https://golang.org/pkg/net/http/#Client
- `read_buffer_size` (default = 0): ReadBufferSize for HTTP client.
- `write_buffer_size` (default = 512 * 1024): WriteBufferSize for HTTP client.


- `headers` (no default): Name/value pairs added to the HTTP request headers.

- `format` Deprecated without replacement. If you rely on this, let us know by opening an issue before v0.59.0 and we'll
assist you in finding a solution. The current default is `body` but the `json` encoder will be used after v0.59.0. To be
ready for future versions, set this to `json` explicitly.

Example:
The following options are now deprecated:

- `labels.{attributes/resource}`. Deprecated and will be removed by v0.59.0. See the [Labels](#labels) section for more information.
- `labels.record`. Deprecated and will be removed by v0.59.0. See the [Labels](#labels) section for more information.
- `tenant`: Deprecated and will be removed by v0.59.0. See the [Labels](#tenant-information) section for more information.
- `format` Deprecated without replacement. If you rely on this, let us know by opening an issue before v0.59.0 and we'll assist you in finding a solution.

Example:
```yaml
loki:
endpoint: http://loki:3100/loki/api/v1/push
tenant_id: "example"
labels:
resource:
# Allowing 'container.name' attribute and transform it to 'container_name', which is a valid Loki label name.
container.name: "container_name"
# Allowing 'k8s.cluster.name' attribute and transform it to 'k8s_cluster_name', which is a valid Loki label name.
k8s.cluster.name: "k8s_cluster_name"
receivers:
otlp:

exporters:
loki:
endpoint: https://loki.example.com:3100/loki/api/v1/push

processors:
attributes:
actions:
- action: insert
key: loki.attribute.labels
value: [http.status_code]

resource:
attributes:
# Allowing 'severity' attribute and not providing a mapping, since the attribute name is a valid Loki label name.
severity: ""
http.status_code: "http_status_code"
record:
# Adds 'traceID' as a log label, seen as 'traceid' in Loki.
traceID: "traceid"

headers:
"X-Custom-Header": "loki_rocks"
- action: insert
key: loki.attribute.labels
value: [http.status]
- action: insert
key: loki.resource.labels
value: [host.name, pod.name]

extensions:

service:
extensions:
pipelines:
logs:
receivers: [otlp]
processors: [resource, attributes]
exporters: [loki]
```
The full list of settings exposed for this exporter are documented [here](./config.go) with detailed sample
configurations [here](./testdata/config.yaml).
## Tenant information
## Labels
This processor is able to acquire the tenant ID based on different sources. At this moment, there are three possible sources:
The Loki exporter can convert OTLP resource and log attributes into Loki labels, which are indexed. For that, you need to configure
hints, specifying which attributes should be placed as labels. The hints are themselves attributes and will be ignored when
exporting to Loki. The following example uses the `attributes` processor to hint the Loki exporter to set the `http.status_code`
attribute as label and the `resource` processor to give a hint to the Loki exporter to set the `pod.name` as label.

- static
- context
- attribute
```yaml
processors:
attributes:
actions:
- action: insert
key: loki.attribute.labels
value: [http.status_code]
resource:
attributes:
- action: insert
key: loki.resource.labels
value: [pod.name]
```

Each one has a strategy for obtaining the tenant ID, as follows:
## Tenant information

- when "static" is set, the tenant is the literal value from the "tenant.value" property.
- when "context" is set, the tenant is looked up from the request metadata, such as HTTP headers, using the "value" as the
key (likely the header name).
- when "attribute" is set, the tenant is looked up from the resource attributes in the batch: the first value found among
the resource attributes is used. If you intend to have multiple tenants per HTTP request, make sure to use a processor
that groups tenants in batches, such as the `groupbyattrs` processor.
It is recommended to use the [`header_setter`](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/headerssetter) extension to configure the tenant information to send to Loki. In case a static tenant
should be used, you can make use of the `headers` option for regular HTTP client settings, like the following:

The value that is determined to be the tenant is then sent as the value for the HTTP header `X-Scope-OrgID`. When a tenant
is not provided, or a tenant cannot be determined, the logs are still sent to Loki but without the HTTP header.
```yaml
exporters:
loki:
endpoint: http://localhost:3100/loki/api/v1/push
headers:
"X-Scope-OrgID": acme
```

## Advanced Configuration

Expand Down
105 changes: 28 additions & 77 deletions exporter/lokiexporter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"fmt"
"net/url"

"github.com/prometheus/common/model"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/exporter/exporterhelper"
Expand All @@ -32,12 +31,14 @@ type Config struct {
exporterhelper.RetrySettings `mapstructure:"retry_on_failure"`

// TenantID defines the tenant ID to associate log streams with.
// Deprecated: use Tenant instead, with Source set to "static" and
// Value set to the value you'd set in this field.
TenantID string `mapstructure:"tenant_id"`
// Deprecated: [v0.57.0] use the attribute processor to add a `loki.tenant` hint.
// See this component's documentation for more information on how to specify the hint.
TenantID *string `mapstructure:"tenant_id"`

// Labels defines how labels should be applied to log streams sent to Loki.
Labels LabelsConfig `mapstructure:"labels"`
// Deprecated: [v0.57.0] use the attribute processor to add a `loki.attribute.labels` hint.
// See this component's documentation for more information on how to specify the hint.
Labels *LabelsConfig `mapstructure:"labels"`

// Allows you to choose the entry format in the exporter.
// Deprecated: [v0.57.0] Only the JSON format will be supported in the future. If you rely on the
Expand All @@ -46,104 +47,54 @@ type Config struct {
Format *string `mapstructure:"format"`

// Tenant defines how to obtain the tenant ID
// Deprecated: [v0.57.0] use the attribute processor to add a `loki.tenant` hint.
// See this component's documentation for more information on how to specify the hint.
Tenant *Tenant `mapstructure:"tenant"`
}

type Tenant struct {
// Source defines where to obtain the tenant ID. Possible values: static, context, attribute.
Source string `mapstruct:"source"`

// Value will be used by the tenant source provider to lookup the value. For instance,
// when the source=static, the value is a static value. When the source=context, value
// should be the context key that holds the tenant information.
Value string `mapstruct:"value"`
}

func (c *Config) validate() error {
func (c *Config) Validate() error {
if _, err := url.Parse(c.Endpoint); c.Endpoint == "" || err != nil {
return fmt.Errorf("\"endpoint\" must be a valid URL")
}

// further validation is needed only if we are in legacy mode
if !c.isLegacy() {
return nil
}

if c.Tenant != nil {
if c.Tenant.Source != "attributes" && c.Tenant.Source != "context" && c.Tenant.Source != "static" {
return fmt.Errorf("invalid tenant source, must be one of 'attributes', 'context', 'static', but is %s", c.Tenant.Source)
}

if len(c.TenantID) > 0 {
if c.TenantID != nil && *c.TenantID != "" {
return fmt.Errorf("both tenant_id and tenant were specified, use only 'tenant' instead")
}
}

return c.Labels.validate()
}
if c.Labels != nil {
return c.Labels.validate()
}

func (c *Config) Validate() error {
return nil
}

// LabelsConfig defines the labels-related configuration
type LabelsConfig struct {
// Attributes are the log record attributes that are allowed to be added as labels on a log stream.
Attributes map[string]string `mapstructure:"attributes"`

// ResourceAttributes are the resource attributes that are allowed to be added as labels on a log stream.
ResourceAttributes map[string]string `mapstructure:"resource"`

// RecordAttributes are the attributes from the record that are allowed to be added as labels on a log stream. Possible keys:
// traceID, spanID, severity, severityN.
RecordAttributes map[string]string `mapstructure:"record"`
}

func (c *LabelsConfig) validate() error {
if len(c.Attributes) == 0 && len(c.ResourceAttributes) == 0 && len(c.RecordAttributes) == 0 {
return fmt.Errorf("\"labels.attributes\", \"labels.resource\", or \"labels.record\" must be configured with at least one attribute")
func (c *Config) isLegacy() bool {
if c.Format != nil && *c.Format == "body" {
return true
}

logRecordNameInvalidErr := "the label `%s` in \"labels.attributes\" is not a valid label name. Label names must match " + model.LabelNameRE.String()
for l, v := range c.Attributes {
if len(v) > 0 && !model.LabelName(v).IsValid() {
return fmt.Errorf(logRecordNameInvalidErr, v)
} else if len(v) == 0 && !model.LabelName(l).IsValid() {
return fmt.Errorf(logRecordNameInvalidErr, l)
}
}

resourceNameInvalidErr := "the label `%s` in \"labels.resource\" is not a valid label name. Label names must match " + model.LabelNameRE.String()
for l, v := range c.ResourceAttributes {
if len(v) > 0 && !model.LabelName(v).IsValid() {
return fmt.Errorf(resourceNameInvalidErr, v)
} else if len(v) == 0 && !model.LabelName(l).IsValid() {
return fmt.Errorf(resourceNameInvalidErr, l)
}
if c.Labels != nil {
return true
}

possibleRecordAttributes := map[string]bool{
"traceID": true,
"spanID": true,
"severity": true,
"severityN": true,
}
for k := range c.RecordAttributes {
if _, found := possibleRecordAttributes[k]; !found {
return fmt.Errorf("record attribute %q not recognized, possible values: traceID, spanID, severity, severityN", k)
}
if c.Tenant != nil {
return true
}
return nil
}

// getAttributes creates a lookup of allowed attributes to valid Loki label names.
func (c *LabelsConfig) getAttributes(labels map[string]string) map[string]model.LabelName {

attributes := map[string]model.LabelName{}

for attrName, lblName := range labels {
if len(lblName) > 0 {
attributes[attrName] = model.LabelName(lblName)
continue
}

attributes[attrName] = model.LabelName(attrName)
if c.TenantID != nil {
return true
}

return attributes
return false
}
Loading

0 comments on commit adc8fdf

Please sign in to comment.