1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
use crate::login::{LoginCredentials, StaticLoginCredentials};
use std::borrow::Cow;
#[cfg(feature = "metrics-collection")]
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;

/// Configures settings for a `TwitchIRCClient`.
#[derive(Debug)]
pub struct ClientConfig<L: LoginCredentials> {
    /// Gets a set of credentials every time the client needs to log in on a new connection.
    /// See [`LoginCredentials`] for details.
    pub login_credentials: L,

    /// A new connection will automatically be created if a channel is joined and all
    /// currently established connections have joined at least this many channels.
    pub max_channels_per_connection: usize,

    /// A new connection will automatically be created if any message is to be sent
    /// and all currently established connections have recently sent more than this many
    /// messages (time interval is defined by `max_waiting_messages_duration_window`)
    pub max_waiting_messages_per_connection: usize,

    /// We assume messages to be "waiting" for this amount of time after sending them out, e.g.
    /// typically 100 or 150 milliseconds (purely a value that has been measured/observed,
    /// not documented or fixed in any way)
    pub time_per_message: Duration,

    /// rate-limits the opening of new connections. By default this is constructed with 1 permit
    /// only, which means connections cannot be opened in parallel. If this is set to more than 1
    /// permit, then that many connections can be opened in parallel.
    ///
    /// This is designed to be wrapped in an Arc to allow it to be shared between multiple
    /// TwitchIRCClient instances.
    pub connection_rate_limiter: Arc<Semaphore>,

    /// Allow a new connection to be made after this period has elapsed. By default this is set
    /// to 2 seconds, and combined with the permits=1 of the semaphore, allows one connection
    /// to be made every 2 seconds.
    ///
    /// More specifically, after taking the permit from the semaphore, the permit will be put
    /// back after this period has elapsed.
    pub new_connection_every: Duration,

    /// Imposes a general timeout for new connections. This is in place in addition to possible
    /// operating system timeouts (E.g. for new TCP connections), since additional "connect" work
    /// takes place after the TCP connection is opened, e.g. to set up TLS or perform a WebSocket
    /// handshake. Default value: 20 seconds.
    pub connect_timeout: Duration,

    /// Disable or enable and configure the collection of metrics on this `TwitchIRCClient`
    /// using the `prometheus` crate. See more information about the possible options on the
    /// [`MetricsConfig`] enum.
    ///
    /// This crate is currently capable of exporting the following prometheus metrics:
    /// * `twitchirc_messages_received` with label `command` counts all incoming messages. (Counter)
    ///
    /// * `twitchirc_messages_sent` counts messages sent out, with a `command` label. (Counter)
    ///
    /// * `twitchirc_channels` with `type=allocated/confirmed` counts how many channels
    ///   you are joined to (Gauge). Allocated channels are joins that passed through the `TwitchIRCClient`
    ///   but may be waiting e.g. for the connection to finish connecting. Once a
    ///   confirmation response is received by Twitch that the channel was joined successfully,
    ///   that channel is additionally `confirmed`.
    ///
    /// * `twitchirc_connections` counts how many connections this client has in use (Gauge).
    ///    The label `state=initializing/open` identifies how many connections are
    ///    in the process of connecting (`initializing`) vs how many connections are already established (`open`).
    ///
    /// * `twitchirc_connections_failed` counts every time a connection fails (Counter). Note however, depending
    ///   on conditions e.g. how many channels were joined on that channel, there can be cases where
    ///   a connection failing would not mandate the creation of a new connection (e.g. if
    ///   you have parted channels on other connections, making it so all the channels the failed
    ///   connection was joined to can be re-joined on those already existing connections).
    ///
    /// * `twitchirc_connections_created` on the other hand tracks how many times, since
    ///   the creation of the client, a new connection has been made.
    #[cfg(feature = "metrics-collection")]
    pub metrics_config: MetricsConfig,

    /// Allows you to differentiate between multiple clients with
    /// [the `tracing` crate](https://docs.rs/tracing).
    ///
    /// This library logs a variety of trace, debug, info, warning and error messages using the
    /// `tracing` crate. An example log line using the default `tracing_subscriber` output format
    /// might look like this:
    ///
    /// ```text
    /// 2022-02-07T10:44:23.297571Z  INFO client_loop: twitch_irc::client::event_loop: Making a new pool connection, new ID is 0
    /// ```
    ///
    /// You may optionally set this configuration variable to some string, which will then
    /// modify all log messages by giving the `client_loop` span the `name` attribute:
    ///
    /// ```
    /// use std::borrow::Cow;
    /// use twitch_irc::ClientConfig;
    ///
    /// let mut config = ClientConfig::default();
    /// config.tracing_identifier = Some(Cow::Borrowed("bot_one"));
    /// ```
    ///
    /// All log output will then look like this (note the additional `{name=bot_one}`:
    ///
    /// ```text
    /// 2022-02-07T10:48:34.769272Z  INFO client_loop{name=bot_one}: twitch_irc::client::event_loop: Making a new pool connection, new ID is 0
    /// ```
    ///
    /// Essentially, this library makes use of `tracing` `Span`s to differentiate between
    /// different async tasks and to also differentiate log messages coming from different
    /// connections.
    ///
    /// Specifying this option will further allow you to differentiate between multiple
    /// clients if your application is running multiple of them. It will add the `name=your_value`
    /// attribute to the `client_loop` span, which is root for all further deeper spans in the
    /// client. This means that all log output from a single client will all be under that span,
    /// with that name.
    pub tracing_identifier: Option<Cow<'static, str>>,
}

/// Used to configure the options around metrics collection using the `prometheus` crate.
#[cfg(feature = "metrics-collection")]
#[derive(Debug)]
pub enum MetricsConfig {
    /// Metrics are not collected. Metrics are not registered with any registry.
    ///
    /// Useful if an application only requires monitoring on some of its running `twitch-irc`
    /// clients.
    Disabled,
    /// Metrics are collected. The metrics are immediately registered when
    /// [`TwitchIRCClient::new`](crate::TwitchIRCClient::new) is called.
    Enabled {
        /// Add these "constant labels" to all metrics produced by this client. This allows you
        /// to, for example, differentiate between multiple clients by naming them, or you
        /// may wish to place other relevant metadata pertaining to the whole client on all the
        /// metrics.
        ///
        /// This defaults to an empty map.
        constant_labels: HashMap<String, String>,
        /// Specifies what [`Registry`](prometheus::Registry) to register all metrics for this
        /// client with.
        ///
        /// Defaults to `None`, in which case the metrics are registered with the
        /// [global default registry of the `prometheus` crate](prometheus::default_registry).
        metrics_registry: Option<prometheus::Registry>,
    },
}

#[cfg(feature = "metrics-collection")]
impl Default for MetricsConfig {
    fn default() -> Self {
        MetricsConfig::Enabled {
            constant_labels: HashMap::new(),
            metrics_registry: None,
        }
    }
}

impl<L: LoginCredentials> ClientConfig<L> {
    /// Create a new configuration from the given login credentials, with all other configuration
    /// options being default.
    pub fn new_simple(login_credentials: L) -> ClientConfig<L> {
        ClientConfig {
            login_credentials,
            max_channels_per_connection: 90,

            max_waiting_messages_per_connection: 5,
            time_per_message: Duration::from_millis(150),

            // 1 connection every 2 seconds seems to work well
            connection_rate_limiter: Arc::new(Semaphore::new(1)),
            new_connection_every: Duration::from_secs(2),
            connect_timeout: Duration::from_secs(20),

            #[cfg(feature = "metrics-collection")]
            metrics_config: MetricsConfig::default(),
            tracing_identifier: None,
        }
    }
}

impl Default for ClientConfig<StaticLoginCredentials> {
    fn default() -> ClientConfig<StaticLoginCredentials> {
        ClientConfig::new_simple(StaticLoginCredentials::anonymous())
    }
}