44package minsev // import "go.opentelemetry.io/contrib/processors/minsev"
55
66import (
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).
1622type 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.
1938const (
2039 // A fine-grained debugging log record. Typically disabled in default
@@ -69,6 +88,10 @@ const (
6988// It implements [Severitier].
7089func (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.
112323func (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.
122361type Severitier interface {
123362 Severity () log.Severity
0 commit comments