Skip to content

Commit c7246fd

Browse files
authored
Merge pull request #47 from jkbrsn/cli-timeout
CLI timeout
2 parents d4695ce + b38597a commit c7246fd

File tree

10 files changed

+139
-57
lines changed

10 files changed

+139
-57
lines changed

.gitignore

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,3 @@ bin/
2525

2626
# Snap packages
2727
*.snap
28-
29-
# AI agent files
30-
AGENTS.md
31-
CLAUDE.md
32-

AGENTS.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build & Test Commands
6+
7+
```bash
8+
# Build
9+
make build # Build binary → bin/wsstat
10+
make build-all # Build for all 9 platforms (linux/darwin/windows × 386/amd64/arm64)
11+
12+
# Test
13+
make test # Run all tests with -shuffle=on
14+
make test N=5 # Run tests 5 times (burst/flaky detection)
15+
make test RACE=1 # Enable race detector
16+
make test V=1 # Verbose output
17+
make test N=3 RACE=1 V=1 # Combined flags
18+
19+
# Single package
20+
go test ./internal/app -v -race
21+
22+
# Single test
23+
go test ./... -run '^TestName$' -v -race
24+
go test ./ -run 'TestWSStat/Basic' -v # Focused subtest
25+
26+
# Lint & Format
27+
make lint # golangci-lint (revive, misspell, standard linters)
28+
make fmt # gofmt -s -w .
29+
```
30+
31+
## Project Structure
32+
33+
- **Module**: `github.com/jkbrsn/wsstat/v2`
34+
- **CLI**: `cmd/wsstat/` - flag parsing, config assembly, delegates to internal/app
35+
- **App layer**: `internal/app/` - high-level Client API, output formatting, orchestration
36+
- **Core library**: root package (`wsstat.go`, `result.go`, `subscription.go`, `wrappers.go`) - public API; wraps gorilla/websocket with timing instrumentation
37+
38+
## Architecture
39+
40+
Three-layer design:
41+
1. **CLI** parses flags, validates URLs (auto-adds `wss://` if missing), builds config
42+
2. **internal/app.Client** orchestrates measurement/subscription flows, handles output formatting
43+
3. **Root wsstat package** provides `WSStat` type for timed WebSocket operations with `Result` containing DNS/TCP/TLS/WS/RTT timings
44+
45+
All layers use functional options pattern: `New(opts ...Option)` with `WithTimeout()`, `WithTLSConfig()`, `WithBufferSize()`, etc.
46+
47+
## Code Conventions
48+
49+
- **Imports**: std lib, blank line, third-party, blank line, local packages; alphabetical within groups
50+
- **Formatting**: `gofmt -s`; soft 100-char line limit; max 80 lines per function (excluding tests)
51+
- **Errors**: wrap with `fmt.Errorf("context: %w", err)`; no trailing periods; sentinels as `var ErrX = errors.New(...)`
52+
- **Logging**: default `zerolog.Nop()`; inject via options; avoid noisy logs in tests
53+
- **Types**: prefer concrete types; avoid `interface{}`; nil-safe slices/maps; pass `context.Context`
54+
- **Security**: TLS verifies by default; CLI `-insecure` switches to `ws://`
55+
56+
## Testing Notes
57+
58+
- Tests start an echo WebSocket server on `localhost:8080` (TestMain); avoid port conflicts
59+
- Uses `testify` (assert, require) for assertions
60+
- CI runs with race detector and 16x repetition for flaky detection
61+
62+
## PR/Commit Standards
63+
64+
- Conventional commits: `feat:`, `fix:`, `docs:`, `chore(scope):` (e.g., `chore(version): bump to 2.2.0`)
65+
- Ensure `make lint && make test` pass before submitting
66+
- If user-facing behavior changes, update `CHANGELOG.md` and `VERSION`

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Changelog
2+
3+
Notable changes to this project will be documented in this file. To keep it lightweight, releases 2+ minor versions back will be churned regularly.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6+
7+
## [2.2.0]
8+
9+
### Added
10+
11+
- (CLI) New option `--timeout` (default 5s).
12+
- Applies both to connection dial and read timeouts.
13+
- `AGENTS.md`, symlinked to `CLAUDE.md` and `GEMINI.md`.
14+
15+
## [2.1.3]
16+
17+
### Changed
18+
19+
- Upgraded to Go 1.25.6.
20+
21+
## [2.1.1]
22+
23+
### Fixed
24+
25+
- (CLI) Terminal output now shows the correct IP when using the `--resolve` option.
26+
27+
## [2.1.0]
28+
29+
### Added
30+
31+
- (CLI) New option `--resolve`, allowing for direct IP targeting rather than DNS resolution.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

GEMINI.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

README.md

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -113,59 +113,30 @@ For macOS:
113113

114114
### Usage
115115

116-
All the options are available from the help output, have a look at `wsstat -h`:
116+
Some examples:
117117

118-
```sh
119-
wsstat 2.0.0
120-
Measure latency on WebSocket connections
121-
122-
USAGE:
123-
wsstat [options] <url>
124-
wsstat -subscribe [options] <url>
125-
126-
General:
127-
-c, --count <int> number of interactions [default: 1; unlimited when subscribing]
128-
--version print program version and exit
129-
130-
Input (choose one):
131-
--rpc-method <string> JSON-RPC method name to send (with id=1, jsonrpc=2.0)
132-
-t, --text <string> text message to send
133-
134-
Subscription:
135-
-s, --subscribe stream events until interrupted
136-
--subscribe-once subscribe and exit after the first event
137-
-b, --buffer <int> subscription delivery buffer size in messages [default: 0]
138-
--summary-interval <duration>
139-
print stat summaries every interval (e.g., 5s, 1m) [default: disabled]
140-
141-
Connection:
142-
-H, --header <string> HTTP header to include with request (repeatable; format: "Key: Value")
143-
--resolve <string> resolve host:port to specific address (repeatable; format: "HOST:PORT:ADDRESS")
144-
-k, --insecure skip TLS certificate verification (use with caution)
145-
--no-tls assume ws:// when URL lacks scheme [default: wss://]
146-
--color <string> color output mode: auto, always, never [default: auto]
147-
148-
Output:
149-
-q, --quiet suppress all output except response
150-
-v, --verbose increase verbosity (level 1)
151-
-vv increase verbosity (level 2)
152-
-f, --format <string> output format: auto, json, raw [default: auto]
153-
154-
Verbosity Levels:
155-
(default) minimal request info with summary timings
156-
-v adds target/TLS summaries and timing diagram
157-
-vv includes full TLS certificates and headers
158-
159-
Examples:
160-
wsstat wss://echo.example.com
161-
wsstat -t "ping" wss://echo.example.com
162-
wsstat --rpc-method eth_blockNumber wss://rpc.example.com/ws
163-
wsstat --subscribe --summary-interval 5s wss://stream.example.com/feed
164-
wsstat -H "Authorization: Bearer TOKEN" -H "Origin: https://foo" wss://api.example.com/ws
165-
wsstat --resolve example.com:443:127.0.0.1 wss://example.com/ws
166-
wsstat --insecure -vv wss://self-signed.example.com
118+
```bash
119+
# Basic request
120+
wsstat wss://echo.example.com
121+
122+
# Send an RPC method
123+
wsstat --rpc-method eth_blockNumber wss://rpc.example.com/ws
124+
125+
# Start a subscription
126+
wsstat --subscribe --summary-interval 5s wss://stream.example.com/feed
127+
128+
# Attach headers to dial request
129+
wsstat -H "Authorization: Bearer TOKEN" -H "Origin: https://foo" wss://api.example.com/ws
130+
131+
# Resolve to a target IP and set a longer timeout
132+
wsstat --resolve example.com:443:127.0.0.1 --timeout 30s wss://example.com/ws
133+
134+
# Allow insecure connection, make output extra verbose
135+
wsstat --insecure -vv wss://self-signed.example.com
167136
```
168137

138+
For a full list of the available options, check the `wsstat --help` option of your client.
139+
169140
### Subscription Mode
170141

171142
Long-lived streaming endpoints can be exercised with the subscription mode:
@@ -203,7 +174,7 @@ wsstat -subscribe-once -text '{"method":"subscribe_ticker"}' wss://example.org/w
203174

204175
For machine-readable output of summaries, add `-format json`.
205176

206-
## wsstat Package
177+
## wsstat Library Package
207178

208179
Use the `wsstat` Golang package to trace WebSocket connection and latency in your Go applications. It wraps [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) for the WebSocket protocol implementation, and measures the duration of the different phases of the connection cycle.
209180

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.1.3
1+
2.2.0

cmd/wsstat/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Config struct {
2121
SubscribeOnce bool
2222
BufferSize int
2323
SummaryInterval time.Duration
24+
Timeout time.Duration
2425
Format string
2526
ColorMode string
2627
Quiet bool
@@ -81,6 +82,7 @@ func parseConfig() (*Config, error) {
8182
SubscribeOnce: *subscribeOnce,
8283
BufferSize: *bufferSize,
8384
SummaryInterval: *summaryInterval,
85+
Timeout: *timeout,
8486
Format: strings.ToLower(*formatOption),
8587
ColorMode: strings.ToLower(*colorArg),
8688
Quiet: *quiet,

cmd/wsstat/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ var (
5454
subscribeOnce = flag.Bool("subscribe-once", false, "subscribe and exit after the first event")
5555
bufferSize = flag.Int("buffer", 0, "subscription delivery buffer size (messages)")
5656
summaryInterval = flag.Duration("summary-interval", 0, "print subscription summaries every interval (e.g., 1s, 5m, 1h); 0 disables")
57+
timeout = flag.Duration("timeout", 0, "read/dial timeout (e.g., 30s, 1m); 0 uses default (5s)")
5758

5859
// Output
5960
formatOption = flag.String("format", "auto", "output format: auto, json, or raw")
@@ -128,6 +129,7 @@ func run() error {
128129
app.WithBuffer(cfg.BufferSize),
129130
app.WithSummaryInterval(cfg.SummaryInterval),
130131
app.WithInsecure(cfg.Insecure),
132+
app.WithTimeout(cfg.Timeout),
131133
)
132134

133135
if err := ws.Validate(); err != nil {
@@ -196,6 +198,7 @@ func printUsage() {
196198
fmt.Fprintln(os.Stderr, " --resolve <string> resolve host:port to specific address (repeatable; format: \"HOST:PORT:ADDRESS\")")
197199
fmt.Fprintln(os.Stderr, " -k, --insecure skip TLS certificate verification (use with caution)")
198200
fmt.Fprintln(os.Stderr, " --no-tls assume ws:// when URL lacks scheme [default: wss://]")
201+
fmt.Fprintln(os.Stderr, " --timeout <duration> read/dial timeout (e.g., 30s, 1m) [default: 5s]")
199202
fmt.Fprintln(os.Stderr, " --color <string> color output mode: auto, always, never [default: auto]")
200203
fmt.Fprintln(os.Stderr)
201204

internal/app/client.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ type Client struct {
7676

7777
// TLS configuration
7878
insecure bool // skip TLS certificate verification
79+
80+
// Timeouts
81+
timeout time.Duration // read/dial timeout; 0 means use library default
7982
}
8083

8184
// Option configures a Client.
@@ -165,6 +168,11 @@ func WithInsecure(insecure bool) Option {
165168
return func(c *Client) { c.insecure = insecure }
166169
}
167170

171+
// WithTimeout sets the read/dial timeout. Zero uses the library default (5s).
172+
func WithTimeout(d time.Duration) Option {
173+
return func(c *Client) { c.timeout = d }
174+
}
175+
168176
// Count returns the configured interaction count.
169177
func (c *Client) Count() int { return c.count }
170178

@@ -197,6 +205,10 @@ func (c *Client) wsstatOptions() []wsstat.Option {
197205
opts = append(opts, wsstat.WithResolves(c.resolves))
198206
}
199207

208+
if c.timeout > 0 {
209+
opts = append(opts, wsstat.WithTimeout(c.timeout))
210+
}
211+
200212
return opts
201213
}
202214

0 commit comments

Comments
 (0)