Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions docs/sources/reference/components/loki/loki.process.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,15 +582,16 @@ stage.label_keep {

### `stage.labels`

The `stage.labels` inner block configures a labels processing stage that can read data from the extracted values map and set new labels on incoming log entries.
The `stage.labels` inner block configures a labels processing stage that can read data from the extracted values map or structured metadata and set new labels on incoming log entries.

For labels that are static, refer to [`stage.static_labels`][stage.static_labels]

The following arguments are supported:

| Name | Type | Description | Default | Required |
| -------- | ------------- | --------------------------------------- | ------- | -------- |
| `values` | `map(string)` | Configures a `labels` processing stage. | `{}` | no |
| Name | Type | Description | Default | Required |
| ------------- | ------------- | -------------------------------------------------------------------------------------------------------- | ------------- | -------- |
| `values` | `map(string)` | Configures a `labels` processing stage. | `{}` | no |
| `source_type` | `string` | Where to retrieve the data from. Allowed values are `"extracted"` (default) or `"structured_metadata"`. | `"extracted"` | no |

In a labels stage, the map's keys define the label to set and the values are how to look them up.
If the value is empty, it's inferred to be the same as the key.
Expand All @@ -604,6 +605,21 @@ stage.labels {
}
```

```alloy
stage.labels {
Copy link
Copy Markdown
Contributor

@dehaansa dehaansa Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example should include the recommendation to strip the moved attributes with a stage.structured_metadata_drop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

source_type = "structured_metadata"
values = {
env = "", // Sets up an 'env' label, based on the 'env' structured metadata value.
user = "username", // Sets up a 'user' label, based on the 'username' structured metadata value.
}
}

// Drop the converted structured metadata
stage.structured_metadata_drop {
values = [ "env", "username" ]
}
```

### `stage.limit`

The `stage.limit` inner block configures a rate-limiting stage that throttles logs based on several options.
Expand Down
98 changes: 78 additions & 20 deletions internal/component/loki/process/stages/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,51 @@ import (
"errors"
"fmt"
"reflect"
"time"

"github.com/go-kit/log"
"github.com/prometheus/common/model"

"github.com/grafana/alloy/internal/runtime/logging/level"
"github.com/grafana/loki/pkg/push"
)

const (
ErrEmptyLabelStageConfig = "label stage config cannot be empty"
ErrInvalidLabelName = "invalid label name: %s"
ErrInvalidSourceType = "invalid labels source_type: %s. Can only be 'extracted' or 'structured_metadata'"

LabelsSourceStructuredMetadata string = "structured_metadata"
LabelsSourceExtractedMap string = "extracted"
)

// LabelsConfig is a set of labels to be extracted
type LabelsConfig struct {
Values map[string]*string `alloy:"values,attr"`
Values map[string]*string `alloy:"values,attr"`
SourceType SourceType `alloy:"source_type,attr,optional"`
}

// validateLabelsConfig validates the Label stage configuration
func validateLabelsConfig(c map[string]*string) (map[string]string, error) {
func validateLabelsConfig(cfg *LabelsConfig) (map[string]string, error) {
if cfg.Values == nil {
return nil, errors.New(ErrEmptyLabelStageConfig)
}

if cfg.SourceType == "" {
cfg.SourceType = SourceTypeExtractedMap
}

switch cfg.SourceType {
case SourceTypeExtractedMap, SourceTypeStructuredMetadata:
default:
return nil, fmt.Errorf(ErrInvalidSourceType, cfg.SourceType)
}

// We must not mutate the c.Values, create a copy with changes we need.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this comment but then we still change c.Values to the new map, why was this change necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was implemented to get access to SourceType (and it's default, set above) in Run.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure but we should still not set new values for c.Values but return a map like we did before this change.

We can keep both in labelStage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, will change it! Does this mean I'm not supposed to change SourceType as well within validateLabelsConfig? Asking because that's the reason why I changed it to the current implementation and the idea, again, was taken from the truncate stage. Sorry for bothering you with this!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally not.
There are two interfaces that you can implement for LabelsConfig:

  1. syntax.Validator - with this you can perform validation when config is evaluated. So when starting alloy we could catch these mistakes earlier.
  2. syntax.Defaulter - with this you can set default values.

Ideally we use these but I have noticed that a lot of stage configs don't do it but I don't want to block this work further so lets just create the map like it did before and return it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification! :) I've reverted the changes from validateLabelsConfig.

ret := map[string]string{}
if c == nil {
if cfg.Values == nil {
return nil, errors.New(ErrEmptyLabelStageConfig)
}
for labelName, labelSrc := range c {
for labelName, labelSrc := range cfg.Values {
// TODO: add support for different validation schemes.
//nolint:staticcheck
if !model.LabelName(labelName).IsValid() {
Expand All @@ -42,54 +61,93 @@ func validateLabelsConfig(c map[string]*string) (map[string]string, error) {
ret[labelName] = *labelSrc
}
}

return ret, nil
}

// newLabelStage creates a new label stage to set labels from extracted data
func newLabelStage(logger log.Logger, configs LabelsConfig) (Stage, error) {
labelsConfig, err := validateLabelsConfig(configs.Values)
labelsConfig, err := validateLabelsConfig(&configs)
if err != nil {
return nil, err
}
return toStage(&labelStage{
return &labelStage{
cfg: &configs,
labelsConfig: labelsConfig,
logger: logger,
}), nil
}, nil
}

// labelStage sets labels from extracted data
type labelStage struct {
cfg *LabelsConfig
labelsConfig map[string]string
logger log.Logger
}

// Process implements Stage
func (l *labelStage) Process(labels model.LabelSet, extracted map[string]any, _ *time.Time, _ *string) {
processLabelsConfigs(l.logger, extracted, l.labelsConfig, func(labelName model.LabelName, labelValue model.LabelValue) {
labels[labelName] = labelValue
})
// Run implements Stage
func (l *labelStage) Run(in chan Entry) chan Entry {
Comment thread
kalleep marked this conversation as resolved.
out := make(chan Entry)
go func() {
defer close(out)
for e := range in {
switch l.cfg.SourceType {
case SourceTypeExtractedMap:
l.addLabelFromExtractedMap(e.Labels, e.Extracted)
case SourceTypeStructuredMetadata:
l.addLabelsFromStructuredMetadata(e.Labels, e.StructuredMetadata)
Comment thread
kalleep marked this conversation as resolved.
}
out <- e
}
}()
return out
}

type labelsConsumer func(labelName model.LabelName, labelValue model.LabelValue)

func processLabelsConfigs(logger log.Logger, extracted map[string]any, labelsConfig map[string]string, consumer labelsConsumer) {
for lName, lSrc := range labelsConfig {
func (l *labelStage) addLabelFromExtractedMap(labels model.LabelSet, extracted map[string]any) {
for lName, lSrc := range l.labelsConfig {
if lValue, ok := extracted[lSrc]; ok {
s, err := getString(lValue)
if err != nil {
if Debug {
level.Debug(logger).Log("msg", "failed to convert extracted label value to string", "err", err, "type", reflect.TypeOf(lValue))
level.Debug(l.logger).Log("msg", "failed to convert extracted label value to string", "err", err, "type", reflect.TypeOf(lValue))
}
continue
}
labelValue := model.LabelValue(s)
if !labelValue.IsValid() {
if Debug {
level.Debug(logger).Log("msg", "invalid label value parsed", "value", labelValue)
level.Debug(l.logger).Log("msg", "invalid label value parsed", "value", labelValue)
}
continue
}
consumer(model.LabelName(lName), labelValue)

labels[model.LabelName(lName)] = labelValue
}
}
}

func (l *labelStage) addLabelsFromStructuredMetadata(labels model.LabelSet, metadata push.LabelsAdapter) {
for lName, lSrc := range l.labelsConfig {
for _, kv := range metadata {
if kv.Name != lSrc {
continue
}

labelValue := model.LabelValue(kv.Value)
if !labelValue.IsValid() {
if Debug {
level.Debug(l.logger).Log("msg", "invalid structured metadata label value", "label", lName, "value", labelValue)
}
break
}

labels[model.LabelName(lName)] = labelValue
break
}
}
}

// Cleanup implements Stage.
func (*labelStage) Cleanup() {
// no-op
}
Loading
Loading