package observe import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "git.yuntuo.hk/k2safe/go-omni/logx" "git.yuntuo.hk/k2safe/go-omni/ws" "github.com/gorilla/websocket" ) func echo(w http.ResponseWriter, r *http.Request) { conn, err := (&websocket.Upgrader{}).Upgrade(w, r, nil) if err != nil { return } defer conn.Close() for { kind, data, err := conn.ReadMessage() if err != nil { return } if err = conn.WriteMessage(kind, data); err != nil { return } } } func testClient(t *testing.T) *ws.Client { t.Helper() c, err := ws.NewClient(ws.ClientConfig{ProxyMode: "direct", Session: ws.SessionConfig{CloseTimeout: 50 * time.Millisecond}}) if err != nil { t.Fatal(err) } t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := c.Shutdown(ctx); err != nil { t.Error(err) } }) return c } func endpoint(s *httptest.Server) string { return "ws" + strings.TrimPrefix(s.URL, "http") } func wait(t *testing.T, ch <-chan struct{}) { t.Helper() select { case <-ch: case <-time.After(2 * time.Second): t.Fatal("timeout") } } func TestObserverConcurrentSessionsAndTotals(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(echo)) defer server.Close() client := testClient(t) o := New(nil) const connections = 8 const count = 10 var workers sync.WaitGroup stopSamples := make(chan struct{}) samplesDone := make(chan struct{}) go func() { defer close(samplesDone) for { select { case <-stopSamples: return default: _ = o.Snapshot() } } }() for i := 0; i < connections; i++ { workers.Add(1) go func() { defer workers.Done() opened := make(chan struct{}) received := make(chan struct{}, count) s, err := client.Dial(context.Background(), endpoint(server), o.Wrap(ws.Handlers{OnOpen: func(context.Context, *ws.Session) error { close(opened); return nil }, OnMessage: func(context.Context, *ws.Session, ws.Message) error { received <- struct{}{}; return nil }})) if err != nil { t.Error(err) return } wait(t, opened) for j := 0; j < count; j++ { if err = s.Send(context.Background(), ws.Message{Type: ws.Text, Data: []byte("test")}); err != nil { t.Error(err) return } wait(t, received) } _ = s.Close(1000, "") wait(t, s.Done()) }() } workers.Wait() close(stopSamples) wait(t, samplesDone) got := o.Snapshot() if got.Opened != connections || got.Closed != connections || got.Active != 0 || got.MessagesSent != connections*count || got.MessagesReceived != connections*count || got.BytesSent != 4*connections*count || got.BytesReceived != 4*connections*count { t.Fatalf("snapshot: %+v", got) } if again := o.Snapshot(); again != got { t.Fatalf("closed totals changed: %+v -> %+v", got, again) } } func TestObserverCloseCallbackBoundaryAndPanic(t *testing.T) { for _, panics := range []bool{false, true} { t.Run(fmt.Sprint(panics), func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(echo)) defer server.Close() client := testClient(t) o := New(nil) opened := make(chan struct{}) closing := make(chan struct{}) release := make(chan struct{}) s, err := client.Dial(context.Background(), endpoint(server), o.Wrap(ws.Handlers{OnOpen: func(context.Context, *ws.Session) error { close(opened); return nil }, OnClose: func(*ws.Session, error) { close(closing) <-release if panics { panic("SECRET CALLBACK PANIC") } }})) if err != nil { t.Fatal(err) } wait(t, opened) _ = s.Close(1000, "") wait(t, closing) if got := o.Snapshot(); got.Active != 1 || got.Closed != 0 { t.Fatalf("closed before callback finished: %+v", got) } select { case <-s.Done(): t.Fatal("Done closed before user callback") default: } close(release) wait(t, s.Done()) got := o.Snapshot() if got.Active != 0 || got.Closed != 1 { t.Fatalf("callback did not release observation: %+v", got) } if panics && got.CloseCallbackPanics != 1 { t.Fatalf("panic not counted: %+v", got) } }) } } func TestObserverPayloadFreeLogsAndIndependentDurations(t *testing.T) { var output bytes.Buffer logger, err := logx.New(logx.Config{Writer: &output, Format: "json", Level: "info"}) if err != nil { t.Fatal(err) } defer logger.Close() o := New(logger) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := (&websocket.Upgrader{}).Upgrade(w, r, nil) if err != nil { return } defer conn.Close() if _, _, err = conn.ReadMessage(); err == nil { _ = conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(1008, "SECRET CLOSE REASON"), time.Now().Add(time.Second)) _, _, _ = conn.ReadMessage() } })) defer server.Close() client := testClient(t) var sessions []*ws.Session for i := 0; i < 2; i++ { opened := make(chan struct{}) s, err := client.Dial(context.Background(), endpoint(server)+"/?token=SECRET-URL", o.Wrap(ws.Handlers{OnOpen: func(context.Context, *ws.Session) error { close(opened); return nil }})) if err != nil { t.Fatal(err) } wait(t, opened) sessions = append(sessions, s) if i == 0 { time.Sleep(40 * time.Millisecond) } } for _, s := range sessions { if err = s.Send(context.Background(), ws.Message{Type: ws.Text, Data: []byte("SECRET MESSAGE")}); err != nil { t.Fatal(err) } wait(t, s.Done()) } if strings.Contains(output.String(), "SECRET") { t.Fatalf("log leaked source data: %s", output.String()) } var durations []float64 for _, line := range strings.Split(strings.TrimSpace(output.String()), "\n") { var row map[string]any if err = json.Unmarshal([]byte(line), &row); err != nil { t.Fatal(err) } for key := range row { switch key { case "time", "level", "msg", "sessionId", "phase", "durationMs", "closeCode": default: t.Fatalf("unexpected log field %q", key) } } if row["msg"] == "ws.close" { if row["closeCode"] != float64(1008) { t.Fatalf("close code: %#v", row) } durations = append(durations, row["durationMs"].(float64)) } } if len(durations) != 2 || durations[0] < 30 { t.Fatalf("timers not per session: %v", durations) } } func TestObserverQueueSnapshotsAndSampledPeaks(t *testing.T) { startSending := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := (&websocket.Upgrader{}).Upgrade(w, r, nil) if err != nil { return } defer conn.Close() <-startSending for i := 0; i < 5; i++ { if conn.WriteMessage(websocket.TextMessage, []byte("queue")) != nil { return } } for { if _, _, err = conn.ReadMessage(); err != nil { return } } })) defer server.Close() client := testClient(t) o := New(nil) opened := make(chan struct{}) release := make(chan struct{}) s, err := client.Dial(context.Background(), endpoint(server), o.Wrap(ws.Handlers{OnOpen: func(context.Context, *ws.Session) error { close(opened); return nil }, OnMessage: func(context.Context, *ws.Session, ws.Message) error { <-release; return nil }})) if err != nil { t.Fatal(err) } wait(t, opened) close(startSending) deadline := time.After(2 * time.Second) var sampled Snapshot for { sampled = o.Snapshot() if sampled.MessagesReceived == 5 { break } select { case <-deadline: close(release) t.Fatal("receive timeout") default: time.Sleep(time.Millisecond) } } if sampled.ReadQueuedMessages != 5 || sampled.ReadQueuedBytes != 25 || sampled.SampledPeakReadQueuedMessages != 5 { t.Fatalf("queue snapshot: %+v", sampled) } close(release) _ = s.Close(1000, "") wait(t, s.Done()) final := o.Snapshot() if final.ReadQueuedMessages != 0 || final.SampledPeakReadQueuedMessages != 5 { t.Fatalf("final: %+v", final) } } type opaqueError struct{} func (opaqueError) Error() string { panic("must not expand error") } func TestObserverCloseFieldsNeverExpandsErrors(t *testing.T) { for _, err := range []error{opaqueError{}, &ws.OpError{Op: "SECRET STAGE", Cause: opaqueError{}}, &ws.CloseError{Code: 1008, Cause: opaqueError{}}} { _, phase := closeFields(err, false) if strings.Contains(phase, "SECRET") { t.Fatal("untrusted phase") } } if code, phase := closeFields(opaqueError{}, true); code != 0 || phase != "on_close_panic" { t.Fatalf("panic fields: %d %s", code, phase) } }