You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	Support custom error type semantics (#7442)
Allow instrumentation to provide domain-specific error type values (i.e.
HTTP or gRPC status codes) for the "error.type" semantic attribute. This
is accomplished by passing an error value that implements the
`interface{ ErrorType string }` interface.
---------
Co-authored-by: Robert Pająk <pellared@hotmail.com>
			
			
This commit is contained in:
		| @@ -42,6 +42,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm | ||||
| - `WithInstrumentationAttributes` in `go.opentelemetry.io/otel/log` synchronously de-duplicates the passed attributes instead of delegating it to the returned `LoggerOption`. (#7266) | ||||
| - `Distinct` in `go.opentelemetry.io/otel/attribute` is no longer guaranteed to uniquely identify an attribute set. Collisions between `Distinct` values for different Sets are possible with extremely high cardinality (billions of series per instrument), but are highly unlikely. (#7175) | ||||
| - The default `TranslationStrategy` in `go.opentelemetry.io/exporters/prometheus` is changed from `otlptranslator.NoUTF8EscapingWithSuffixes` to `otlptranslator.UnderscoreEscapingWithSuffixes`. (#7421) | ||||
| - The `ErrorType` function in `go.opentelemetry.io/otel/semconv/v1.37.0` now handles custom error types. | ||||
|   If an error implements an `ErrorType() string` method, the return value of that method will be used as the error type. (#7442) | ||||
|  | ||||
| <!-- Released section --> | ||||
| <!-- Don't change this section unless doing release --> | ||||
|   | ||||
| @@ -4,28 +4,54 @@ | ||||
| package semconv // import "go.opentelemetry.io/otel/semconv/{{.TagVer}}" | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| ) | ||||
|  | ||||
| // ErrorType returns an [attribute.KeyValue] identifying the error type of err. | ||||
| // | ||||
| // If err is nil, the returned attribute has the default value | ||||
| // [ErrorTypeOther]. | ||||
| // | ||||
| // If err's type has the method | ||||
| // | ||||
| //	ErrorType() string | ||||
| // | ||||
| // | ||||
| // then the returned attribute has the value of err.ErrorType(). Otherwise, the | ||||
| // returned attribute has a value derived from the concrete type of err. | ||||
| // | ||||
| // The key of the returned attribute is [ErrorTypeKey]. | ||||
| func ErrorType(err error) attribute.KeyValue { | ||||
| 	if err == nil { | ||||
| 		return ErrorTypeOther | ||||
| 	} | ||||
| 	t := reflect.TypeOf(err) | ||||
| 	var value string | ||||
| 	if t.PkgPath() == "" && t.Name() == "" { | ||||
| 		// Likely a builtin type. | ||||
| 		value = t.String() | ||||
| 	} else { | ||||
| 		value = fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) | ||||
| 	} | ||||
|  | ||||
| 	if value == "" { | ||||
| 		return ErrorTypeOther | ||||
| 	} | ||||
| 	return ErrorTypeKey.String(value) | ||||
| 	return ErrorTypeKey.String(errorType(err)) | ||||
| } | ||||
|  | ||||
| func errorType(err error) string { | ||||
| 	var s string | ||||
| 	if et, ok := err.(interface{ ErrorType() string }); ok { | ||||
| 		// Prioritize the ErrorType method if available. | ||||
| 		s = et.ErrorType() | ||||
| 	} | ||||
| 	if s == "" { | ||||
| 		// Fallback to reflection if the ErrorType method is not supported or | ||||
| 		// returns an empty value. | ||||
|  | ||||
| 		t := reflect.TypeOf(err) | ||||
| 		pkg, name := t.PkgPath(), t.Name() | ||||
| 		if pkg != "" && name != "" { | ||||
| 			s = pkg + "." + name | ||||
| 		} else { | ||||
| 			// The type has no package path or name (predeclared, not-defined, | ||||
| 			// or alias for a not-defined type). | ||||
| 			// | ||||
| 			// This is not guaranteed to be unique, but is a best effort. | ||||
| 			s = t.String() | ||||
| 		} | ||||
| 	} | ||||
| 	return s | ||||
| } | ||||
|   | ||||
| @@ -5,55 +5,41 @@ package semconv // import "go.opentelemetry.io/otel/semconv/{{.TagVer}}" | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| ) | ||||
|  | ||||
| type CustomError struct{} | ||||
|  | ||||
| func (CustomError) Error() string { | ||||
| 	return "custom error" | ||||
| } | ||||
| const pkg = "go.opentelemetry.io/otel/semconv/{{.TagVer}}" | ||||
|  | ||||
| func TestErrorType(t *testing.T) { | ||||
| 	customErr := CustomError{} | ||||
| 	builtinErr := errors.New("something went wrong") | ||||
| 	var nilErr error | ||||
| 	check(t, nil, ErrorTypeOther.Value.AsString()) | ||||
| 	check(t, errors.New("msg"), "*errors.errorString") | ||||
| 	check(t, custom("aborted"), "aborted") | ||||
| 	check(t, custom(""), pkg+".ErrCustomType") // empty ErrorType, use concrete type. | ||||
| } | ||||
|  | ||||
| 	wantCustomType := reflect.TypeOf(customErr) | ||||
| 	wantCustomStr := fmt.Sprintf("%s.%s", wantCustomType.PkgPath(), wantCustomType.Name()) | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name string | ||||
| 		err  error | ||||
| 		want attribute.KeyValue | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "BuiltinError", | ||||
| 			err:  builtinErr, | ||||
| 			want: attribute.String("error.type", "*errors.errorString"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "CustomError", | ||||
| 			err:  customErr, | ||||
| 			want: attribute.String("error.type", wantCustomStr), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "NilError", | ||||
| 			err:  nilErr, | ||||
| 			want: ErrorTypeOther, | ||||
| 		}, | ||||
| func check(t *testing.T, err error, want string) { | ||||
| 	t.Helper() | ||||
| 	got := ErrorType(err) | ||||
| 	if got.Key != ErrorTypeKey { | ||||
| 		t.Errorf("ErrorType(%v) key = %v, want %v", err, got.Key, ErrorTypeKey) | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := ErrorType(tt.err) | ||||
| 			if got != tt.want { | ||||
| 				t.Errorf("ErrorType(%v) = %v, want %v", tt.err, got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	if got.Value.AsString() != want { | ||||
| 		t.Errorf("ErrorType(%v) value = %v, want %v", err, got.Value.AsString(), want) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func custom(typ string) error { | ||||
| 	return ErrCustomType{Type: typ} | ||||
| } | ||||
|  | ||||
| type ErrCustomType struct { | ||||
| 	Type string | ||||
| } | ||||
|  | ||||
| func (e ErrCustomType) Error() string { | ||||
| 	return "custom: " + e.Type | ||||
| } | ||||
|  | ||||
| func (e ErrCustomType) ErrorType() string { | ||||
| 	return e.Type | ||||
| } | ||||
|   | ||||
| @@ -4,28 +4,57 @@ | ||||
| package semconv // import "go.opentelemetry.io/otel/semconv/v1.37.0" | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| ) | ||||
|  | ||||
| // ErrorType returns an [attribute.KeyValue] identifying the error type of err. | ||||
| // | ||||
| // If err is nil, the returned attribute has the default value | ||||
| // [ErrorTypeOther]. | ||||
| // | ||||
| // If err implements the interface | ||||
| // | ||||
| //	// ErrorTyper is an error that provides a specific type definition for the | ||||
| //	// error it represents. | ||||
| //	type ErrorTyper interface { | ||||
| //	    ErrorType() string | ||||
| //	} | ||||
| // | ||||
| // then the returned attribute has the value of err.ErrorType(). Otherwise, the | ||||
| // returned attribute has a value derived from the concrete type of err. | ||||
| // | ||||
| // The key of the returned attribute is [ErrorTypeKey]. | ||||
| func ErrorType(err error) attribute.KeyValue { | ||||
| 	if err == nil { | ||||
| 		return ErrorTypeOther | ||||
| 	} | ||||
| 	t := reflect.TypeOf(err) | ||||
| 	var value string | ||||
| 	if t.PkgPath() == "" && t.Name() == "" { | ||||
| 		// Likely a builtin type. | ||||
| 		value = t.String() | ||||
| 	} else { | ||||
| 		value = fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) | ||||
| 	} | ||||
|  | ||||
| 	if value == "" { | ||||
| 		return ErrorTypeOther | ||||
| 	} | ||||
| 	return ErrorTypeKey.String(value) | ||||
| 	return ErrorTypeKey.String(errorType(err)) | ||||
| } | ||||
|  | ||||
| func errorType(err error) string { | ||||
| 	var s string | ||||
| 	if et, ok := err.(interface{ ErrorType() string }); ok { | ||||
| 		// Prioritize the ErrorType method if available. | ||||
| 		s = et.ErrorType() | ||||
| 	} | ||||
| 	if s == "" { | ||||
| 		// Fallback to reflection if the ErrorType method is not supported or | ||||
| 		// returns an empty value. | ||||
|  | ||||
| 		t := reflect.TypeOf(err) | ||||
| 		pkg, name := t.PkgPath(), t.Name() | ||||
| 		if pkg != "" && name != "" { | ||||
| 			s = pkg + "." + name | ||||
| 		} else { | ||||
| 			// The type has no package path or name (predeclared, not-defined, | ||||
| 			// or alias for a not-defined type). | ||||
| 			// | ||||
| 			// This is not guaranteed to be unique, but is a best effort. | ||||
| 			s = t.String() | ||||
| 		} | ||||
| 	} | ||||
| 	return s | ||||
| } | ||||
|   | ||||
| @@ -5,55 +5,41 @@ package semconv // import "go.opentelemetry.io/otel/semconv/v1.37.0" | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| ) | ||||
|  | ||||
| type CustomError struct{} | ||||
|  | ||||
| func (CustomError) Error() string { | ||||
| 	return "custom error" | ||||
| } | ||||
| const pkg = "go.opentelemetry.io/otel/semconv/v1.37.0" | ||||
|  | ||||
| func TestErrorType(t *testing.T) { | ||||
| 	customErr := CustomError{} | ||||
| 	builtinErr := errors.New("something went wrong") | ||||
| 	var nilErr error | ||||
| 	check(t, nil, ErrorTypeOther.Value.AsString()) | ||||
| 	check(t, errors.New("msg"), "*errors.errorString") | ||||
| 	check(t, custom("aborted"), "aborted") | ||||
| 	check(t, custom(""), pkg+".ErrCustomType") // empty ErrorType, use concrete type. | ||||
| } | ||||
|  | ||||
| 	wantCustomType := reflect.TypeOf(customErr) | ||||
| 	wantCustomStr := fmt.Sprintf("%s.%s", wantCustomType.PkgPath(), wantCustomType.Name()) | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name string | ||||
| 		err  error | ||||
| 		want attribute.KeyValue | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "BuiltinError", | ||||
| 			err:  builtinErr, | ||||
| 			want: attribute.String("error.type", "*errors.errorString"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "CustomError", | ||||
| 			err:  customErr, | ||||
| 			want: attribute.String("error.type", wantCustomStr), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "NilError", | ||||
| 			err:  nilErr, | ||||
| 			want: ErrorTypeOther, | ||||
| 		}, | ||||
| func check(t *testing.T, err error, want string) { | ||||
| 	t.Helper() | ||||
| 	got := ErrorType(err) | ||||
| 	if got.Key != ErrorTypeKey { | ||||
| 		t.Errorf("ErrorType(%v) key = %v, want %v", err, got.Key, ErrorTypeKey) | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := ErrorType(tt.err) | ||||
| 			if got != tt.want { | ||||
| 				t.Errorf("ErrorType(%v) = %v, want %v", tt.err, got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	if got.Value.AsString() != want { | ||||
| 		t.Errorf("ErrorType(%v) value = %v, want %v", err, got.Value.AsString(), want) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func custom(typ string) error { | ||||
| 	return ErrCustomType{Type: typ} | ||||
| } | ||||
|  | ||||
| type ErrCustomType struct { | ||||
| 	Type string | ||||
| } | ||||
|  | ||||
| func (e ErrCustomType) Error() string { | ||||
| 	return "custom: " + e.Type | ||||
| } | ||||
|  | ||||
| func (e ErrCustomType) ErrorType() string { | ||||
| 	return e.Type | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user