Skip to content

Commit 16ddba8

Browse files
committed
Severity impl TextUnmarshaler/TextMarshaler
1 parent 56fc1df commit 16ddba8

File tree

3 files changed

+248
-2
lines changed

3 files changed

+248
-2
lines changed

processors/minsev/severity.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
package minsev // import "go.opentelemetry.io/contrib/processors/minsev"
55

66
import (
7+
"encoding"
8+
"errors"
79
"fmt"
10+
"strconv"
11+
"strings"
812
"sync/atomic"
913

1014
"go.opentelemetry.io/otel/log"
@@ -16,8 +20,14 @@ import (
1620
// as errors and critical events).
1721
type Severity int
1822

19-
// Ensure Severity implements fmt.Stringer.
20-
var _ fmt.Stringer = Severity(0)
23+
var (
24+
// Ensure Severity implements fmt.Stringer.
25+
_ fmt.Stringer = Severity(0)
26+
// Ensure Severity implements encoding.TextMarshaler.
27+
_ encoding.TextMarshaler = Severity(0)
28+
// Ensure Severity implements encoding.TextUnmarshaler.
29+
_ encoding.TextUnmarshaler = (*Severity)(nil)
30+
)
2131

2232
// Severity values defined by OpenTelemetry.
2333
const (
@@ -73,6 +83,10 @@ const (
7383
// It implements [Severitier].
7484
func (s Severity) Severity() log.Severity {
7585
// Unknown defaults to log.SeverityUndefined.
86+
//
87+
// TODO: return a clamped log.Severity. If s is less than
88+
// SeverityTrace1, return log.SeverityTrace1, if s is greater than
89+
// SeverityFatal4, return log.SeverityFatal4.
7690
return translations[s]
7791
}
7892

@@ -147,6 +161,105 @@ func (s Severity) String() string {
147161
}
148162
}
149163

164+
// AppendText implements [encoding.TextAppender] by calling [Severity.String].
165+
func (s Severity) AppendText(b []byte) ([]byte, error) {
166+
return append(b, s.String()...), nil
167+
}
168+
169+
// MarshalText implements [encoding.TextMarshaler] by calling
170+
// [Severity.AppendText].
171+
func (s Severity) MarshalText() ([]byte, error) {
172+
return s.AppendText(nil)
173+
}
174+
175+
// UnmarshalText implements [encoding.TextUnmarshaler]. It accepts any string
176+
// produced by [Severity.MarshalText], ignoring case. It also accepts numeric
177+
// offsets that would result in a different string on output. For example,
178+
// "ERROR-8" will marshal as SeverityInfo.
179+
func (s *Severity) UnmarshalText(data []byte) error {
180+
return s.parse(string(data))
181+
}
182+
183+
// parse parses str into s.
184+
//
185+
// It will return an error if str is not a valid severity string.
186+
//
187+
// The string is expected to be in the format of "NAME[N][+/-OFFSET]", where
188+
// NAME is one of the severity names ("TRACE", "DEBUG", "INFO", "WARN",
189+
// "ERROR", "FATAL"), OFFSET is an optional signed integer offset, and N is an
190+
// optional fine-grained severity level that modifies the base severity name.
191+
//
192+
// Name is parsed in a case-insensitive way. Meaning, "info", "Info",
193+
// "iNfO", etc. are all equivalent to "INFO".
194+
//
195+
// Fine-grained severity levels are expected to be in the range of 1 to 4,
196+
// where 1 is the base severity level, and 2, 3, and 4 are more fine-grained
197+
// levels. However, fine-grained levels greater than 4 are also accepted, and
198+
// they will be treated as an 1-based offset from the base severity level.
199+
//
200+
// For example, "ERROR3" will be parsed as "ERROR" with a fine-grained level of
201+
// 3, which corresponds to `SeverityError3`, "FATAL+2" will be parsed as
202+
// "FATAL" with an offset of +2, which corresponds to `SeverityFatal2`, and
203+
// "INFO2+1" is parsed as INFO with a fine-grained level of 2 and an offset of
204+
// +1, which corresponds to `SeverityInfo3`.
205+
//
206+
// Fine-grained severity levels are based on counting numbers excluding zero.
207+
// If a fine-grained level of 0 is provided it is treaded as equivalent to the
208+
// base severity level. For example, "INFO0" is equivalent to `SeverityInfo1`.
209+
func (s *Severity) parse(str string) (err error) {
210+
defer func() {
211+
if err != nil {
212+
err = fmt.Errorf("minsev: severity string %q: %w", str, err)
213+
}
214+
}()
215+
216+
name := str
217+
offset := 0
218+
219+
// Parse +/- offset suffix, if present.
220+
if i := strings.IndexAny(str, "+-"); i >= 0 {
221+
name = str[:i]
222+
offset, err = strconv.Atoi(str[i:])
223+
if err != nil {
224+
return err
225+
}
226+
}
227+
228+
// Parse fine-grained severity level suffix, if present.
229+
// This supports formats like "ERROR3", "FATAL4", etc.
230+
i := len(name)
231+
n, multi := 0, 1
232+
for ; i > 0 && str[i-1] >= '0' && str[i-1] <= '9'; i-- {
233+
n += int(str[i-1]-'0') * multi
234+
multi *= 10
235+
}
236+
if i < len(name) {
237+
name = name[:i]
238+
if n != 0 {
239+
offset += n - 1 // Convert 1-based to 0-based.
240+
}
241+
}
242+
243+
switch strings.ToUpper(name) {
244+
case "TRACE":
245+
*s = SeverityTrace1
246+
case "DEBUG":
247+
*s = SeverityDebug1
248+
case "INFO":
249+
*s = SeverityInfo1
250+
case "WARN":
251+
*s = SeverityWarn1
252+
case "ERROR":
253+
*s = SeverityError1
254+
case "FATAL":
255+
*s = SeverityFatal1
256+
default:
257+
return errors.New("unknown name")
258+
}
259+
*s += Severity(offset)
260+
return nil
261+
}
262+
150263
// A SeverityVar is a [Severity] variable, to allow a [LogProcessor] severity
151264
// to change dynamically. It implements [Severitier] as well as a Set method,
152265
// and it is safe for use by multiple goroutines.
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+
)

processors/minsev/severity_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
1112
"go.opentelemetry.io/otel/log"
1213
)
1314

@@ -81,10 +82,128 @@ var validEncodingTests = []struct {
8182
{"SeverityFatal4Plus2", SeverityFatal4 + 2, "FATAL+6"},
8283
}
8384

85+
var validDecodingTests = []struct {
86+
Name string
87+
Severity Severity
88+
Text string
89+
}{
90+
{"SeverityTrace", SeverityTrace, "TRACE"},
91+
{"SeverityTrace1", SeverityTrace1, "TRACE"},
92+
{"SeverityTrace2", SeverityTrace2, "TRACE2"},
93+
{"SeverityTrace3", SeverityTrace3, "TRACE3"},
94+
{"SeverityTrace4", SeverityTrace4, "TRACE4"},
95+
{"SeverityDebug", SeverityDebug, "DEBUG"},
96+
{"SeverityDebug1", SeverityDebug1, "DEBUG"},
97+
{"SeverityDebug2", SeverityDebug2, "DEBUG2"},
98+
{"SeverityDebug3", SeverityDebug3, "DEBUG3"},
99+
{"SeverityDebug4", SeverityDebug4, "DEBUG4"},
100+
{"SeverityInfo", SeverityInfo, "INFO"},
101+
{"SeverityInfo1", SeverityInfo1, "INFO"},
102+
{"SeverityInfo2", SeverityInfo2, "INFO2"},
103+
{"SeverityInfo3", SeverityInfo3, "INFO3"},
104+
{"SeverityInfo4", SeverityInfo4, "INFO4"},
105+
{"SeverityWarn", SeverityWarn, "WARN"},
106+
{"SeverityWarn1", SeverityWarn1, "WARN"},
107+
{"SeverityWarn2", SeverityWarn2, "WARN2"},
108+
{"SeverityWarn3", SeverityWarn3, "WARN3"},
109+
{"SeverityWarn4", SeverityWarn4, "WARN4"},
110+
{"SeverityError", SeverityError, "ERROR"},
111+
{"SeverityError1", SeverityError1, "ERROR"},
112+
{"SeverityError2", SeverityError2, "ERROR2"},
113+
{"SeverityError3", SeverityError3, "ERROR3"},
114+
{"SeverityError4", SeverityError4, "ERROR4"},
115+
{"SeverityFatal", SeverityFatal, "FATAL"},
116+
{"SeverityFatal1", SeverityFatal1, "FATAL"},
117+
{"SeverityFatal2", SeverityFatal2, "FATAL2"},
118+
{"SeverityFatal3", SeverityFatal3, "FATAL3"},
119+
{"SeverityFatal4", SeverityFatal4, "FATAL4"},
120+
121+
// Test case insensitivity.
122+
{"SeverityTraceLower", SeverityTrace1, "trace"},
123+
{"SeverityDebugMixed", SeverityDebug1, "Debug"},
124+
{"SeverityInfoMixed", SeverityInfo1, "InFo"},
125+
{"SeverityInfo3Lower", SeverityInfo3, "info3"},
126+
127+
// Test offset calculations.
128+
{"SeverityTraceMinus2", SeverityTrace1 - 2, "TRACE-2"},
129+
{"SeverityWarnPlus2", SeverityWarn3, "WARN+2"},
130+
{"SeverityWarn2Plus2", SeverityWarn4, "WARN2+2"},
131+
{"SeverityErrorMinus4", SeverityWarn1, "ERROR-4"},
132+
{"SeverityError2Minus4", SeverityWarn2, "ERROR2-4"},
133+
{"SeverityFatalPlus10", SeverityFatal1 + 10, "FATAL+10"},
134+
135+
// Test oversized fine-grained severity.
136+
{"SeverityTrace15", SeverityWarn3, "TRACE15"},
137+
{"SeverityTrace101", SeverityTrace1 + 100, "TRACE101"},
138+
139+
// Test fine-grained severity of zero.
140+
{"SeverityTrace0", SeverityTrace, "TRACE0"},
141+
{"SeverityTrace0Plus1", SeverityTrace2, "TRACE0+1"},
142+
}
143+
144+
var invalidText = []string{
145+
"UNKNOWN",
146+
"DEBUG3+abc",
147+
"INFO+abc",
148+
"ERROR-xyz",
149+
"not-a-level",
150+
}
151+
84152
func TestSeverityString(t *testing.T) {
85153
for _, test := range validEncodingTests {
86154
t.Run(test.Name, func(t *testing.T) {
87155
assert.Equal(t, test.Text, test.Severity.String())
88156
})
89157
}
90158
}
159+
160+
func TestSeverityMarshalText(t *testing.T) {
161+
for _, test := range validEncodingTests {
162+
t.Run(test.Name, func(t *testing.T) {
163+
got, err := test.Severity.MarshalText()
164+
require.NoError(t, err)
165+
assert.Equal(t, test.Text, string(got))
166+
})
167+
}
168+
}
169+
170+
func TestSeverityUnmarshalText(t *testing.T) {
171+
for _, test := range validDecodingTests {
172+
t.Run(test.Name, func(t *testing.T) {
173+
var sev Severity
174+
require.NoError(t, sev.UnmarshalText([]byte(test.Text)))
175+
const msg = "UnmarshalText(%q) != %d (%[2]s)"
176+
assert.Equalf(t, test.Severity, sev, msg, test.Text, test.Severity)
177+
})
178+
}
179+
}
180+
181+
func TestSeverityUnmarshalTextError(t *testing.T) {
182+
for _, test := range invalidText {
183+
t.Run(test, func(t *testing.T) {
184+
var sev Severity
185+
err := sev.UnmarshalText([]byte(test))
186+
assert.Error(t, err)
187+
})
188+
}
189+
}
190+
191+
func TestSeverityAppendText(t *testing.T) {
192+
tests := []struct {
193+
sev Severity
194+
prefix string
195+
expected string
196+
}{
197+
{SeverityInfo1, "", "INFO"},
198+
{SeverityError1, "level=", "level=ERROR"},
199+
{SeverityWarn3, "severity:", "severity:WARN3"},
200+
}
201+
202+
for _, test := range tests {
203+
t.Run(test.expected, func(t *testing.T) {
204+
result, err := test.sev.AppendText([]byte(test.prefix))
205+
require.NoError(t, err)
206+
assert.Equal(t, test.expected, string(result))
207+
})
208+
}
209+
}

0 commit comments

Comments
 (0)