Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add the `WithLoggerProviderOptions`, `WithMeterProviderOptions` and `WithTracerProviderOptions` options to `NewSDK` to allow passing custom options to providers in `go.opentelemetry.io/contrib/otelconf`. (#7552)
- Added V2 version of AWS EC2 detector `go.opentelemetry.io/contrib/detectors/aws/ec2/v2` due to deprecation of `github.com/aws/aws-sdk-go`. (#6961)

### Changed

- 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

- `WithSpanOptions` in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` is deprecated.
Expand Down
71 changes: 51 additions & 20 deletions processors/minsev/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,28 @@ 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() {
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 +35,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 +54,42 @@ func ExampleSeveritier() {
// false
// true
}

func ExampleSeveritier_json() {
// Demonstrates how to use a Severitier that reads from a JSON
// configuration.

// 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
}
223 changes: 223 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,169 @@ 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() => "INFO2"
// (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) {
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 +294,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 +313,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
14 changes: 14 additions & 0 deletions processors/minsev/severity_go1.24.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:build go1.24
// +build go1.24

package minsev // import "go.opentelemetry.io/contrib/processors/minsev"

import "encoding"

var (
_ encoding.TextAppender = Severity(0) // Ensure Severity implements encoding.TextAppender.
_ encoding.TextAppender = (*SeverityVar)(nil) // Ensure Severity implements encoding.TextAppender.
)
Loading
Loading