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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- `go.opentelemetry.io/contrib/bridges/otellogrus`
- `go.opentelemetry.io/contrib/bridges/otelslog`
- `go.opentelemetry.io/contrib/bridges/otelzap`
- The `Severity` type from `go.opentelemetry.io/contrib/processors/minsev` now implements the `fmt.Stringer`, `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, `encoding.TextAppender`, `json.Marshaler`, and `json.Unmarshaler` interfaces. (#7652)
- The `SeverityVar` type from `go.opentelemetry.io/contrib/processors/minsev` now implements the `fmt.Stringer`, `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, and `encoding.TextAppender` interfaces. (#7652)

### Deprecated

Expand Down
72 changes: 52 additions & 20 deletions processors/minsev/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,30 @@ package minsev_test

import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"

"go.opentelemetry.io/otel/log"
logsdk "go.opentelemetry.io/otel/sdk/log"

"go.opentelemetry.io/contrib/processors/minsev"
)

const key = "OTEL_LOG_LEVEL"

var getSeverity = sync.OnceValue(func() log.Severity {
conv := map[string]log.Severity{
"": log.SeverityInfo, // Default to SeverityInfo for unset.
"debug": log.SeverityDebug,
"info": log.SeverityInfo,
"warn": log.SeverityWarn,
"error": log.SeverityError,
}
// log.SeverityUndefined for unknown values.
return conv[strings.ToLower(os.Getenv(key))]
})

type EnvSeverity struct{}
type EnvSeverity struct {
Var string
}

func (EnvSeverity) Severity() log.Severity { return getSeverity() }
func (s EnvSeverity) Severity() log.Severity {
var sev minsev.Severity
_ = sev.UnmarshalText([]byte(os.Getenv(s.Var)))
return sev.Severity() // Default to SeverityInfo if not set or error.
}

func ExampleSeveritier() {
// This example demonstrates how to use a Severitier that reads from
// an environment variable.
func ExampleSeveritier_environment() {
const key = "LOG_LEVEL"
// Mock an environmental variable setup that would be done externally.
_ = os.Setenv(key, "error")

Expand All @@ -43,7 +37,7 @@ func ExampleSeveritier() {

// Wrap the processor so that it filters by severity level defined
// via environmental variable.
processor = minsev.NewLogProcessor(processor, EnvSeverity{})
processor = minsev.NewLogProcessor(processor, EnvSeverity{key})
lp := logsdk.NewLoggerProvider(
logsdk.WithProcessor(processor),
)
Expand All @@ -62,3 +56,41 @@ func ExampleSeveritier() {
// false
// true
}

// This example demonstrates how to use a Severitier that reads from a JSON
// configuration.
func ExampleSeveritier_json() {
// Example JSON configuration that specifies the minimum severity level.
// This would be provided by the application user.
const jsonConfig = `{"log_level":"error"}`

var config struct {
Severity minsev.Severity `json:"log_level"`
}
if err := json.Unmarshal([]byte(jsonConfig), &config); err != nil {
panic(err)
}

// Existing processor that emits telemetry.
var processor logsdk.Processor = logsdk.NewBatchProcessor(nil)

// Wrap the processor so that it filters by severity level defined
// in the JSON configuration. Note that the severity level itself is a
// Severitier implementation.
processor = minsev.NewLogProcessor(processor, config.Severity)
lp := logsdk.NewLoggerProvider(logsdk.WithProcessor(processor))

// Show that Logs API respects the minimum severity level processor.
l := lp.Logger("ExampleSeveritier")

ctx := context.Background()
params := log.EnabledParameters{Severity: log.SeverityDebug}
fmt.Println(l.Enabled(ctx, params))

params.Severity = log.SeverityError
fmt.Println(l.Enabled(ctx, params))

// Output:
// false
// true
}
239 changes: 239 additions & 0 deletions processors/minsev/severity.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
package minsev // import "go.opentelemetry.io/contrib/processors/minsev"

import (
"encoding"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"sync/atomic"

"go.opentelemetry.io/otel/log"
Expand All @@ -15,6 +21,19 @@ import (
// as errors and critical events).
type Severity int

var (
// Ensure Severity implements fmt.Stringer.
_ fmt.Stringer = Severity(0)
// Ensure Severity implements json.Marshaler.
_ json.Marshaler = Severity(0)
// Ensure Severity implements json.Unmarshaler.
_ json.Unmarshaler = (*Severity)(nil)
// Ensure Severity implements encoding.TextMarshaler.
_ encoding.TextMarshaler = Severity(0)
// Ensure Severity implements encoding.TextUnmarshaler.
_ encoding.TextUnmarshaler = (*Severity)(nil)
)

// Severity values defined by OpenTelemetry.
const (
// A fine-grained debugging log record. Typically disabled in default
Expand Down Expand Up @@ -69,6 +88,10 @@ const (
// It implements [Severitier].
func (s Severity) Severity() log.Severity {
// Unknown defaults to log.SeverityUndefined.
//
// TODO: return a clamped log.Severity. If s is less than
// SeverityTrace1, return log.SeverityTrace1, if s is greater than
// SeverityFatal4, return log.SeverityFatal4.
return translations[s]
}

Expand Down Expand Up @@ -99,6 +122,185 @@ var translations = map[Severity]log.Severity{
SeverityFatal4: log.SeverityFatal4,
}

// String returns a name for the severity level. If the severity level has a
// name, then that name in uppercase is returned. If the severity level is
// outside named values, then an signed integer is appended to the uppercased
// name.
//
// Examples:
//
// SeverityWarn1.String() => "WARN"
// (SeverityInfo1+2).String() => "INFO3"
// (SeverityFatal4+2).String() => "FATAL+6"
// (SeverityTrace1-3).String() => "TRACE-3"
func (s Severity) String() string {
str := func(base string, val Severity) string {
switch val {
case 0:
return base
case 1, 2, 3:
// No sign for known fine-grained severity values.
return fmt.Sprintf("%s%d", base, val+1)
}

if val > 0 {
// Exclude zero from positive scale count.
val++
}
return fmt.Sprintf("%s%+d", base, val)
}

switch {
case s < SeverityDebug1:
return str("TRACE", s-SeverityTrace1)
case s < SeverityInfo1:
return str("DEBUG", s-SeverityDebug1)
case s < SeverityWarn1:
return str("INFO", s-SeverityInfo1)
case s < SeverityError1:
return str("WARN", s-SeverityWarn1)
case s < SeverityFatal1:
return str("ERROR", s-SeverityError1)
default:
return str("FATAL", s-SeverityFatal1)
}
}

// MarshalJSON implements [encoding/json.Marshaler] by quoting the output of
// [Severity.String].
func (s Severity) MarshalJSON() ([]byte, error) {
// AppendQuote is sufficient for JSON-encoding all Severity strings. They
// don't contain any runes that would produce invalid JSON when escaped.
return strconv.AppendQuote(nil, s.String()), nil
}

// UnmarshalJSON implements [encoding/json.Unmarshaler] It accepts any string
// produced by [Severity.MarshalJSON], ignoring case. It also accepts numeric
// offsets that would result in a different string on output. For example,
// "ERROR-8" will unmarshal as [SeverityInfo].
func (s *Severity) UnmarshalJSON(data []byte) error {
str, err := strconv.Unquote(string(data))
if err != nil {
return err
}
return s.parse(str)
}

// AppendText implements [encoding.TextAppender] by calling [Severity.String].
func (s Severity) AppendText(b []byte) ([]byte, error) {
return append(b, s.String()...), nil
}

// MarshalText implements [encoding.TextMarshaler] by calling
// [Severity.AppendText].
func (s Severity) MarshalText() ([]byte, error) {
return s.AppendText(nil)
}

// UnmarshalText implements [encoding.TextUnmarshaler]. It accepts any string
// produced by [Severity.MarshalText], ignoring case. It also accepts numeric
// offsets that would result in a different string on output. For example,
// "ERROR-8" will marshal as [SeverityInfo].
func (s *Severity) UnmarshalText(data []byte) error {
return s.parse(string(data))
}

// parse parses str into s.
//
// It will return an error if str is not a valid severity string.
//
// The string is expected to be in the format of "NAME[N][+/-OFFSET]", where
// NAME is one of the severity names ("TRACE", "DEBUG", "INFO", "WARN",
// "ERROR", "FATAL"), OFFSET is an optional signed integer offset, and N is an
// optional fine-grained severity level that modifies the base severity name.
//
// Name is parsed in a case-insensitive way. Meaning, "info", "Info",
// "iNfO", etc. are all equivalent to "INFO".
//
// Fine-grained severity levels are expected to be in the range of 1 to 4,
// where 1 is the base severity level, and 2, 3, and 4 are more fine-grained
// levels. However, fine-grained levels greater than 4 are also accepted, and
// they will be treated as an 1-based offset from the base severity level.
//
// For example, "ERROR3" will be parsed as "ERROR" with a fine-grained level of
// 3, which corresponds to [SeverityError3], "FATAL+2" will be parsed as
// "FATAL" with an offset of +2, which corresponds to [SeverityFatal2], and
// "INFO2+1" is parsed as INFO with a fine-grained level of 2 and an offset of
// +1, which corresponds to [SeverityInfo3].
//
// Fine-grained severity levels are based on counting numbers excluding zero.
// If a fine-grained level of 0 is provided it is treaded as equivalent to the
// base severity level. For example, "INFO0" is equivalent to [SeverityInfo1].
func (s *Severity) parse(str string) (err error) {
if str == "" {
// Handle empty str as a special case and parse it as the default
// SeverityInfo1.
//
// Do not parse this below in the switch statement of the name. That
// will allow strings like "2", "-1", "2+1", "+3", etc. to be accepted
// and that adds ambiguity. For example, a user may expect that "2" is
// parsed as SeverityInfo2 based on an implied "SeverityInfo1" prefix,
// but they may also expect it be parsed as SeverityInfo3 which has a
// numeric value of 2. Avoid this ambiguity by treating those inputs
// as invalid, and only accept the empty string as a special case.

*s = SeverityInfo1 // Default severity.
return nil
}

defer func() {
if err != nil {
err = fmt.Errorf("minsev: severity string %q: %w", str, err)
}
}()

name := str
offset := 0

// Parse +/- offset suffix, if present.
if i := strings.IndexAny(str, "+-"); i >= 0 {
name = str[:i]
offset, err = strconv.Atoi(str[i:])
if err != nil {
return err
}
}

// Parse fine-grained severity level suffix, if present.
// This supports formats like "ERROR3", "FATAL4", etc.
i := len(name)
n, multi := 0, 1
for ; i > 0 && str[i-1] >= '0' && str[i-1] <= '9'; i-- {
n += int(str[i-1]-'0') * multi
multi *= 10
}
if i < len(name) {
name = name[:i]
if n != 0 {
offset += n - 1 // Convert 1-based to 0-based.
}
}

switch strings.ToUpper(name) {
case "TRACE":
*s = SeverityTrace1
case "DEBUG":
*s = SeverityDebug1
case "INFO":
*s = SeverityInfo1
case "WARN":
*s = SeverityWarn1
case "ERROR":
*s = SeverityError1
case "FATAL":
*s = SeverityFatal1
default:
return errors.New("unknown name")
}
*s += Severity(offset)
return nil
}

// A SeverityVar is a [Severity] variable, to allow a [LogProcessor] severity
// to change dynamically. It implements [Severitier] as well as a Set method,
// and it is safe for use by multiple goroutines.
Expand All @@ -108,6 +310,15 @@ type SeverityVar struct {
val atomic.Int64
}

var (
// Ensure Severity implements fmt.Stringer.
_ fmt.Stringer = (*SeverityVar)(nil)
// Ensure Severity implements encoding.TextMarshaler.
_ encoding.TextMarshaler = (*SeverityVar)(nil)
// Ensure Severity implements encoding.TextUnmarshaler.
_ encoding.TextUnmarshaler = (*SeverityVar)(nil)
)

// Severity returns v's severity.
func (v *SeverityVar) Severity() log.Severity {
return Severity(int(v.val.Load())).Severity()
Expand All @@ -118,6 +329,34 @@ func (v *SeverityVar) Set(l Severity) {
v.val.Store(int64(l))
}

// String returns a string representation of the SeverityVar.
func (v *SeverityVar) String() string {
return fmt.Sprintf("SeverityVar(%s)", Severity(int(v.val.Load())).String())
}

// AppendText implements [encoding.TextAppender]
// by calling [Severity.AppendText].
func (v *SeverityVar) AppendText(b []byte) ([]byte, error) {
return Severity(int(v.val.Load())).AppendText(b)
}

// MarshalText implements [encoding.TextMarshaler]
// by calling [SeverityVar.AppendText].
func (v *SeverityVar) MarshalText() ([]byte, error) {
return v.AppendText(nil)
}

// UnmarshalText implements [encoding.TextUnmarshaler]
// by calling [Severity.UnmarshalText].
func (v *SeverityVar) UnmarshalText(data []byte) error {
var s Severity
if err := s.UnmarshalText(data); err != nil {
return err
}
v.Set(s)
return nil
}

// A Severitier provides a [log.Severity] value.
type Severitier interface {
Severity() log.Severity
Expand Down
Loading
Loading