add cloud playback diagnostics

This commit is contained in:
ShukeBta
2026-06-13 17:26:49 +08:00
parent 5f42836bbf
commit b6047af9fb
2 changed files with 135 additions and 2 deletions
+110 -1
View File
@@ -3,11 +3,18 @@
package handler package handler
import ( import (
"crypto/sha256"
"encoding/hex"
"io" "io"
"net/http" "net/http"
"net/url"
"path"
"sort"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/model" "github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service" "github.com/ShukeBta/MediaStationGo/internal/service"
@@ -303,8 +310,12 @@ func serveCloudResolvedLink(svc *service.Container, c *gin.Context, typ, ref str
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "cloud storage service unavailable"}) c.JSON(http.StatusServiceUnavailable, gin.H{"error": "cloud storage service unavailable"})
return return
} }
resolveStart := time.Now()
link, err := svc.StorageCfg.CloudResolve(c.Request.Context(), typ, ref, c.Request.UserAgent()) link, err := svc.StorageCfg.CloudResolve(c.Request.Context(), typ, ref, c.Request.UserAgent())
resolveDur := time.Since(resolveStart)
if err != nil { if err != nil {
logCloudPlayback(svc, "cloud playback resolve failed",
append(cloudPlaybackLogFields(typ, ref, nil, resolveDur), zap.Error(err))...)
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
@@ -319,6 +330,13 @@ func serveCloudResolvedLink(svc *service.Container, c *gin.Context, typ, ref str
} }
if !link.Proxy { if !link.Proxy {
// Pure offload: send the client straight to the cloud CDN. // Pure offload: send the client straight to the cloud CDN.
logCloudPlayback(svc, "cloud playback redirect",
append(cloudPlaybackLogFields(typ, ref, link, resolveDur),
zap.String("mode", "redirect"),
zap.Int("status", http.StatusFound),
zap.String("method", c.Request.Method),
zap.String("range", c.GetHeader("Range")),
)...)
c.Redirect(http.StatusFound, link.URL) c.Redirect(http.StatusFound, link.URL)
return return
} }
@@ -345,8 +363,18 @@ func serveCloudResolvedLink(svc *service.Container, c *gin.Context, typ, ref str
if c.GetHeader("Accept-Encoding") == "" { if c.GetHeader("Accept-Encoding") == "" {
req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Accept-Encoding", "identity")
} }
upstreamStart := time.Now()
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
upstreamHeaderDur := time.Since(upstreamStart)
if err != nil { if err != nil {
logCloudPlayback(svc, "cloud playback proxy upstream failed",
append(cloudPlaybackLogFields(typ, ref, link, resolveDur),
zap.String("mode", "proxy"),
zap.String("method", method),
zap.String("range", c.GetHeader("Range")),
zap.Int64("upstream_header_ms", durationMilliseconds(upstreamHeaderDur)),
zap.Error(err),
)...)
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return return
} }
@@ -363,9 +391,29 @@ func serveCloudResolvedLink(svc *service.Container, c *gin.Context, typ, ref str
c.Header("Cache-Control", "no-store") c.Header("Cache-Control", "no-store")
} }
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
var copied int64
var copyErr error
streamStart := time.Now()
if c.Request.Method != http.MethodHead { if c.Request.Method != http.MethodHead {
_, _ = io.Copy(c.Writer, resp.Body) copied, copyErr = io.Copy(c.Writer, resp.Body)
} }
fields := append(cloudPlaybackLogFields(typ, ref, link, resolveDur),
zap.String("mode", "proxy"),
zap.String("method", method),
zap.String("range", c.GetHeader("Range")),
zap.Int("status", resp.StatusCode),
zap.String("content_range", resp.Header.Get("Content-Range")),
zap.String("content_length", resp.Header.Get("Content-Length")),
zap.Int64("upstream_header_ms", durationMilliseconds(upstreamHeaderDur)),
zap.Int64("stream_ms", durationMilliseconds(time.Since(streamStart))),
zap.Int64("total_ms", durationMilliseconds(time.Since(resolveStart))),
zap.Int64("bytes", copied),
)
if copyErr != nil {
logCloudPlayback(svc, "cloud playback proxy copy failed", append(fields, zap.Error(copyErr))...)
return
}
logCloudPlayback(svc, "cloud playback proxy finished", fields...)
} }
func isCloudImageRef(ref string) bool { func isCloudImageRef(ref string) bool {
@@ -377,3 +425,64 @@ func isCloudImageRef(ref string) bool {
} }
return false return false
} }
func logCloudPlayback(svc *service.Container, msg string, fields ...zap.Field) {
if svc == nil || svc.Log == nil {
return
}
svc.Log.Info(msg, fields...)
}
func cloudPlaybackLogFields(typ, ref string, link *cloud.DirectLink, resolveDur time.Duration) []zap.Field {
refHash, refExt := cloudPlaybackRefFingerprint(ref)
fields := []zap.Field{
zap.String("provider", strings.TrimSpace(typ)),
zap.String("ref_hash", refHash),
zap.String("ref_ext", refExt),
zap.Int64("resolve_ms", durationMilliseconds(resolveDur)),
}
if link != nil {
fields = append(fields,
zap.String("target_host", cloudPlaybackLinkHost(link.URL)),
zap.Bool("headers_required", len(link.Headers) > 0),
zap.Strings("header_names", cloudPlaybackHeaderNames(link.Headers)),
)
}
return fields
}
func cloudPlaybackRefFingerprint(ref string) (string, string) {
ref = strings.TrimSpace(ref)
sum := sha256.Sum256([]byte(ref))
ext := strings.ToLower(path.Ext(strings.Trim(strings.ReplaceAll(ref, "\\", "/"), "/")))
return hex.EncodeToString(sum[:])[:12], ext
}
func cloudPlaybackLinkHost(raw string) string {
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil || u.Host == "" {
return ""
}
return u.Host
}
func cloudPlaybackHeaderNames(headers map[string]string) []string {
if len(headers) == 0 {
return nil
}
out := make([]string, 0, len(headers))
for key := range headers {
if key = strings.TrimSpace(key); key != "" {
out = append(out, key)
}
}
sort.Strings(out)
return out
}
func durationMilliseconds(d time.Duration) int64 {
if d <= 0 {
return 0
}
return d.Milliseconds()
}
+25 -1
View File
@@ -1,6 +1,9 @@
package handler package handler
import "testing" import (
"strings"
"testing"
)
func TestCloudMountLibraryNameDefaultsToDirectoryBaseName(t *testing.T) { func TestCloudMountLibraryNameDefaultsToDirectoryBaseName(t *testing.T) {
tests := []struct { tests := []struct {
@@ -24,3 +27,24 @@ func TestCloudMountLibraryNameDefaultsToDirectoryBaseName(t *testing.T) {
}) })
} }
} }
func TestCloudPlaybackDiagnosticsDoNotExposeRawRefOrURL(t *testing.T) {
rawRef := "/剧集/国产剧/很长的敏感文件名.S01E01.mkv"
refHash, refExt := cloudPlaybackRefFingerprint(rawRef)
if refHash == "" || strings.Contains(rawRef, refHash) {
t.Fatalf("ref hash should be a short fingerprint, got %q", refHash)
}
if refExt != ".mkv" {
t.Fatalf("ref ext = %q, want .mkv", refExt)
}
if host := cloudPlaybackLinkHost("https://cdn.example.test/movie.mkv?token=secret"); host != "cdn.example.test" {
t.Fatalf("host = %q, want cdn.example.test", host)
}
names := cloudPlaybackHeaderNames(map[string]string{
"Authorization": "Bearer secret",
"Cookie": "sid=secret",
})
if got := strings.Join(names, ","); got != "Authorization,Cookie" {
t.Fatalf("header names = %q", got)
}
}