Skip to content

Commit 1fff770

Browse files
committed
feat: added loki streams processing at /loki/push
Signed-off-by: Lukas Schmidt <[email protected]>
1 parent 66c79d4 commit 1fff770

File tree

9 files changed

+2099
-303
lines changed

9 files changed

+2099
-303
lines changed

README.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Coverage Status](https://coveralls.io/repos/github/blind-oracle/cortex-tenant/badge.svg?branch=main)](https://coveralls.io/github/blind-oracle/cortex-tenant?branch=main)
55
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/cortex-tenant)](https://artifacthub.io/packages/helm/cortex-tenant/cortex-tenant)
66

7-
Prometheus remote write proxy which marks timeseries with a Cortex/Mimir tenant ID based on labels.
7+
Prometheus/Loki remote write proxy which marks timeseries with a Cortex/Mimir/Loki tenant ID based on labels.
88

99
## Status
1010

@@ -16,19 +16,20 @@ The project is stable and (hopefully) almost bug-free. I will accept PRs, but pr
1616

1717
## Overview
1818

19-
Cortex/Mimir tenants (separate namespaces where metrics are stored to and queried from) are identified by `X-Scope-OrgID` HTTP header on both writes and queries.
19+
Cortex/Mimir/Loki tenants (separate namespaces where metrics are stored to and queried from) are identified by `X-Scope-OrgID` HTTP header on both writes and queries.
2020

2121
~~Problem is that Prometheus can't be configured to send this header~~ Actually in some recent version (year 2021 onwards) this functionality was added, but the tenant is the same for all jobs. This makes it impossible to use a single Prometheus (or an HA pair) to write to multiple tenants.
2222

2323
This software solves the problem using the following logic:
2424

25-
- Receive Prometheus remote write
26-
- Search each timeseries for a specific label name and extract a tenant ID from its value.
25+
- Receive Prometheus remote write or [Loki push](https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs)
26+
- Search each timeseries/stream for a specific label name and extract a tenant ID from its value.
27+
For Loki the stream labels are preferred over [structured metadata](https://grafana.com/docs/loki/latest/get-started/labels/structured-metadata/) labels.
2728
If the label wasn't found then it can fall back to a configurable default ID.
28-
If none is configured then the write request will be rejected with HTTP code 400
29-
- Optionally removes this label from the timeseries
30-
- Groups timeseries by tenant
31-
- Issues a number of parallel per-tenant HTTP requests to Cortex/Mimir with the relevant tenant HTTP header (`X-Scope-OrgID` by default)
29+
If none is configured then the write/push request will be rejected with HTTP code 400
30+
- Optionally removes this label from the timeseries/stream
31+
- Groups timeseries/streams by tenant
32+
- Issues a number of parallel per-tenant HTTP requests to Cortex/Mimir/Loki with the relevant tenant HTTP header (`X-Scope-OrgID` by default)
3233

3334
## Usage
3435

@@ -38,6 +39,7 @@ This software solves the problem using the following logic:
3839

3940
- GET `/alive` returns 200 by default and 503 if the service is shutting down (if `timeout_shutdown` setting is > 0)
4041
- POST `/push` receives metrics from Prometheus - configure remote write to send here
42+
- POST `/loki/push` receives logs from Loki - configure push to send here
4143

4244
### Configuration
4345

@@ -59,6 +61,10 @@ listen_pprof: 0.0.0.0:7008
5961
# env: CT_TARGET
6062
target: http://127.0.0.1:9091/receive
6163

64+
# Where to send the modified requests (Loki)
65+
# env: CT_TARGET_LOKI
66+
target_loki: http://127.0.0.1:3100/loki/api/v1/push
67+
6268
# Whether to enable querying for IPv6 records
6369
# env: CT_ENABLE_IPV6
6470
enable_ipv6: false
@@ -137,10 +143,11 @@ tenant:
137143
- tenant
138144
- other_tenant
139145

140-
# Whether to remove the tenant label from the request
146+
# Whether to remove the tenant label from the request.
147+
# Has no effect for Loki stream messages, they are kept as is.
141148
# env: CT_TENANT_LABEL_REMOVE
142149
label_remove: true
143-
150+
144151
# To which header to add the tenant ID
145152
# env: CT_TENANT_HEADER
146153
header: X-Scope-OrgID

config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"os"
6+
"slices"
67
"time"
78

89
"github.com/caarlos0/env/v8"
@@ -18,6 +19,7 @@ type config struct {
1819
MetricsIncludeTenant bool `yaml:"metrics_include_tenant" env:"CT_METRICS_INCLUDE_TENANT"`
1920

2021
Target string `env:"CT_TARGET"`
22+
TargetLoki string `yaml:"target_loki" env:"CT_TARGET_LOKI"`
2123
EnableIPv6 bool `yaml:"enable_ipv6" env:"CT_ENABLE_IPV6"`
2224

2325
LogLevel string `yaml:"log_level" env:"CT_LOG_LEVEL"`
@@ -86,6 +88,10 @@ func configLoad(file string) (*config, error) {
8688
cfg.Target = "127.0.0.1:9090"
8789
}
8890

91+
if cfg.TargetLoki == "" {
92+
cfg.TargetLoki = "127.0.0.1:3100"
93+
}
94+
8995
if cfg.Timeout == 0 {
9096
cfg.Timeout = 10 * time.Second
9197
}
@@ -109,6 +115,9 @@ func configLoad(file string) (*config, error) {
109115
// Default to the Label if list is empty
110116
if len(cfg.Tenant.LabelList) == 0 {
111117
cfg.Tenant.LabelList = append(cfg.Tenant.LabelList, cfg.Tenant.Label)
118+
} else {
119+
// Reverse entries to always prefer last label in list when found
120+
slices.Reverse(cfg.Tenant.LabelList)
112121
}
113122

114123
if cfg.Auth.Egress.Username != "" {

go.mod

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,137 @@
11
module github.com/blind-oracle/cortex-tenant
22

3-
go 1.23
3+
go 1.25.0
44

55
require (
66
github.com/blind-oracle/go-common v1.0.7
77
github.com/caarlos0/env/v8 v8.0.0
88
github.com/gogo/protobuf v1.3.2
9-
github.com/golang/snappy v0.0.4
9+
github.com/golang/snappy v1.0.0
1010
github.com/google/uuid v1.6.0
11+
github.com/grafana/loki/pkg/push v0.0.0-20240924133635-758364c7775f
12+
github.com/grafana/loki/v3 v3.5.4
1113
github.com/hashicorp/go-multierror v1.1.1
1214
github.com/pkg/errors v0.9.1
13-
github.com/prometheus/client_golang v1.20.5
14-
github.com/prometheus/prometheus v0.300.1
15+
github.com/prometheus/client_golang v1.21.1
16+
github.com/prometheus/prometheus v0.302.1
1517
github.com/sirupsen/logrus v1.9.3
1618
github.com/stretchr/testify v1.10.0
1719
github.com/valyala/fasthttp v1.58.0
1820
gopkg.in/yaml.v2 v2.4.0
1921
)
2022

2123
require (
24+
dario.cat/mergo v1.0.1 // indirect
25+
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
26+
github.com/Masterminds/goutils v1.1.1 // indirect
27+
github.com/Masterminds/semver/v3 v3.3.1 // indirect
28+
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
29+
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
2230
github.com/andybalholm/brotli v1.1.1 // indirect
31+
github.com/armon/go-metrics v0.4.1 // indirect
2332
github.com/beorn7/perks v1.0.1 // indirect
33+
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 // indirect
2434
github.com/cespare/xxhash/v2 v2.3.0 // indirect
35+
github.com/coreos/go-semver v0.3.1 // indirect
36+
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
2537
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
38+
github.com/dennwc/varint v1.0.0 // indirect
39+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
40+
github.com/dustin/go-humanize v1.0.1 // indirect
41+
github.com/edsrzf/mmap-go v1.2.0 // indirect
42+
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
43+
github.com/fatih/color v1.18.0 // indirect
44+
github.com/felixge/httpsnoop v1.0.4 // indirect
45+
github.com/fsnotify/fsnotify v1.8.0 // indirect
46+
github.com/go-kit/log v0.2.1 // indirect
47+
github.com/go-logfmt/logfmt v0.6.0 // indirect
48+
github.com/go-logr/logr v1.4.2 // indirect
49+
github.com/go-logr/stdr v1.2.2 // indirect
50+
github.com/go-redsync/redsync/v4 v4.13.0 // indirect
51+
github.com/gogo/googleapis v1.4.1 // indirect
52+
github.com/gogo/status v1.1.1 // indirect
53+
github.com/golang/protobuf v1.5.4 // indirect
54+
github.com/google/btree v1.1.3 // indirect
55+
github.com/gorilla/mux v1.8.1 // indirect
56+
github.com/grafana/dskit v0.0.0-20250317084829-9cdd36a91f10 // indirect
57+
github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1 // indirect
58+
github.com/grafana/jsonparser v0.0.0-20241004153430-023329977675 // indirect
59+
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
2660
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
61+
github.com/hashicorp/consul/api v1.31.2 // indirect
2762
github.com/hashicorp/errwrap v1.1.0 // indirect
28-
github.com/klauspost/compress v1.17.11 // indirect
29-
github.com/kr/text v0.2.0 // indirect
30-
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
63+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
64+
github.com/hashicorp/go-hclog v1.6.3 // indirect
65+
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
66+
github.com/hashicorp/go-metrics v0.5.4 // indirect
67+
github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect
68+
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
69+
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
70+
github.com/hashicorp/golang-lru v1.0.2 // indirect
71+
github.com/hashicorp/memberlist v0.5.3 // indirect
72+
github.com/hashicorp/serf v0.10.1 // indirect
73+
github.com/huandu/xstrings v1.5.0 // indirect
74+
github.com/jpillora/backoff v1.0.0 // indirect
75+
github.com/json-iterator/go v1.1.12 // indirect
76+
github.com/klauspost/compress v1.18.0 // indirect
77+
github.com/mattn/go-colorable v0.1.14 // indirect
78+
github.com/mattn/go-isatty v0.0.20 // indirect
79+
github.com/mdlayher/socket v0.5.1 // indirect
80+
github.com/mdlayher/vsock v1.2.1 // indirect
81+
github.com/miekg/dns v1.1.63 // indirect
82+
github.com/mitchellh/copystructure v1.2.0 // indirect
83+
github.com/mitchellh/go-homedir v1.1.0 // indirect
84+
github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect
85+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
86+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
87+
github.com/modern-go/reflect2 v1.0.2 // indirect
3188
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
89+
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
90+
github.com/opentracing-contrib/go-grpc v0.1.1 // indirect
91+
github.com/opentracing-contrib/go-stdlib v1.1.0 // indirect
92+
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
93+
github.com/pires/go-proxyproto v0.7.0 // indirect
3294
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3395
github.com/prometheus/client_model v0.6.1 // indirect
34-
github.com/prometheus/common v0.61.0 // indirect
96+
github.com/prometheus/common v0.62.0 // indirect
97+
github.com/prometheus/exporter-toolkit v0.13.2 // indirect
3598
github.com/prometheus/procfs v0.15.1 // indirect
99+
github.com/redis/go-redis/v9 v9.7.3 // indirect
100+
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
101+
github.com/sercand/kuberesolver/v5 v5.1.1 // indirect
102+
github.com/shopspring/decimal v1.4.0 // indirect
103+
github.com/sony/gobreaker/v2 v2.1.0 // indirect
104+
github.com/spf13/cast v1.7.1 // indirect
105+
github.com/stretchr/objx v0.5.2 // indirect
106+
github.com/tjhop/slog-gokit v0.1.4 // indirect
107+
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
108+
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
36109
github.com/valyala/bytebufferpool v1.0.0 // indirect
37-
golang.org/x/sys v0.28.0 // indirect
38-
golang.org/x/text v0.21.0 // indirect
39-
google.golang.org/protobuf v1.36.0 // indirect
110+
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
111+
go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
112+
go.etcd.io/etcd/client/v3 v3.5.4 // indirect
113+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
114+
go.opentelemetry.io/collector/pdata v1.28.1 // indirect
115+
go.opentelemetry.io/otel v1.35.0 // indirect
116+
go.opentelemetry.io/otel/metric v1.35.0 // indirect
117+
go.opentelemetry.io/otel/trace v1.35.0 // indirect
118+
go.uber.org/atomic v1.11.0 // indirect
119+
go.uber.org/multierr v1.11.0 // indirect
120+
go.uber.org/zap v1.27.0 // indirect
121+
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect
122+
golang.org/x/crypto v0.36.0 // indirect
123+
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
124+
golang.org/x/mod v0.22.0 // indirect
125+
golang.org/x/net v0.38.0 // indirect
126+
golang.org/x/oauth2 v0.28.0 // indirect
127+
golang.org/x/sync v0.12.0 // indirect
128+
golang.org/x/sys v0.31.0 // indirect
129+
golang.org/x/text v0.23.0 // indirect
130+
golang.org/x/time v0.11.0 // indirect
131+
golang.org/x/tools v0.29.0 // indirect
132+
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
133+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
134+
google.golang.org/grpc v1.71.0 // indirect
135+
google.golang.org/protobuf v1.36.6 // indirect
40136
gopkg.in/yaml.v3 v3.0.1 // indirect
41137
)

0 commit comments

Comments
 (0)