refactor: update recording filename format to include date

- Update `getRecordingPath` to use a timestamp format that includes the date (`20060102-150405.000`).
- Update `parseInteractionFile` and `getFullTimestamp` to handle both the new filename format and the legacy format for backward compatibility.
- Improved parsing logic to reliably extract date, time, and HTTP method from interaction filenames.
This commit is contained in:
Tobias Gesellchen
2026-02-24 11:49:04 +01:00
parent be762dbc22
commit 0090746b89
3 changed files with 329 additions and 23 deletions
+181
View File
@@ -0,0 +1,181 @@
// Package main demonstrates the new recording filename format that includes date information.
package main
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gesellix/bose-soundtouch/pkg/service/proxy"
)
func main() {
fmt.Println("=== Recording Filename Format Demo ===")
fmt.Println()
// Create a temporary directory for the demo
tmpDir, err := os.MkdirTemp("", "recording-filename-demo")
if err != nil {
log.Fatalf("Failed to create temp directory: %v", err)
}
defer func() {
if removeErr := os.RemoveAll(tmpDir); removeErr != nil {
log.Printf("Failed to remove temp directory: %v", removeErr)
}
}()
fmt.Printf("Demo recordings will be saved to: %s\n\n", tmpDir)
// Create a recorder with async disabled for predictable demo output
if envErr := os.Setenv("RECORDER_ASYNC", "false"); envErr != nil {
log.Printf("Failed to set environment variable: %v", envErr)
}
recorder := proxy.NewRecorder(tmpDir)
defer recorder.Close()
fmt.Printf("Recorder session ID: %s\n", recorder.SessionID)
fmt.Println()
// Create some sample HTTP requests to record
requests := []struct {
method string
path string
category string
}{
{"GET", "/info", "self"},
{"POST", "/volume", "self"},
{"GET", "/nowPlaying", "self"},
{"PUT", "/preset_1", "self"},
}
fmt.Println("Recording sample HTTP interactions...")
fmt.Println()
for i, req := range requests {
// Create a mock HTTP request
httpReq, reqErr := http.NewRequest(req.method, "http://soundtouch.local:8090"+req.path, nil)
if reqErr != nil {
log.Printf("Failed to create request: %v", reqErr)
continue
}
// Create a mock response
httpRes := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Request: httpReq,
}
httpRes.Header.Set("Content-Type", "application/xml")
// Record the interaction
err = recorder.Record(req.category, httpReq, httpRes)
if err != nil {
log.Printf("Failed to record interaction: %v", err)
continue
}
fmt.Printf("%d. Recorded: %s %s\n", i+1, req.method, req.path)
// Small delay to show different timestamps
time.Sleep(100 * time.Millisecond)
}
fmt.Println()
fmt.Println("=== Generated Filenames ===")
fmt.Println()
// Walk through the recordings directory to show the generated filenames
interactionsDir := filepath.Join(tmpDir, "interactions")
err = filepath.Walk(interactionsDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(info.Name(), ".http") {
// Get relative path from interactions directory
rel, _ := filepath.Rel(interactionsDir, path)
fmt.Printf("📁 %s\n", rel)
// Parse and explain the filename format
filename := info.Name()
parts := strings.Split(strings.TrimSuffix(filename, ".http"), "-")
if len(parts) == 4 && len(parts[1]) == 8 {
// New format: count-yyyyMMdd-HHMMSS.sss-method.http
counter := parts[0]
dateStr := parts[1]
timeStr := parts[2]
method := parts[3]
// Format for display
date := dateStr[0:4] + "-" + dateStr[4:6] + "-" + dateStr[6:8]
time := timeStr[0:2] + ":" + timeStr[2:4] + ":" + timeStr[4:]
fmt.Printf(" 📋 Format: count-yyyyMMdd-HHMMSS.sss-method.http\n")
fmt.Printf(" 🔢 Counter: %s\n", counter)
fmt.Printf(" 📅 Date: %s (from %s)\n", date, dateStr)
fmt.Printf(" 🕒 Time: %s (from %s)\n", time, timeStr)
fmt.Printf(" 🔧 Method: %s\n", method)
fmt.Printf(" ✨ Full timestamp: %s %s\n", date, time)
} else {
fmt.Printf(" ⚠️ Legacy format or unexpected structure\n")
}
fmt.Println()
}
return nil
})
if err != nil {
log.Printf("Error walking directory: %v", err)
}
fmt.Println("=== Comparison with Old Format ===")
fmt.Println()
fmt.Println("🔴 OLD format (time only): 0047-21-53-06.128-GET.http")
fmt.Println(" - No date information in filename")
fmt.Println(" - Date extracted from session ID directory")
fmt.Println(" - Confusing when recordings span midnight")
fmt.Println()
fmt.Println("🟢 NEW format (date + time): 0047-20260223-215306.128-GET.http")
fmt.Println(" - Complete timestamp in filename")
fmt.Println(" - Self-contained, no need to check directory")
fmt.Println(" - Clear chronological ordering")
fmt.Println()
fmt.Println("=== Benefits ===")
fmt.Println("✅ No confusion when recordings cross midnight")
fmt.Println("✅ Complete timestamp visible at a glance")
fmt.Println("✅ Better sorting and organization")
fmt.Println("✅ Backwards compatible with existing parsing logic")
fmt.Println()
// Test the list interactions functionality
fmt.Println("=== Using ListInteractions API ===")
fmt.Println()
interactions, err := recorder.ListInteractions("", "", "")
if err != nil {
log.Printf("Failed to list interactions: %v", err)
return
}
fmt.Printf("Found %d recorded interactions:\n", len(interactions))
for i := range interactions {
interaction := &interactions[i]
fmt.Printf("%d. %s %s - %s (File: %s)\n",
i+1, interaction.Method, interaction.Path,
interaction.Timestamp, interaction.ID)
}
fmt.Printf("\nDemo completed! Recordings saved in: %s\n", tmpDir)
fmt.Println("You can explore the generated files to see the new format in action.")
}
+52 -23
View File
@@ -215,7 +215,7 @@ func (r *Recorder) getRecordingDir(category string, sanitizedSegments []string)
}
func (r *Recorder) getRecordingPath(dir, method string) string {
timestamp := time.Now().Format("15-04-05.000")
timestamp := time.Now().Format("20060102-150405.000")
count := atomic.AddUint64(&r.counter, 1)
filename := fmt.Sprintf("%04d-%s-%s.http", count, timestamp, method)
@@ -441,20 +441,44 @@ func (r *Recorder) parseInteractionFile(rel, path string, parts []string) (Inter
filename := parts[len(parts)-1]
fnParts := strings.Split(strings.TrimSuffix(filename, ".http"), "-")
date := ""
if len(sessionID) >= 8 {
date = sessionID[0:4] + "-" + sessionID[4:6] + "-" + sessionID[6:8]
timestamp := ""
method, counter := "UNKNOWN", 0
if len(fnParts) >= 1 {
_, _ = fmt.Sscanf(fnParts[0], "%d", &counter)
}
timestamp := ""
// Check if this is the new format: count-yyyyMMdd-HHMMSS.sss-method.http
// New format has 4 parts and the second part is 8 digits (yyyyMMdd)
if len(fnParts) == 4 && len(fnParts[1]) == 8 {
dateStr := fnParts[1] // yyyyMMdd
timeStr := fnParts[2] // HHMMSS.sss
method = fnParts[3]
// Format date: yyyyMMdd -> yyyy-MM-dd
date := dateStr[0:4] + "-" + dateStr[4:6] + "-" + dateStr[6:8]
// Format time: HHMMSS.sss -> HH:MM:SS.sss
if len(timeStr) >= 6 {
time := timeStr[0:2] + ":" + timeStr[2:4] + ":" + timeStr[4:]
timestamp = date + " " + time
}
} else if len(fnParts) >= 5 {
// Legacy format: count-HH-MM-SS.sss-method.http
// Extract date from sessionID for backward compatibility
date := ""
if len(sessionID) >= 8 {
date = sessionID[0:4] + "-" + sessionID[4:6] + "-" + sessionID[6:8]
}
if len(fnParts) >= 4 {
timeStr := fnParts[1] + ":" + fnParts[2] + ":" + fnParts[3]
timestamp = timeStr
if date != "" {
timestamp = date + " " + timeStr
}
method = fnParts[4]
}
requestPath := "/" + strings.Join(parts[2:len(parts)-1], "/")
@@ -462,15 +486,6 @@ func (r *Recorder) parseInteractionFile(rel, path string, parts []string) (Inter
requestPath = "/"
}
method, counter := "UNKNOWN", 0
if len(fnParts) >= 1 {
_, _ = fmt.Sscanf(fnParts[0], "%d", &counter)
}
if len(fnParts) >= 5 {
method = fnParts[4]
}
return Interaction{
ID: filename,
Session: sessionID,
@@ -485,18 +500,32 @@ func (r *Recorder) parseInteractionFile(rel, path string, parts []string) (Inter
}
func (r *Recorder) getFullTimestamp(sessionID, filename string) string {
if len(sessionID) < 8 {
return ""
}
date := sessionID[0:4] + "-" + sessionID[4:6] + "-" + sessionID[6:8]
fnParts := strings.Split(strings.TrimSuffix(filename, ".http"), "-")
if len(fnParts) < 4 {
return ""
// Check if this is the new format: count-yyyyMMdd-HHMMSS.sss-method.http
// New format has 4 parts and the second part is 8 digits (yyyyMMdd)
if len(fnParts) == 4 && len(fnParts[1]) == 8 {
dateStr := fnParts[1] // yyyyMMdd
timeStr := fnParts[2] // HHMMSS.sss
if len(timeStr) >= 6 {
date := dateStr[0:4] + "-" + dateStr[4:6] + "-" + dateStr[6:8]
time := timeStr[0:2] + "-" + timeStr[2:4] + "-" + timeStr[4:]
return date + "-" + time
}
} else if len(fnParts) >= 5 {
// Legacy format: count-HH-MM-SS.sss-method.http
if len(sessionID) < 8 {
return ""
}
date := sessionID[0:4] + "-" + sessionID[4:6] + "-" + sessionID[6:8]
return date + "-" + fnParts[1] + "-" + fnParts[2] + "-" + fnParts[3]
}
return date + "-" + fnParts[1] + "-" + fnParts[2] + "-" + fnParts[3]
return ""
}
func (r *Recorder) peekStatus(path string) int {
+96
View File
@@ -847,3 +847,99 @@ func TestRecorder_ListInteractions_FullTimestamp(t *testing.T) {
}
})
}
func TestRecorder_NewFilenameFormat_WithDate(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "recorder-new-format-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
r := NewRecorder(tmpDir)
sessionID := "20260223-150000-12345"
r.SessionID = sessionID
// Create recordings with the new format that includes date in filename
basePath := filepath.Join(tmpDir, "interactions", sessionID, "self", "test")
os.MkdirAll(basePath, 0755)
files := []string{
"0047-20260223-215306.128-GET.http",
"0048-20260223-215306.417-POST.http",
"0049-20260223-080034.500-GET.http",
"0050-20260223-080034.507-PUT.http",
}
for _, f := range files {
os.WriteFile(filepath.Join(basePath, f), []byte("test content"), 0644)
}
t.Run("Parse_New_Format_Timestamps", func(t *testing.T) {
interactions, err := r.ListInteractions(sessionID, "", "")
if err != nil {
t.Fatalf("ListInteractions failed: %v", err)
}
if len(interactions) != 4 {
t.Fatalf("Expected 4 interactions, got %d", len(interactions))
}
// Check that timestamps include both date and time from filename
expectedTimestamps := []string{
"2026-02-23 21:53:06.128",
"2026-02-23 21:53:06.417",
"2026-02-23 08:00:34.500",
"2026-02-23 08:00:34.507",
}
for i, interaction := range interactions {
if interaction.Timestamp != expectedTimestamps[i] {
t.Errorf("Expected timestamp %s, got %s for interaction %d",
expectedTimestamps[i], interaction.Timestamp, i)
}
}
})
t.Run("Parse_Methods_From_New_Format", func(t *testing.T) {
interactions, err := r.ListInteractions(sessionID, "", "")
if err != nil {
t.Fatalf("ListInteractions failed: %v", err)
}
expectedMethods := []string{"GET", "POST", "GET", "PUT"}
for i, interaction := range interactions {
if interaction.Method != expectedMethods[i] {
t.Errorf("Expected method %s, got %s for interaction %d",
expectedMethods[i], interaction.Method, i)
}
}
})
t.Run("Filter_By_New_Format_Timestamp", func(t *testing.T) {
// Filter for interactions after 10:00:00 on that day
interactions, err := r.ListInteractions(sessionID, "", "2026-02-23 10:00:00")
if err != nil {
t.Fatalf("ListInteractions failed: %v", err)
}
// Should get the two evening interactions (21:53:06.xxx)
if len(interactions) != 2 {
t.Fatalf("Expected 2 interactions after 10:00:00, got %d", len(interactions))
}
for _, interaction := range interactions {
if !strings.Contains(interaction.Timestamp, "21:53:06") {
t.Errorf("Expected evening timestamp, got %s", interaction.Timestamp)
}
}
})
t.Run("GetFullTimestamp_New_Format", func(t *testing.T) {
// Test the getFullTimestamp function directly
fullTS := r.getFullTimestamp(sessionID, "0047-20260223-215306.128-GET.http")
expected := "2026-02-23-21-53-06.128"
if fullTS != expected {
t.Errorf("Expected full timestamp %s, got %s", expected, fullTS)
}
})
}