Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add dslog and tenant packages #33

Closed
wants to merge 12 commits into from
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

* [CHANGE] Removed global metrics for KV package. Making a KV object will now require a prometheus registerer that will
be used to register all relevant KV class metrics. #22
* [CHANGE] Added CHANGELOG.md and Pull Request template to reference the changelog
* [CHANGE] Added CHANGELOG.md and Pull Request template to reference the changelog
* [ENHANCEMENT] Add `dslog` package #
Copy link
Contributor

Choose a reason for hiding this comment

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

I kind of like the look of this, do you think we should do this on all of these changes going forward? We could also add a new tag like [Addition] or something if we'd like.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@treid314 I'm actually not 100% sure, I just thought it was the right thing to do since @pracucci asked us to keep the changelog up to date.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've mostly been using the changelog to track any constructor or method input changes, there just haven't really been any for a bit. I think it's great to track adding modules as well to show us whats in here.

* [ENHANCEMENT] Add `tenant` package #
95 changes: 95 additions & 0 deletions dslog/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package dslog

import (
"fmt"
"os"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/weaveworks/common/logging"
)

// PrometheusLogger exposes Prometheus counters for each of go-kit's log levels.
type PrometheusLogger struct {
logger log.Logger
logMessages *prometheus.CounterVec
experimentalFeaturesInUse prometheus.Counter
}

// NewPrometheusLogger creates a new instance of PrometheusLogger which exposes
// Prometheus counters for various log levels.
func NewPrometheusLogger(l logging.Level, format logging.Format, reg prometheus.Registerer, metricsNamespace string) log.Logger {
logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr))
if format.String() == "json" {
logger = log.NewJSONLogger(log.NewSyncWriter(os.Stderr))
}
logger = level.NewFilter(logger, l.Gokit)

logMessages := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
Name: "log_messages_total",
Help: "Total number of log messages.",
}, []string{"level"})
// Initialise counters for all supported levels:
supportedLevels := []level.Value{
level.DebugValue(),
level.InfoValue(),
level.WarnValue(),
level.ErrorValue(),
}
for _, level := range supportedLevels {
logMessages.WithLabelValues(level.String())
}

logger = &PrometheusLogger{
logger: logger,
logMessages: logMessages,
experimentalFeaturesInUse: promauto.With(reg).NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Name: "experimental_features_in_use_total",
Help: "The number of experimental features in use.",
},
),
}

// return a Logger without caller information, shouldn't use directly
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
return logger
}

// Log increments the appropriate Prometheus counter depending on the log level.
func (pl *PrometheusLogger) Log(kv ...interface{}) error {
pl.logger.Log(kv...)
l := "unknown"
for i := 1; i < len(kv); i += 2 {
if v, ok := kv[i].(level.Value); ok {
l = v.String()
break
}
}
pl.logMessages.WithLabelValues(l).Inc()
return nil
}

// CheckFatal prints an error and exits with error code 1 if err is non-nil.
func CheckFatal(location string, err error, logger log.Logger) {
if err != nil {
logger := level.Error(logger)
if location != "" {
logger = log.With(logger, "msg", "error "+location)
}
// %+v gets the stack trace from errors using github.com/pkg/errors
logger.Log("err", fmt.Sprintf("%+v", err))
os.Exit(1)
}
}

// WarnExperimentalUse logs a warning and increments the experimental features metric.
func WarnExperimentalUse(feature string, logger log.Logger) {
level.Warn(logger).Log("msg", "experimental feature in use", "feature", feature)
if pl, ok := logger.(*PrometheusLogger); ok {
pl.experimentalFeaturesInUse.Inc()
}
}
52 changes: 52 additions & 0 deletions dslog/wrappers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dslog

import (
"context"

"github.com/go-kit/kit/log"
"github.com/weaveworks/common/tracing"

"github.com/grafana/dskit/tenant"
)

// WithUserID returns a Logger that has information about the current user in
// its details.
func WithUserID(userID string, l log.Logger) log.Logger {
// See note in WithContext.
return log.With(l, "org_id", userID)
}

// WithTraceID returns a Logger that has information about the traceID in
// its details.
func WithTraceID(traceID string, l log.Logger) log.Logger {
// See note in WithContext.
return log.With(l, "traceID", traceID)
}

// WithContext returns a Logger that has information about the current user in
// its details.
//
// e.g.
// log := util.WithContext(ctx)
// log.Errorf("Could not chunk chunks: %v", err)
func WithContext(ctx context.Context, l log.Logger) log.Logger {
// Weaveworks uses "orgs" and "orgID" to represent Cortex users,
// even though the code-base generally uses `userID` to refer to the same thing.
userID, err := tenant.ID(ctx)
if err == nil {
l = WithUserID(userID, l)
}

traceID, ok := tracing.ExtractSampledTraceID(ctx)
if !ok {
return l
}

return WithTraceID(traceID, l)
}

// WithSourceIPs returns a Logger that has information about the source IPs in
// its details.
func WithSourceIPs(sourceIPs string, l log.Logger) log.Logger {
return log.With(l, "sourceIPs", sourceIPs)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/prometheus/common v0.26.0
github.com/sercand/kuberesolver v2.4.0+incompatible // indirect
github.com/stretchr/testify v1.7.0
github.com/weaveworks/common v0.0.0-20210722103813-e649eff5ab4a
github.com/weaveworks/common v0.0.0-20210901124008-1fa3f9fa874c
go.etcd.io/etcd v3.3.25+incompatible
go.etcd.io/etcd/client/v3 v3.5.0
go.etcd.io/etcd/server/v3 v3.5.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,8 @@ github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/
github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/weaveworks/common v0.0.0-20210722103813-e649eff5ab4a h1:ALomSnvy/NPeVoc4a1o7keaHHgLS76r9ZYIlwWWF+KA=
github.com/weaveworks/common v0.0.0-20210722103813-e649eff5ab4a/go.mod h1:YU9FvnS7kUnRt6HY10G+2qHkwzP3n3Vb1XsXDsJTSp8=
github.com/weaveworks/common v0.0.0-20210901124008-1fa3f9fa874c h1:+yzwVr4/12cUgsdjbEHq6MsKB7jWBZpZccAP6xvqTzQ=
github.com/weaveworks/common v0.0.0-20210901124008-1fa3f9fa874c/go.mod h1:YU9FvnS7kUnRt6HY10G+2qHkwzP3n3Vb1XsXDsJTSp8=
github.com/weaveworks/promrus v1.2.0 h1:jOLf6pe6/vss4qGHjXmGz4oDJQA+AOCqEL3FvvZGz7M=
github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
Expand Down
152 changes: 152 additions & 0 deletions tenant/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package tenant

import (
"context"
"errors"
"net/http"
"strings"

"github.com/weaveworks/common/user"
)

var defaultResolver Resolver = NewSingleResolver()

// WithDefaultResolver updates the resolver used for the package methods.
func WithDefaultResolver(r Resolver) {
defaultResolver = r
}

// ID returns exactly a single tenant ID from the context. It should be
// used when a certain endpoint should only support exactly a single
// tenant ID. It returns an error user.ErrNoOrgID if there is no tenant ID
// supplied or user.ErrTooManyOrgIDs if there are multiple tenant IDs present.
func ID(ctx context.Context) (string, error) {
return defaultResolver.TenantID(ctx)
}

// IDs returns all tenant IDs from the context. It should return
// normalized list of ordered and distinct tenant IDs (as produced by
// NormalizeTenantIDs).
func IDs(ctx context.Context) ([]string, error) {
return defaultResolver.TenantIDs(ctx)
}

type Resolver interface {
// TenantID returns exactly a single tenant ID from the context. It should be
// used when a certain endpoint should only support exactly a single
// tenant ID. It returns an error user.ErrNoOrgID if there is no tenant ID
// supplied or user.ErrTooManyOrgIDs if there are multiple tenant IDs present.
TenantID(context.Context) (string, error)

// TenantIDs returns all tenant IDs from the context. It should return
// normalized list of ordered and distinct tenant IDs (as produced by
// NormalizeTenantIDs).
TenantIDs(context.Context) ([]string, error)
}

// NewSingleResolver creates a tenant resolver, which restricts all requests to
// be using a single tenant only. This allows a wider set of characters to be
// used within the tenant ID and should not impose a breaking change.
func NewSingleResolver() *SingleResolver {
return &SingleResolver{}
}

type SingleResolver struct {
}

// containsUnsafePathSegments will return true if the string is a directory
// reference like `.` and `..` or if any path separator character like `/` and
// `\` can be found.
func containsUnsafePathSegments(id string) bool {
// handle the relative reference to current and parent path.
if id == "." || id == ".." {
return true
}

return strings.ContainsAny(id, "\\/")
}

var errInvalidTenantID = errors.New("invalid tenant ID")

func (t *SingleResolver) TenantID(ctx context.Context) (string, error) {
//lint:ignore faillint wrapper around upstream method
id, err := user.ExtractOrgID(ctx)
if err != nil {
return "", err
}

if containsUnsafePathSegments(id) {
return "", errInvalidTenantID
}

return id, nil
}

func (t *SingleResolver) TenantIDs(ctx context.Context) ([]string, error) {
orgID, err := t.TenantID(ctx)
if err != nil {
return nil, err
}
return []string{orgID}, err
}

type MultiResolver struct {
}

// NewMultiResolver creates a tenant resolver, which allows request to have
// multiple tenant ids submitted separated by a '|' character. This enforces
// further limits on the character set allowed within tenants as detailed here:
// https://cortexmetrics.io/docs/guides/limitations/#tenant-id-naming)
func NewMultiResolver() *MultiResolver {
return &MultiResolver{}
}

func (t *MultiResolver) TenantID(ctx context.Context) (string, error) {
orgIDs, err := t.TenantIDs(ctx)
if err != nil {
return "", err
}

if len(orgIDs) > 1 {
return "", user.ErrTooManyOrgIDs
}

return orgIDs[0], nil
}

func (t *MultiResolver) TenantIDs(ctx context.Context) ([]string, error) {
//lint:ignore faillint wrapper around upstream method
orgID, err := user.ExtractOrgID(ctx)
if err != nil {
return nil, err
}

orgIDs := strings.Split(orgID, tenantIDsLabelSeparator)
for _, orgID := range orgIDs {
if err := ValidTenantID(orgID); err != nil {
return nil, err
}
if containsUnsafePathSegments(orgID) {
return nil, errInvalidTenantID
}
}

return NormalizeTenantIDs(orgIDs), nil
}

// ExtractTenantIDFromHTTPRequest extracts a single TenantID through a given
// resolver directly from a HTTP request.
func ExtractTenantIDFromHTTPRequest(req *http.Request) (string, context.Context, error) {
//lint:ignore faillint wrapper around upstream method
_, ctx, err := user.ExtractOrgIDFromHTTPRequest(req)
if err != nil {
return "", nil, err
}

tenantID, err := defaultResolver.TenantID(ctx)
if err != nil {
return "", nil, err
}

return tenantID, ctx, nil
}
Loading