Skip to content

Commit eba4ca0

Browse files
committed
Support text interchange with minsev Severity
1 parent 8c7ab53 commit eba4ca0

File tree

4 files changed

+617
-20
lines changed

4 files changed

+617
-20
lines changed

processors/minsev/example_test.go

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,28 @@ package minsev_test
55

66
import (
77
"context"
8+
"encoding/json"
89
"fmt"
910
"os"
10-
"strings"
11-
"sync"
1211

1312
"go.opentelemetry.io/otel/log"
1413
logsdk "go.opentelemetry.io/otel/sdk/log"
1514

1615
"go.opentelemetry.io/contrib/processors/minsev"
1716
)
1817

19-
const key = "OTEL_LOG_LEVEL"
20-
21-
var getSeverity = sync.OnceValue(func() log.Severity {
22-
conv := map[string]log.Severity{
23-
"": log.SeverityInfo, // Default to SeverityInfo for unset.
24-
"debug": log.SeverityDebug,
25-
"info": log.SeverityInfo,
26-
"warn": log.SeverityWarn,
27-
"error": log.SeverityError,
28-
}
29-
// log.SeverityUndefined for unknown values.
30-
return conv[strings.ToLower(os.Getenv(key))]
31-
})
32-
33-
type EnvSeverity struct{}
18+
type EnvSeverity struct {
19+
Var string
20+
}
3421

35-
func (EnvSeverity) Severity() log.Severity { return getSeverity() }
22+
func (s EnvSeverity) Severity() log.Severity {
23+
var sev minsev.Severity
24+
_ = sev.UnmarshalText([]byte(os.Getenv(s.Var)))
25+
return sev.Severity() // Default to SeverityInfo if not set or error.
26+
}
3627

37-
func ExampleSeveritier() {
28+
func ExampleSeveritier_environment() {
29+
const key = "LOG_LEVEL"
3830
// Mock an environmental variable setup that would be done externally.
3931
_ = os.Setenv(key, "error")
4032

@@ -43,7 +35,7 @@ func ExampleSeveritier() {
4335

4436
// Wrap the processor so that it filters by severity level defined
4537
// via environmental variable.
46-
processor = minsev.NewLogProcessor(processor, EnvSeverity{})
38+
processor = minsev.NewLogProcessor(processor, EnvSeverity{key})
4739
lp := logsdk.NewLoggerProvider(
4840
logsdk.WithProcessor(processor),
4941
)
@@ -62,3 +54,42 @@ func ExampleSeveritier() {
6254
// false
6355
// true
6456
}
57+
58+
func ExampleSeveritier_json() {
59+
// Demonstrates how to use a Severitier that reads from a JSON
60+
// configuration.
61+
62+
// Example JSON configuration that specifies the minimum severity level.
63+
// This would be provided by the application user.
64+
const jsonConfig = `{"log_level":"error"}`
65+
66+
var config struct {
67+
Severity minsev.Severity `json:"log_level"`
68+
}
69+
if err := json.Unmarshal([]byte(jsonConfig), &config); err != nil {
70+
panic(err)
71+
}
72+
73+
// Existing processor that emits telemetry.
74+
var processor logsdk.Processor = logsdk.NewBatchProcessor(nil)
75+
76+
// Wrap the processor so that it filters by severity level defined
77+
// in the JSON configuration. Note that the severity level itself is a
78+
// Severitier implementation.
79+
processor = minsev.NewLogProcessor(processor, config.Severity)
80+
lp := logsdk.NewLoggerProvider(logsdk.WithProcessor(processor))
81+
82+
// Show that Logs API respects the minimum severity level processor.
83+
l := lp.Logger("ExampleSeveritier")
84+
85+
ctx := context.Background()
86+
params := log.EnabledParameters{Severity: log.SeverityDebug}
87+
fmt.Println(l.Enabled(ctx, params))
88+
89+
params.Severity = log.SeverityError
90+
fmt.Println(l.Enabled(ctx, params))
91+
92+
// Output:
93+
// false
94+
// true
95+
}

processors/minsev/severity.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
package minsev // import "go.opentelemetry.io/contrib/processors/minsev"
55

66
import (
7+
"encoding"
8+
"encoding/json"
9+
"errors"
10+
"fmt"
11+
"strconv"
12+
"strings"
713
"sync/atomic"
814

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

24+
var (
25+
// Ensure Severity implements fmt.Stringer.
26+
_ fmt.Stringer = Severity(0)
27+
// Ensure Severity implements json.Marshaler.
28+
_ json.Marshaler = Severity(0)
29+
// Ensure Severity implements json.Unmarshaler.
30+
_ json.Unmarshaler = (*Severity)(nil)
31+
// Ensure Severity implements encoding.TextMarshaler.
32+
_ encoding.TextMarshaler = Severity(0)
33+
// Ensure Severity implements encoding.TextUnmarshaler.
34+
_ encoding.TextUnmarshaler = (*Severity)(nil)
35+
)
36+
1837
// Severity values defined by OpenTelemetry.
1938
const (
2039
// A fine-grained debugging log record. Typically disabled in default
@@ -99,6 +118,171 @@ var translations = map[Severity]log.Severity{
99118
SeverityFatal4: log.SeverityFatal4,
100119
}
101120

121+
// String returns a name for the severity level. If the severity level has a
122+
// name, then that name in uppercase is returned. If the severity level is
123+
// outside named values, then an signed integer is appended to the uppercased
124+
// name.
125+
//
126+
// Examples:
127+
//
128+
// SeverityWarn1.String() => "WARN"
129+
// (SeverityInfo1+2).String() => "INFO2"
130+
// (SeverityFatal4+2).String() => "FATAL+6"
131+
// (SeverityTrace1-3).String() => "TRACE-3"
132+
func (s Severity) String() string {
133+
str := func(base string, val Severity) string {
134+
switch val {
135+
case 0:
136+
return base
137+
case 1, 2, 3:
138+
// No sign for known fine-grained severity values.
139+
return fmt.Sprintf("%s%d", base, val+1)
140+
}
141+
142+
if val > 0 {
143+
// Exclude zero from positive scale count.
144+
val++
145+
}
146+
return fmt.Sprintf("%s%+d", base, val)
147+
}
148+
149+
switch {
150+
case s < SeverityDebug1:
151+
return str("TRACE", s-SeverityTrace1)
152+
case s < SeverityInfo1:
153+
return str("DEBUG", s-SeverityDebug1)
154+
case s < SeverityWarn1:
155+
return str("INFO", s-SeverityInfo1)
156+
case s < SeverityError1:
157+
return str("WARN", s-SeverityWarn1)
158+
case s < SeverityFatal1:
159+
return str("ERROR", s-SeverityError1)
160+
default:
161+
return str("FATAL", s-SeverityFatal1)
162+
}
163+
}
164+
165+
// MarshalJSON implements [encoding/json.Marshaler] by quoting the output of
166+
// [Severity.String].
167+
func (s Severity) MarshalJSON() ([]byte, error) {
168+
// AppendQuote is sufficient for JSON-encoding all Severity strings. They
169+
// don't contain any runes that would produce invalid JSON when escaped.
170+
return strconv.AppendQuote(nil, s.String()), nil
171+
}
172+
173+
// UnmarshalJSON implements [encoding/json.Unmarshaler] It accepts any string
174+
// produced by [Severity.MarshalJSON], ignoring case. It also accepts numeric
175+
// offsets that would result in a different string on output. For example,
176+
// "ERROR-8" will unmarshal as SeverityInfo.
177+
func (s *Severity) UnmarshalJSON(data []byte) error {
178+
str, err := strconv.Unquote(string(data))
179+
if err != nil {
180+
return err
181+
}
182+
return s.parse(str)
183+
}
184+
185+
// AppendText implements [encoding.TextAppender] by calling [Severity.String].
186+
func (s Severity) AppendText(b []byte) ([]byte, error) {
187+
return append(b, s.String()...), nil
188+
}
189+
190+
// MarshalText implements [encoding.TextMarshaler] by calling
191+
// [Severity.AppendText].
192+
func (s Severity) MarshalText() ([]byte, error) {
193+
return s.AppendText(nil)
194+
}
195+
196+
// UnmarshalText implements [encoding.TextUnmarshaler]. It accepts any string
197+
// produced by [Severity.MarshalText], ignoring case. It also accepts numeric
198+
// offsets that would result in a different string on output. For example,
199+
// "ERROR-8" will marshal as SeverityInfo.
200+
func (s *Severity) UnmarshalText(data []byte) error {
201+
return s.parse(string(data))
202+
}
203+
204+
// parse parses str into s.
205+
//
206+
// It will return an error if str is not a valid severity string.
207+
//
208+
// The string is expected to be in the format of "NAME[N][+/-OFFSET]", where
209+
// NAME is one of the severity names ("TRACE", "DEBUG", "INFO", "WARN",
210+
// "ERROR", "FATAL"), OFFSET is an optional signed integer offset, and N is an
211+
// optional fine-grained severity level that modifies the base severity name.
212+
//
213+
// Name is parsed in a case-insensitive way. Meaning, "info", "Info",
214+
// "iNfO", etc. are all equivalent to "INFO".
215+
//
216+
// Fine-grained severity levels are expected to be in the range of 1 to 4,
217+
// where 1 is the base severity level, and 2, 3, and 4 are more fine-grained
218+
// levels. However, fine-grained levels greater than 4 are also accepted, and
219+
// they will be treated as an 1-based offset from the base severity level.
220+
//
221+
// For example, "ERROR3" will be parsed as "ERROR" with a fine-grained level of
222+
// 3, which corresponds to `SeverityError3`, "FATAL+2" will be parsed as
223+
// "FATAL" with an offset of +2, which corresponds to `SeverityFatal2`, and
224+
// "INFO2+1" is parsed as INFO with a fine-grained level of 2 and an offset of
225+
// +1, which corresponds to `SeverityInfo3`.
226+
//
227+
// Fine-grained severity levels are based on counting numbres excluding zero.
228+
// If a fine-grained level of 0 is provided it is treaded as equivalent to the
229+
// base severity level. For example, "INFO0" is equivalent to `SeverityInfo1`.
230+
func (s *Severity) parse(str string) (err error) {
231+
defer func() {
232+
if err != nil {
233+
err = fmt.Errorf("minsev: severity string %q: %w", str, err)
234+
}
235+
}()
236+
237+
name := str
238+
offset := 0
239+
240+
// Parse +/- offset suffix, if present.
241+
if i := strings.IndexAny(str, "+-"); i >= 0 {
242+
name = str[:i]
243+
offset, err = strconv.Atoi(str[i:])
244+
if err != nil {
245+
return err
246+
}
247+
}
248+
249+
// Parse fine-grained severity level suffix, if present.
250+
// This supports formats like "ERROR3", "FATAL4", etc.
251+
i := len(name)
252+
for i > 0 && str[i-1] >= '0' && str[i-1] <= '9' {
253+
i--
254+
}
255+
if i < len(name) {
256+
n, err := strconv.Atoi(name[i:])
257+
if err != nil {
258+
return err
259+
}
260+
name = name[:i]
261+
if n != 0 {
262+
offset += n - 1 // Convert 1-based to 0-based.
263+
}
264+
}
265+
266+
switch strings.ToUpper(name) {
267+
case "TRACE":
268+
*s = SeverityTrace1
269+
case "DEBUG":
270+
*s = SeverityDebug1
271+
case "INFO":
272+
*s = SeverityInfo1
273+
case "WARN":
274+
*s = SeverityWarn1
275+
case "ERROR":
276+
*s = SeverityError1
277+
case "FATAL":
278+
*s = SeverityFatal1
279+
default:
280+
return errors.New("unknown name")
281+
}
282+
*s += Severity(offset)
283+
return nil
284+
}
285+
102286
// A SeverityVar is a [Severity] variable, to allow a [LogProcessor] severity
103287
// to change dynamically. It implements [Severitier] as well as a Set method,
104288
// and it is safe for use by multiple goroutines.
@@ -108,6 +292,15 @@ type SeverityVar struct {
108292
val atomic.Int64
109293
}
110294

295+
var (
296+
// Ensure Severity implements fmt.Stringer.
297+
_ fmt.Stringer = (*SeverityVar)(nil)
298+
// Ensure Severity implements encoding.TextMarshaler.
299+
_ encoding.TextMarshaler = (*SeverityVar)(nil)
300+
// Ensure Severity implements encoding.TextUnmarshaler.
301+
_ encoding.TextUnmarshaler = (*SeverityVar)(nil)
302+
)
303+
111304
// Severity returns v's severity.
112305
func (v *SeverityVar) Severity() log.Severity {
113306
return Severity(int(v.val.Load())).Severity()
@@ -118,6 +311,34 @@ func (v *SeverityVar) Set(l Severity) {
118311
v.val.Store(int64(l))
119312
}
120313

314+
// String returns a string representation of the SeverityVar.
315+
func (v *SeverityVar) String() string {
316+
return fmt.Sprintf("SeverityVar(%s)", Severity(int(v.val.Load())).String())
317+
}
318+
319+
// AppendText implements [encoding.TextAppender]
320+
// by calling [Severity.AppendText].
321+
func (v *SeverityVar) AppendText(b []byte) ([]byte, error) {
322+
return Severity(int(v.val.Load())).AppendText(b)
323+
}
324+
325+
// MarshalText implements [encoding.TextMarshaler]
326+
// by calling [SeverityVar.AppendText].
327+
func (v *SeverityVar) MarshalText() ([]byte, error) {
328+
return v.AppendText(nil)
329+
}
330+
331+
// UnmarshalText implements [encoding.TextUnmarshaler]
332+
// by calling [Severity.UnmarshalText].
333+
func (v *SeverityVar) UnmarshalText(data []byte) error {
334+
var s Severity
335+
if err := s.UnmarshalText(data); err != nil {
336+
return err
337+
}
338+
v.Set(s)
339+
return nil
340+
}
341+
121342
// A Severitier provides a [log.Severity] value.
122343
type Severitier interface {
123344
Severity() log.Severity
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build go1.24
5+
// +build go1.24
6+
7+
package minsev // import "go.opentelemetry.io/contrib/processors/minsev"
8+
9+
import "encoding"
10+
11+
var (
12+
_ encoding.TextAppender = Severity(0) // Ensure Severity implements encoding.TextAppender.
13+
_ encoding.TextAppender = (*SeverityVar)(nil) // Ensure Severity implements encoding.TextAppender.
14+
)

0 commit comments

Comments
 (0)