Commit 4fafe86
committed
Merge #143: Add OutputSink abstraction for multi-destination output
1bd16d0 feat: make StandardSink public for composite usage (copilot-swe-agent[bot])
e8a98e5 fix: clippy doc-markdown warnings and format code (copilot-swe-agent[bot])
21f7884 feat: add OutputSink abstraction for multi-destination output (copilot-swe-agent[bot])
8bf1580 Initial plan (copilot-swe-agent[bot])
Pull request description:
Implements composable sink pattern enabling output to multiple destinations (console, file, telemetry) simultaneously while maintaining backward compatibility.
## Changes
**Core Abstractions**
- `OutputSink` trait with `write_message(message, formatted)` method
- `StandardSink` for stdout/stderr routing (default, backward compatible)
- `CompositeSink` for fan-out to multiple sinks
**Example Implementations**
- `FileSink` for file output
- `TelemetrySink` stub for observability integration
**Integration**
- `UserOutput` now uses `Box<dyn OutputSink>` instead of direct writers
- Added `UserOutput::with_sink()` constructor
- Made `StandardSink` public for composite usage
- All existing constructors unchanged
## Usage
```rust
// Default behavior unchanged
let mut output = UserOutput::new(VerbosityLevel::Normal);
// Console + File
let composite = CompositeSink::new(vec![
Box::new(StandardSink::default_console()),
Box::new(FileSink::new("output.log")?),
]);
let mut output = UserOutput::with_sink(VerbosityLevel::Normal, Box::new(composite));
// Console + Telemetry
let composite = CompositeSink::new(vec![
Box::new(StandardSink::default_console()),
Box::new(TelemetrySink::new("https://telemetry.example.com".to_string())),
]);
let mut output = UserOutput::with_sink(VerbosityLevel::Normal, Box::new(composite));
```
## Notes
- `flush()` is now no-op (documented limitation - StandardSink relies on OS line-buffering)
- 15 new tests added for sink implementations and integration
- External crates can implement custom sinks
## Related
Part of #102 - User Output Architecture Improvements
Depends on #127 - Message Trait (already merged)
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by firewall rules:
>
> - `192.0.2.1`
> - Triggering command: `ssh -i /nonexistent/key -p 22 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 [email protected] echo 'SSH connected'` (packet block)
>
> If you need me to access, download, or install something from one of these locations, you can either:
>
> - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/torrust/torrust-tracker-deployer/settings/copilot/coding_agent) (admins only)
>
> </details>
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>Proposal 9: Output Sink Abstraction</issue_title>
> <issue_description>## Overview
>
> Introduce an `OutputSink` abstraction layer to enable writing output to multiple destinations beyond stdout/stderr. This proposal enables output to files, network endpoints, telemetry systems, or multiple destinations simultaneously through a composable sink pattern.
>
> ## Specification
>
> See detailed specification: [docs/issues/140-output-sink-abstraction.md](../docs/issues/140-output-sink-abstraction.md)
>
> ## Goals
>
> - [ ] Define `OutputSink` trait for output destinations
> - [ ] Implement `StandardSink` for stdout/stderr (backward compatible)
> - [ ] Implement `CompositeSink` for multiple destinations
> - [ ] Enable extensibility for custom sinks (file, network, telemetry)
> - [ ] Maintain backward compatibility with existing API
> - [ ] Support testing with mock sinks
>
> ## Implementation Plan
>
> ### Phase 1: Core Trait and StandardSink (2 hours)
> - [ ] Define `OutputSink` trait with `write_message()` method
> - [ ] Implement `StandardSink` with stdout/stderr routing
> - [ ] Add `StandardSink::default_console()` convenience constructor
> - [ ] Ensure backward compatibility
>
> ### Phase 2: CompositeSink Implementation (1 hour)
> - [ ] Implement `CompositeSink` for multi-destination output
> - [ ] Add `new()` and `add_sink()` methods
> - [ ] Test fan-out behavior to multiple sinks
>
> ### Phase 3: UserOutput Integration (1.5 hours)
> - [ ] Update `UserOutput` to use `Box<dyn OutputSink>`
> - [ ] Add `with_sink()` constructor for custom sinks
> - [ ] Update `write()` method to use sink
> - [ ] Verify backward compatibility
>
> ### Phase 4: Example Sinks and Documentation (1 hour)
> - [ ] Implement `FileSink` as example
> - [ ] Implement `TelemetrySink` as example (mock)
> - [ ] Add comprehensive usage examples
>
> ### Phase 5: Testing (1.5 hours)
> - [ ] Add unit tests for `StandardSink`
> - [ ] Add unit tests for `CompositeSink`
> - [ ] Add integration tests with `UserOutput`
> - [ ] Create `MockSink` for test infrastructure
>
> ### Phase 6: Quality Assurance (1 hour)
> - [ ] Run `./scripts/pre-commit.sh` and fix any issues
> - [ ] Verify backward compatibility
>
> **Total Estimated Time**: 8 hours
>
> ## Acceptance Criteria
>
> ### Functional Requirements
> - [ ] `OutputSink` trait defines contract for output destinations
> - [ ] `StandardSink` maintains backward-compatible console output
> - [ ] `CompositeSink` enables multi-destination output
> - [ ] `UserOutput` works with any sink implementation
> - [ ] Custom sinks can be implemented externally
>
> ### API Design Requirements
> - [ ] Sink trait is simple and focused
> - [ ] Composition is straightforward and intuitive
> - [ ] Backward compatibility is maintained
> - [ ] API follows Rust conventions
>
> ### Testing Requirements
> - [ ] Unit tests cover sink implementations
> - [ ] Integration tests verify `UserOutput` with different sinks
> - [ ] Mock sink enables easy testing
> - [ ] All existing tests continue to pass
>
> ### Quality Requirements
> - [ ] Pre-commit checks pass: `./scripts/pre-commit.sh`
> - [ ] Code follows project conventions
> - [ ] Backward compatibility is verified
>
> ## Related
>
> - **Parent Epic**: #102 - User Output Architecture Improvements
> - **Refactoring Plan**: [docs/refactors/plans/user-output-architecture-improvements.md](../docs/refactors/plans/user-output-architecture-improvements.md)
> - **Depends On**: #127 - Use Message Trait for Extensibility
>
> ## Use Cases
>
> **Console Only (Default)**:
> ```rust
> let mut output = UserOutput::new(VerbosityLevel::Normal);
> ```
>
> **Console + File**:
> ```rust
> let composite = CompositeSink::new(vec![
> Box::new(StandardSink::default_console()),
> Box::new(FileSink::new("output.log")?),
> ]);
> let mut output = UserOutput::with_sink(VerbosityLevel::Normal, Box::new(composite));
> ```
>
> **Console + Telemetry**:
> ```rust
> let composite = CompositeSink::new(vec![
> Box::new(StandardSink::default_console()),
> Box::new(TelemetrySink::new("https://telemetry.example.com".to_string())),
> ]);
> let mut output = UserOutput::with_sink(VerbosityLevel::Normal, Box::new(composite));
> ```
>
> ## Labels
>
> `enhancement`, `phase-2`, `user-output`, `P2`
>
> ## Priority
>
> P2 (Phase 2 - Polish & Extensions)
>
> ## Estimated Effort
>
> 8 hours
> </issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> <comment_new><author>@josecelano</author><body>
> **Parent Epic**: #102 - User Output Architecture Improvements
>
> This issue is part of Phase 2: Polish & Extensions of the User Output Architecture refactoring.</body></comment_new>
> </comments>
>
</details>
- Fixes #140
<!-- START COPILOT CODING AGENT TIPS -->
---
✨ Let Copilot coding agent [set things up for you](https://github.com/torrust/torrust-tracker-deployer/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.
ACKs for top commit:
josecelano:
ACK 1bd16d0
Tree-SHA512: de334e58ae78f41e2f2dc61204b82638cd9f082524250475089dbb3fbf6fbb4ffdbb35c5fa9686712ddec8dbc477361437fee316c611d60d8bd42f665faf3e0f1 file changed
+751
-117
lines changed
0 commit comments