Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: no-docker
mvn-args: "-Pno-docker"
- name: docker
mvn-args: ""
permissions:
contents: read
issues: write
Expand All @@ -25,8 +33,8 @@ jobs:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: ./mvnw -q verify |& tee build.log
- name: Build with Maven (${{ matrix.name }})
run: ./mvnw -q ${{ matrix.mvn-args }} verify |& tee build.log
- name: Show build log
if: failure()
run: |
Expand Down Expand Up @@ -58,7 +66,7 @@ jobs:
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Create issue on failure
- name: Create issue on failure (${{ matrix.name }})
if: failure() && github.ref == 'refs/heads/develop'
uses: actions/github-script@v7
with:
Expand All @@ -72,7 +80,7 @@ jobs:
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `CI build failed for ${context.sha.slice(0,7)}`,
body: `Build failed for commit ${context.sha} in workflow run ${context.runId}.\\n\\nBuild error:\\n\\n\u0060\u0060\u0060\\n${errors}\\n\u0060\u0060\u0060\\n\\nLast lines of build log:\\n\\n\u0060\u0060\u0060\\n${log}\\n\u0060\u0060\u0060`,
title: `CI (${{ matrix.name }}) failed for ${context.sha.slice(0,7)}`,
body: `Build failed for commit ${context.sha} in workflow run ${context.runId} (matrix: ${{ matrix.name }}).\\n\\nBuild error:\\n\\n\u0060\u0060\u0060\\n${errors}\\n\u0060\u0060\u0060\\n\\nLast lines of build log:\\n\\n\u0060\u0060\u0060\\n${log}\\n\u0060\u0060\u0060`,
labels: ['ci']
});
30 changes: 30 additions & 0 deletions .project-management/ISSUES_OPERATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Follow-up Issues: Operations Documentation

Create the following GitHub issues to track operations docs and examples.

1) Ops: Micrometer integration examples
- Show counters via `MeterRegistry` (simple counters, timers around send)
- Listener wiring (`onSendFailures`) increments counters
- Sample Prometheus scrape via micrometer-registry-prometheus

2) Ops: Prometheus exporter example
- Minimal HTTP endpoint exposing counters
- Translate `DefaultNoteService.FailureInfo` into metrics labels (relay)
- Include guidance on cardinality

3) Ops: Logging patterns and correlation IDs
- MDC usage to correlate sends with subscriptions
- Recommended logger categories & sample filters
- JSON logging example (Logback)

4) Ops: Configuration deep-dive
- Advanced timeouts and backoff strategies (pros/cons)
- When to adjust `await-timeout-ms` / `poll-interval-ms`
- Retry tuning beyond defaults and trade-offs

5) Ops: Diagnostics cookbook
- Common failure scenarios and how to interpret FailureInfo
- Mapping failures to remediation steps
- Cross-relay differences and best practices

Note: Opening issues requires repository permissions; add the above as individual issues with `docs` and `operations` labels.
15 changes: 14 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ See [docs/explanation/architecture.md](docs/explanation/architecture.md) for det
- **Test all edge cases:** null values, empty strings, invalid inputs
- **Use descriptive test names** or `@DisplayName`

### Client/Handler tests

- See `nostr-java-api/src/test/java/nostr/api/client/README.md` for structure and naming.
- Naming conventions:
- `NostrSpringWebSocketClient*` for high‑level client behavior
- `WebSocketHandler*` for internal handler semantics (send/close/request)
- `NostrRequestDispatcher*` and `NostrSubscriptionManager*` for dispatcher/manager lifecycles
- Use `nostr.api.TestHandlerFactory` to construct `WebSocketClientHandler` from tests outside `nostr.api`.

### Client module tests

- See `nostr-java-client/src/test/java/nostr/client/springwebsocket/README.md` for an overview of the Spring WebSocket client test suite (retry/subscribe/timeout behavior).

### Test Example

```java
Expand Down Expand Up @@ -186,4 +199,4 @@ void testValidateKindRejectsNegative() {
- Summaries in pull requests must cite file paths and include testing output.
- Open pull requests using the template at `.github/pull_request_template.md` and complete every section.

By following these conventions, contributors help keep the codebase maintainable and aligned with the Nostr specifications.
By following these conventions, contributors help keep the codebase maintainable and aligned with the Nostr specifications.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# nostr-java
[![CI](https://github.com/tcheeric/nostr-java/actions/workflows/ci.yml/badge.svg)](https://github.com/tcheeric/nostr-java/actions/workflows/ci.yml)
[![CI Matrix: docker + no-docker](https://img.shields.io/badge/CI%20Matrix-docker%20%2B%20no--docker-blue)](https://github.com/tcheeric/nostr-java/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/tcheeric/nostr-java/branch/main/graph/badge.svg)](https://codecov.io/gh/tcheeric/nostr-java)
[![GitHub release](https://img.shields.io/github/v/release/tcheeric/nostr-java)](https://github.com/tcheeric/nostr-java/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
Expand All @@ -13,9 +14,64 @@

See [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) for installation and usage instructions.

## Running Tests

- Full test suite (requires Docker for Testcontainers ITs):

`mvn -q verify`

- Without Docker (skips Testcontainers-based integration tests via profile):

`mvn -q -Pno-docker verify`

The `no-docker` profile excludes tests under `**/nostr/api/integration/**` and sets `noDocker=true` for conditional test disabling.

### Troubleshooting failed relay sends

When broadcasting to multiple relays, failures on individual relays are tolerated and sending continues to other relays. To inspect which relays failed during the last send on the current thread:

```java
// Using the default client setup
NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(sender);
client.setRelays(Map.of(
"relayA", "wss://relayA.example.com",
"relayB", "wss://relayB.example.com"
));

List<String> responses = client.sendEvent(event);
// Inspect failures (if using DefaultNoteService)
Map<String, Throwable> failures = client.getLastSendFailures();
failures.forEach((relay, error) ->
System.out.println("Relay " + relay + " failed: " + error.getMessage())
);
```

This returns an empty map if a custom `NoteService` is used that does not expose diagnostics.

To receive failure notifications immediately after each send attempt when using the default client:

```java
client.onSendFailures(map -> {
map.forEach((relay, t) -> System.err.println(
"Send failed on relay " + relay + ": " + t.getClass().getSimpleName() + ": " + t.getMessage()
));
});
```

For more detail (timestamp, class, message), use:

```java
Map<String, DefaultNoteService.FailureInfo> info = client.getLastSendFailureDetails();
info.forEach((relay, d) -> System.out.printf(
"[%d] %s failed: %s - %s%n",
d.timestampEpochMillis, relay, d.exceptionClass, d.message
));
```

## Documentation

- Docs index: [docs/README.md](docs/README.md) — quick entry point to all guides and references.
- Operations: [docs/operations/README.md](docs/operations/README.md) — logging, metrics, configuration, diagnostics.
- Getting started: [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) — install via Maven/Gradle and build from source.
- API how‑to: [docs/howto/use-nostr-java-api.md](docs/howto/use-nostr-java-api.md) — create, sign, and publish basic events.
- Streaming subscriptions: [docs/howto/streaming-subscriptions.md](docs/howto/streaming-subscriptions.md) — open and manage long‑lived, non‑blocking subscriptions.
Expand Down
10 changes: 10 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Quick links to the most relevant guides and references.
- [howto/streaming-subscriptions.md](howto/streaming-subscriptions.md) — Long-lived subscriptions
- [howto/custom-events.md](howto/custom-events.md) — Creating custom event types

## Operations

- [operations/README.md](operations/README.md) — Ops index (logging, metrics, config)
- [howto/diagnostics.md](howto/diagnostics.md) — Inspecting relay failures and troubleshooting

## Reference

- [reference/nostr-java-api.md](reference/nostr-java-api.md) — API classes, methods, and examples
Expand All @@ -26,3 +31,8 @@ Quick links to the most relevant guides and references.
## Project

- [CODEBASE_OVERVIEW.md](CODEBASE_OVERVIEW.md) — Codebase layout, testing, contributing

## Tests Overview

- API Client/Handler tests: `nostr-java-api/src/test/java/nostr/api/client/README.md` — logging, relays, handler send/close/request, dispatcher & subscription manager
- Client module (Spring WebSocket): `nostr-java-client/src/test/java/nostr/client/springwebsocket/README.md` — send/subscribe retries and timeout behavior
86 changes: 86 additions & 0 deletions docs/howto/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Diagnostics: Relay Failures and Troubleshooting

This how‑to shows how to inspect, capture, and react to relay send failures when broadcasting events via the API client.

## Overview

- `DefaultNoteService` attempts to send an event to all configured relays.
- Failures on individual relays are tolerated; other relays are still attempted.
- After the send completes, you can inspect failures and structured details.
- You can also register a listener to receive failures in real time.

## Inspect last failures

```java
NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(sender);
client.setRelays(Map.of(
"relayA", "wss://relayA.example.com",
"relayB", "wss://relayB.example.com"
));

List<String> responses = client.sendEvent(event);

// Map<String, Throwable>: relay name to exception
Map<String, Throwable> failures = client.getLastSendFailures();
failures.forEach((relay, error) -> System.err.printf(
"Relay %s failed: %s%n", relay, error.getMessage()
));

// Structured details (timestamp, relay URI, cause chain summary)
Map<String, DefaultNoteService.FailureInfo> details = client.getLastSendFailureDetails();
details.forEach((relay, info) -> System.err.printf(
"[%d] %s (%s) failed: %s | root: %s - %s%n",
info.timestampEpochMillis,
info.relayName,
info.relayUri,
info.message,
info.rootCauseClass,
info.rootCauseMessage
));
```

Note: If you use a custom `NoteService`, these accessors return empty maps unless the implementation exposes diagnostics.

## Receive failures with a listener

Register a callback to receive the failures map immediately after each send attempt:

```java
client.onSendFailures(failureMap -> {
failureMap.forEach((relay, t) -> System.err.printf(
"Failure on %s: %s: %s%n",
relay, t.getClass().getSimpleName(), t.getMessage()
));
});
```

## Tips

- Partial success is common on public relays; prefer aggregating successful responses.
- Use `getLastSendFailureDetails()` when you need to correlate failures with relay URIs or log timestamps.
- Combine diagnostics with your retry/backoff strategy at the application level if needed.

## MDC snippet (correlate logs per send)

Use SLF4J MDC to attach a correlation id for a send. Remember to clear the MDC in `finally`.

```java
import org.slf4j.MDC;
import java.util.UUID;

String correlationId = UUID.randomUUID().toString();
MDC.put("corrId", correlationId);
try {
var responses = client.sendEvent(event);
// Your logging here; include %X{corrId} in your log pattern
log.info("Sent event id={} corrId={} responses={}", event.getId(), correlationId, responses.size());
} finally {
MDC.remove("corrId");
}
```

Logback pattern example:

```properties
logging.pattern.console=%d{HH:mm:ss.SSS} %-5level [%X{corrId}] %logger{36} - %msg%n
```
16 changes: 16 additions & 0 deletions docs/operations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Operations

Operational guidance and runbook-style topics for nostr-java.

## Topics

- Diagnostics and Failures
- See how-to: [../howto/diagnostics.md](../howto/diagnostics.md)
- Logging
- Recommended logger setup, categories, and verbosity — see [logging.md](logging.md)
- Metrics
- Exporting client metrics and subscription activity — see [metrics.md](metrics.md)
- Configuration
- Tuning timeouts, retries, and backoff — see [configuration.md](configuration.md)

If you have specific operational topics you’d like documented first, open an issue and tag it with `docs` and `operations`.
40 changes: 40 additions & 0 deletions docs/operations/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Configuration

Tune WebSocket behavior and retries for your environment.

## Purpose

- Adjust timeouts and poll intervals for send operations.
- Understand retry behavior for transient I/O failures.

## WebSocket client settings

The Spring WebSocket client reads the following properties (with defaults):

- `nostr.websocket.await-timeout-ms` (default: `60000`) — Max time to await a response after send.
- `nostr.websocket.poll-interval-ms` (default: `500`) — Poll interval used during await.

Example (application.properties):

```
nostr.websocket.await-timeout-ms=30000
nostr.websocket.poll-interval-ms=250
```

## Retry behavior

WebSocket send and subscribe operations are annotated with a common retry policy:

- Included exception: `IOException`
- Max attempts: `3`
- Backoff: initial `500ms`, multiplier `2.0`

These values are defined in the `@NostrRetryable` annotation. To customize globally, consider:

- Creating a custom annotation or replacing `@NostrRetryable` with your configuration.
- Providing your own `NoteService` or client wrapper that applies your retry strategy.

## Notes

- Timeouts apply per send; long-running subscriptions are managed separately.
- Ensure your relay endpoints’ SLAs align with chosen timeouts and backoff.
Loading