slog: Levels

This is the first commit for the new experimental package `slog`,
providing structured, leveled logging. golang.org/x/exp/slog
implements the draft proposal currently under discussion
at golang/go.

This commit defines Level and the related type AtomicLevel.

Change-Id: I2825a926dab14ea66a62313ca2b6cd5c1ca04683
Reviewed-on: https://go-review.googlesource.com/c/exp/+/426020
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/level.go b/slog/level.go
new file mode 100644
index 0000000..1664e55
--- /dev/null
+++ b/slog/level.go
@@ -0,0 +1,107 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package slog
+
+import (
+	"fmt"
+	"math"
+	"sync/atomic"
+)
+
+// A Level is the importance or severity of a log event.
+// The higher the level, the less important or severe the event.
+type Level int
+
+// The level numbers below don't really matter too much. Any system can map them
+// to another numbering scheme if it wishes. We picked them to satisfy two
+// constraints.
+//
+// First, we wanted to make it easy to work with verbosities instead of levels.
+// Since higher verbosities are less important, higher levels are as well.
+//
+// Second, we wanted some room between levels to accommodate schemes with named
+// levels between ours. For example, Google Cloud Logging defines a Notice level
+// between Info and Warn. Since there are only a few of these intermediate
+// levels, the gap between the numbers need not be large. We selected a gap of
+// 10, because the majority of humans have 10 fingers.
+//
+// The missing gap between Info and Debug has to do with verbosities again. It
+// is natural to think of verbosity 0 as Info, and then verbosity 1 is the
+// lowest level one would call Debug. The simple formula
+//   level = InfoLevel + verbosity
+// then works well to map verbosities to levels. That is,
+//
+//   Level(InfoLevel+0).String() == "INFO"
+//   Level(InfoLevel+1).String() == "DEBUG"
+//   Level(InfoLevel+2).String() == "DEBUG+1"
+//
+// and so on.
+
+// Names for common levels.
+const (
+	ErrorLevel Level = 10
+	WarnLevel  Level = 20
+	InfoLevel  Level = 30
+	DebugLevel Level = 31
+)
+
+// String returns a name for the level.
+// If the level has a name, then that name
+// in uppercase is returned.
+// If the level is between named values, then
+// an integer is appended to the uppercased name.
+// Examples:
+//
+//	WarnLevel.String() => "WARN"
+//	(WarnLevel-2).String() => "WARN-2"
+func (l Level) String() string {
+	str := func(base string, val Level) string {
+		if val == 0 {
+			return base
+		}
+		return fmt.Sprintf("%s%+d", base, val)
+	}
+
+	switch {
+	case l <= 0:
+		return fmt.Sprintf("!BADLEVEL(%d)", l)
+	case l <= ErrorLevel:
+		return str("ERROR", l-ErrorLevel)
+	case l <= WarnLevel:
+		return str("WARN", l-WarnLevel)
+	case l <= InfoLevel:
+		return str("INFO", l-InfoLevel)
+	default:
+		return str("DEBUG", l-DebugLevel)
+	}
+}
+
+// An AtomicLevel is Level that can be read and written safely by multiple
+// goroutines.
+// Use NewAtomicLevel to create one.
+type AtomicLevel struct {
+	val atomic.Int64
+}
+
+// NewAtomicLevel creates an AtomicLevel initialized to the given Level.
+func NewAtomicLevel(l Level) *AtomicLevel {
+	var r AtomicLevel
+	r.Set(l)
+	return &r
+}
+
+// Level returns r's level.
+// If r is nil, it returns the maximum level.
+func (r *AtomicLevel) Level() Level {
+	if r == nil {
+		return Level(math.MaxInt)
+	}
+	return Level(int(r.val.Load()))
+}
+
+// Set sets r's level to l.
+func (r *AtomicLevel) Set(l Level) {
+	r.val.Store(int64(l))
+}
diff --git a/slog/level_test.go b/slog/level_test.go
new file mode 100644
index 0000000..d58a79c
--- /dev/null
+++ b/slog/level_test.go
@@ -0,0 +1,49 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package slog
+
+import (
+	"math"
+	"testing"
+)
+
+func TestLevelString(t *testing.T) {
+	for _, test := range []struct {
+		in   Level
+		want string
+	}{
+		{0, "!BADLEVEL(0)"},
+		{ErrorLevel, "ERROR"},
+		{ErrorLevel - 2, "ERROR-2"},
+		{WarnLevel, "WARN"},
+		{WarnLevel - 1, "WARN-1"},
+		{InfoLevel, "INFO"},
+		{InfoLevel - 3, "INFO-3"},
+		{DebugLevel, "DEBUG"},
+		{InfoLevel + 2, "DEBUG+1"},
+		{-1, "!BADLEVEL(-1)"},
+	} {
+		got := test.in.String()
+		if got != test.want {
+			t.Errorf("%d: got %s, want %s", test.in, got, test.want)
+		}
+	}
+}
+
+func TestAtomicLevel(t *testing.T) {
+	var r *AtomicLevel
+	if got, want := r.Level(), Level(math.MaxInt); got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	r = NewAtomicLevel(WarnLevel)
+	if got, want := r.Level(), WarnLevel; got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	r.Set(InfoLevel)
+	if got, want := r.Level(), InfoLevel; got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+
+}