增加排查日志

This commit is contained in:
andy committed 2026-09-03 16:48:44 +08:00
1 parent 2a64dc7395
commit 26caa7435f
2 files changed
+284 -1

No files matched your search

+150 -1
View File
@@ -7,9 +7,13 @@ import (
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type Status string
@@ -107,6 +111,7 @@ type Config struct {
type ProviderError struct {
Operation string
Status int
Cause error
}
func (e *ProviderError) Error() string {
@@ -116,6 +121,8 @@ func (e *ProviderError) Error() string {
return "provider " + e.Operation + " failed"
}
func (e *ProviderError) Unwrap() error { return e.Cause }
type httpAdapter struct {
name string
config Config
@@ -141,12 +148,15 @@ func (a *httpAdapter) query(ctx context.Context, id string) (Result, error) {
return a.call(ctx, http.MethodGet, a.queryPath(url.PathEscape(id)), nil, "query")
}
func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte, operation string) (Result, error) {
startedAt := time.Now()
base, err := url.Parse(strings.TrimRight(a.config.BaseURL, "/"))
if err != nil {
logHTTPProviderFailure(httpProviderDiagnostic{Provider: a.name, Operation: operation, ErrorClass: "config", ElapsedMS: elapsedMilliseconds(startedAt)})
return Result{}, errors.New("invalid provider base URL")
}
rel, err := url.Parse(path)
if err != nil {
logHTTPProviderFailure(httpProviderDiagnostic{Provider: a.name, Operation: operation, ErrorClass: "config", ElapsedMS: elapsedMilliseconds(startedAt)})
return Result{}, errors.New("invalid provider path")
}
base.Path = strings.TrimRight(base.Path, "/") + "/"
@@ -154,6 +164,7 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
target := base.ResolveReference(rel)
req, err := http.NewRequestWithContext(ctx, method, target.String(), strings.NewReader(string(body)))
if err != nil {
logHTTPProviderFailure(httpProviderDiagnostic{Provider: a.name, Operation: operation, ErrorClass: "request", ElapsedMS: elapsedMilliseconds(startedAt)})
return Result{}, fmt.Errorf("build provider request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
@@ -163,6 +174,15 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
}
resp, err := a.client.Do(req)
if err != nil {
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name, Operation: operation, ErrorClass: classifyHTTPProviderTransportError(err), ElapsedMS: elapsedMilliseconds(startedAt),
})
return Result{}, &ProviderError{Operation: a.name + " " + operation, Cause: err}
}
if resp == nil {
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name, Operation: operation, ErrorClass: "invalid_response", ElapsedMS: elapsedMilliseconds(startedAt),
})
return Result{}, &ProviderError{Operation: a.name + " " + operation}
}
defer resp.Body.Close()
@@ -171,13 +191,36 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
limit = 2 << 20
}
raw, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
if err != nil || int64(len(raw)) > limit {
if err != nil {
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name, Operation: operation, Status: resp.StatusCode, ErrorClass: "response_read", ElapsedMS: elapsedMilliseconds(startedAt),
})
return Result{}, &ProviderError{Operation: a.name + " " + operation, Cause: err}
}
if int64(len(raw)) > limit {
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name, Operation: operation, Status: resp.StatusCode, ErrorClass: "response_too_large", ElapsedMS: elapsedMilliseconds(startedAt),
})
return Result{}, &ProviderError{Operation: a.name + " " + operation}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
code, requestID, errorType := inspectHTTPProviderFailure(raw, resp.Header)
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name,
Operation: operation,
Status: resp.StatusCode,
Code: code,
RequestID: requestID,
ErrorType: errorType,
ErrorClass: "service",
ElapsedMS: elapsedMilliseconds(startedAt),
})
return Result{}, &ProviderError{Operation: a.name + " " + operation, Status: resp.StatusCode}
}
if !json.Valid(raw) {
logHTTPProviderFailure(httpProviderDiagnostic{
Provider: a.name, Operation: operation, Status: resp.StatusCode, ErrorClass: "invalid_response", ElapsedMS: elapsedMilliseconds(startedAt),
})
return Result{}, &ProviderError{Operation: a.name + " " + operation}
}
result := a.decode(raw)
@@ -185,6 +228,112 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
return result, nil
}
type httpProviderDiagnostic struct {
Provider string
Operation string
Status int
Code string
RequestID string
ErrorType string
ErrorClass string
ElapsedMS int64
}
func logHTTPProviderFailure(diagnostic httpProviderDiagnostic) {
log.Printf(
"zhinian-api provider operation failed provider=%s operation=%s status=%d code=%q requestId=%q errorType=%q errorClass=%s elapsedMs=%d",
diagnostic.Provider,
diagnostic.Operation,
diagnostic.Status,
diagnostic.Code,
diagnostic.RequestID,
diagnostic.ErrorType,
diagnostic.ErrorClass,
diagnostic.ElapsedMS,
)
}
func inspectHTTPProviderFailure(raw []byte, headers http.Header) (code, requestID, errorType string) {
root := map[string]any{}
_ = json.Unmarshal(raw, &root)
providerError := object(root["error"])
if len(providerError) == 0 {
providerError = object(root["Error"])
}
metadata := object(first(root["ResponseMetadata"], root["response_metadata"]))
metadataError := object(first(metadata["Error"], metadata["error"]))
code = safeHTTPProviderDiagnosticToken(first(
providerError["code"], providerError["Code"],
metadataError["code"], metadataError["Code"],
root["code"], root["Code"],
), 64)
requestID = safeHTTPProviderDiagnosticToken(first(
providerError["request_id"], providerError["requestId"], providerError["RequestId"], providerError["RequestID"],
root["request_id"], root["requestId"], root["RequestId"], root["RequestID"],
metadata["request_id"], metadata["requestId"], metadata["RequestId"], metadata["RequestID"],
headers.Get("X-Tt-Logid"), headers.Get("X-Request-Id"),
), 128)
errorType = safeHTTPProviderDiagnosticToken(first(
providerError["type"], providerError["Type"],
metadataError["type"], metadataError["Type"],
root["type"], root["Type"],
), 64)
return code, requestID, errorType
}
func safeHTTPProviderDiagnosticToken(value any, maxLength int) string {
var token string
switch typed := value.(type) {
case string:
token = strings.TrimSpace(typed)
case json.Number:
token = string(typed)
case float64:
token = strconv.FormatFloat(typed, 'f', -1, 64)
case float32:
token = strconv.FormatFloat(float64(typed), 'f', -1, 32)
case int:
token = strconv.Itoa(typed)
case int32:
token = strconv.FormatInt(int64(typed), 10)
case int64:
token = strconv.FormatInt(typed, 10)
}
if len(token) == 0 || len(token) > maxLength {
return ""
}
for _, character := range token {
if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || strings.ContainsRune("-_.:", character) {
continue
}
return ""
}
return token
}
func classifyHTTPProviderTransportError(err error) string {
if errors.Is(err, context.Canceled) {
return "canceled"
}
if errors.Is(err, context.DeadlineExceeded) {
return "timeout"
}
var networkError net.Error
if errors.As(err, &networkError) && networkError.Timeout() {
return "timeout"
}
var dnsError *net.DNSError
if errors.As(err, &dnsError) {
return "dns"
}
var operationError *net.OpError
if errors.As(err, &operationError) && operationError.Op == "dial" {
return "connect"
}
return "transport"
}
func record(raw []byte) map[string]any { var v map[string]any; _ = json.Unmarshal(raw, &v); return v }
func object(v any) map[string]any { x, _ := v.(map[string]any); return x }
func stringValue(values ...any) string {