Skip to content

Commit 14aef57

Browse files
MrAliassahidveljipellared
authored
Support text interchange with minsev severity (#7652)
Co-authored-by: Sahid Velji <[email protected]> Co-authored-by: Robert Pająk <[email protected]>
1 parent 49bf21e commit 14aef57

File tree

5 files changed

+641
-20
lines changed

5 files changed

+641
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2525
- `go.opentelemetry.io/contrib/bridges/otellogrus`
2626
- `go.opentelemetry.io/contrib/bridges/otelslog`
2727
- `go.opentelemetry.io/contrib/bridges/otelzap`
28+
- 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)
29+
- The `SeverityVar` type from `go.opentelemetry.io/contrib/processors/minsev` now implements the `fmt.Stringer`, `encoding.TextMarshaler`, `encoding.TextUnmarshaler`, and `encoding.TextAppender` interfaces. (#7652)
2830

2931
### Deprecated
3032

processors/minsev/example_test.go

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,30 @@ 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+
// This example demonstrates how to use a Severitier that reads from
29+
// an environment variable.
30+
func ExampleSeveritier_environment() {
31+
const key = "LOG_LEVEL"
3832
// Mock an environmental variable setup that would be done externally.
3933
_ = os.Setenv(key, "error")
4034

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

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

processors/minsev/severity.go

Lines changed: 239 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
@@ -69,6 +88,10 @@ const (
6988
// It implements [Severitier].
7089
func (s Severity) Severity() log.Severity {
7190
// Unknown defaults to log.SeverityUndefined.
91+
//
92+
// TODO: return a clamped log.Severity. If s is less than
93+
// SeverityTrace1, return log.SeverityTrace1, if s is greater than
94+
// SeverityFatal4, return log.SeverityFatal4.
7295
return translations[s]
7396
}
7497

@@ -99,6 +122,185 @@ var translations = map[Severity]log.Severity{
99122
SeverityFatal4: log.SeverityFatal4,
100123
}
101124

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

313+
var (
314+
// Ensure Severity implements fmt.Stringer.
315+
_ fmt.Stringer = (*SeverityVar)(nil)
316+
// Ensure Severity implements encoding.TextMarshaler.
317+
_ encoding.TextMarshaler = (*SeverityVar)(nil)
318+
// Ensure Severity implements encoding.TextUnmarshaler.
319+
_ encoding.TextUnmarshaler = (*SeverityVar)(nil)
320+
)
321+
111322
// Severity returns v's severity.
112323
func (v *SeverityVar) Severity() log.Severity {
113324
return Severity(int(v.val.Load())).Severity()
@@ -118,6 +329,34 @@ func (v *SeverityVar) Set(l Severity) {
118329
v.val.Store(int64(l))
119330
}
120331

332+
// String returns a string representation of the SeverityVar.
333+
func (v *SeverityVar) String() string {
334+
return fmt.Sprintf("SeverityVar(%s)", Severity(int(v.val.Load())).String())
335+
}
336+
337+
// AppendText implements [encoding.TextAppender]
338+
// by calling [Severity.AppendText].
339+
func (v *SeverityVar) AppendText(b []byte) ([]byte, error) {
340+
return Severity(int(v.val.Load())).AppendText(b)
341+
}
342+
343+
// MarshalText implements [encoding.TextMarshaler]
344+
// by calling [SeverityVar.AppendText].
345+
func (v *SeverityVar) MarshalText() ([]byte, error) {
346+
return v.AppendText(nil)
347+
}
348+
349+
// UnmarshalText implements [encoding.TextUnmarshaler]
350+
// by calling [Severity.UnmarshalText].
351+
func (v *SeverityVar) UnmarshalText(data []byte) error {
352+
var s Severity
353+
if err := s.UnmarshalText(data); err != nil {
354+
return err
355+
}
356+
v.Set(s)
357+
return nil
358+
}
359+
121360
// A Severitier provides a [log.Severity] value.
122361
type Severitier interface {
123362
Severity() log.Severity

0 commit comments

Comments
 (0)