Documentation
¶
Index ¶
- Constants
- Variables
- func DeliverNow(text string) error
- func Host() string
- func HumanBytes(b int64) string
- func Label(lang, key string) string
- func LabelWith(lang, key string, params map[string]string) string
- func Paginate(msg string, limit int) []string
- func Publish(e Event)
- func Render(e Event, lang string) string
- func RenderClient(e Event, target ClientTarget, lang string) string
- func SendBackup(filename string, data []byte, caption string) error
- func Start(cfg func() Config)
- func Stop()
- func TestDeliver(cfg Config) error
- func Translate(table map[string]map[string]string, lang, key string, params map[string]string) string
- type Bus
- type ClientData
- type ClientTarget
- type Config
- type CoreData
- type Decision
- type Event
- type Kind
- type LoginData
- type MetricData
- type NodeData
- type Notifier
- type OutboundData
- type SMTPConfig
- type Suppressor
- type TelegramConfig
- type WebhookConfig
Constants ¶
const DefaultLang = "en"
DefaultLang is what an unset or unknown notifyLang falls back to.
Variables ¶
var AllKinds = []Kind{ NodeDown, NodeUp, CoreCrash, CoreUp, OutboundDown, OutboundUp, ClientDepleted, ClientExpiring, CPUHigh, MemoryHigh, LoginSuccess, LoginFailed, LoginBanned, }
AllKinds is the order the settings page lists the toggles in.
var Langs = []string{"en", "fa", "ru", "vi", "zhHans", "zhHant"}
Langs are the languages notifications can be written in, using the same keys as the panel's own locales (frontend/src/locales/index.ts) so the settings page can offer one list for both.
Functions ¶
func DeliverNow ¶
DeliverNow sends a pre-composed body to every configured channel, bypassing both the enabled-events filter and the suppressor.
The scheduled digests use it rather than Publish: they are not events. Each has its own schedule setting, which is already the operator's decision about how often to hear from it, and running them through the suppressor would mean a daily report silently swallowed by a cooldown.
func Host ¶
func Host() string
Host is the name that goes at the top of every message. Without it, an operator running more than one panel cannot tell which one is reporting.
func HumanBytes ¶
HumanBytes renders a byte count the way both halves of the Telegram integration show one.
Exported because service/tgbot needs the same rendering: the operator reads a client card from the bot and an expiry alert about that same client, and two copies of this would eventually disagree about the figure. Alongside Translate, Label, Paginate and Host, which are exported for the same reason.
func Label ¶
Label translates one alert-table key. The digest labels live in this table rather than the bot's because the scheduled report is an alert, and the two callers (that report and the bot's /status) must not word it differently.
func LabelWith ¶
LabelWith is Label for the few digest lines that carry a value, such as the count of names a truncated list dropped.
func Paginate ¶
Paginate splits a message into chunks no longer than limit runes, preferring to break at blank lines, then at line ends, and cutting mid-line only when a single line is itself too long.
That last fallback is the point of this function. 3x-ui splits on blank lines alone, so a long message that happens to contain none comes back unsplit and Telegram rejects the whole thing with "400: message is too long" -- which reads as the bot silently doing nothing. Any batch message here (a depletion pass naming every disabled client) is exactly that shape.
func Publish ¶
func Publish(e Event)
Publish is what every event source calls. It never blocks and never returns an error: a notification that cannot be delivered must not change what the caller does, and every caller here is a cron job or the login path.
func RenderClient ¶
func RenderClient(e Event, target ClientTarget, lang string) string
RenderClient turns an event into the message its own client is sent, which is a different message from the operator's rather than a translation of it: the operator is told which of their clients is running out, the client is told what has happened to their own account.
Nothing identifying the panel goes in -- no Host() prefix, no counts of other clients. This is the only notification that leaves the operator's own circle.
A kind with nothing to say to a client renders empty and the caller skips it, so adding a kind does not silently start messaging customers.
func SendBackup ¶
SendBackup uploads a database backup to the configured Telegram chats.
Telegram only: a webhook receiver has nowhere to put a file, and mailing a database around is a different decision from alerting.
func Start ¶
func Start(cfg func() Config)
Start brings the notifier up. It follows service.StartHub's shape: a package-level singleton with no-op entry points once it is down, so callers never have to check whether notifications are running before publishing.
func TestDeliver ¶
TestDeliver sends one event straight to every configured channel, skipping both the enabled-events filter and the suppressor.
It backs the settings page's "send a test message" button, which has to reach the operator even when nothing is switched on yet -- that is the whole point of pressing it. Without this button a wrong chat id fails silently, which is the most common support question these panels get.
It uses the saved settings, not whatever is in the form: the credentials are write-only, so the page has no token to submit even if it wanted to. Save first, then test.
func Translate ¶
func Translate(table map[string]map[string]string, lang, key string, params map[string]string) string
Translate looks key up in table[lang], falling back to English and finally to the key itself, then substitutes the {name} placeholders.
Exported because service/tgbot keeps its own table: the bot's wording is not the alert wording, and neither package should carry the other's strings. What should not be written twice is this -- the fallback rules and the placeholder syntax.
Falling back per key rather than per language matters: a key added in English but not yet translated should render in English everywhere, not turn the whole message into a bare identifier.
Types ¶
type Bus ¶
type Bus struct {
// contains filtered or unexported fields
}
Bus is an in-process fan-out: Publish never blocks, and every subscriber gets every event on its own goroutine.
The two-level queueing is the whole point. Senders here block on network I/O for as long as their timeout allows, and a single shared worker would let a Telegram call that is waiting on a dead connection hold up the Webhook delivery of the same alert -- which is exactly the delivery that was supposed to cover for Telegram being unreachable. Per-subscriber queues also bound the damage: a wedged subscriber fills its own 64 slots and drops, instead of backing up into the bus or spawning a goroutine per event.
Filtering is the subscriber's job. The bus does not know which events an operator enabled.
func NewBus ¶
func NewBus() *Bus
NewBus starts the dispatch loop. Callers own the returned bus and must Stop it.
func (*Bus) Publish ¶
Publish hands an event to the bus and returns immediately.
It is called from cron jobs and from the login path, so it must never block: a full buffer drops the event and logs it. Notifications are not a ledger -- losing one is better than stalling a login behind an unreachable Telegram.
func (*Bus) Stop ¶
func (b *Bus) Stop()
Stop shuts the bus down. Buffered and queued events may be dropped; handlers already running are waited for. After Stop, Subscribe is a no-op -- which is also what keeps its wg.Add from racing Wait, since both go through b.mu.
func (*Bus) Subscribe ¶
Subscribe registers handle under name, replacing any subscriber already using that name. Each subscriber is driven by its own worker, so handle is never called concurrently with itself and sees events in publication order.
func (*Bus) Unsubscribe ¶
Unsubscribe stops and drops the named subscriber. Unknown names are ignored, which is what lets the reload path unsubscribe unconditionally.
type ClientData ¶
type ClientData struct {
Names []string
// DaysLeft / BytesLeft are only set for ClientExpiring, and only the one
// that actually tripped is non-zero.
DaysLeft int
BytesLeft int64
// Targets are the clients this event is about that have a Telegram binding,
// so they can be warned directly as well as the operator. Empty whenever
// nobody involved has one, which is the common case -- the binding is
// optional and set through the bot, not the panel's client form.
//
// The operator's message is still rendered from the fields above; this only
// adds recipients, it does not change what the operator is told.
Targets []ClientTarget
}
ClientData accompanies ClientDepleted / ClientExpiring. Names is plural because DepleteJob disables a whole batch in one pass and sends one event for the batch rather than one per client.
type ClientTarget ¶
ClientTarget is one client to warn on their own Telegram chat, carrying the figures that client's message needs. They are repeated here rather than read off ClientData because a batched event (ClientDepleted) stands for many clients at once, each with its own numbers.
type Config ¶
type Config struct {
Enable bool
Proxy string
Lang string
Events map[Kind]bool
Telegram TelegramConfig
Webhook WebhookConfig
SMTP SMTPConfig
}
Config is the whole notification setup, read fresh for every event.
It is supplied as a callback rather than captured at Start, so a settings change takes effect on the next event with no reload step to remember. The events are rare enough (minutes apart at worst) that the extra settings read does not matter.
func (Config) Wants ¶
Wants reports whether the operator asked to hear about this kind.
Exported because an event source may be expensive enough to be worth skipping entirely: the outbound probe dials every outbound through the core, which is not something to do every five minutes for an alert nobody enabled.
type Decision ¶
type Decision struct {
Send bool
// Failures is how many attempts this single notification stands for, and
// is only set for LoginFailed. It is 1 for the first alert in a window and
// higher when attempts were folded into it.
Failures int
}
Decision is what the suppressor concluded about one event.
type Event ¶
type Event struct {
Kind Kind
// Subject is what the event is about: a node name, a client name, a source
// IP. It is half of the suppression key, so events about different subjects
// never suppress one another.
Subject string
// Data carries the kind-specific payload, one of the types below. Renderers
// type-assert it and must tolerate a nil or mismatched value rather than
// panicking -- an event source that forgets to attach it should degrade to
// a plainer message, not take the panel down.
Data any
// Text, when set, is delivered verbatim instead of rendering Kind. The
// scheduled digests use it: their body is assembled from client and node
// data this package cannot reach without depending on service, which would
// close an import cycle.
Text string
At time.Time
}
Event is one thing worth telling the operator about.
type Kind ¶
type Kind string
Kind identifies what happened.
The string form is what the notifyEvents setting stores, so these values are part of the panel's persisted configuration: renaming one silently turns that event off for every operator who had it enabled. Add new kinds, do not rename existing ones.
const ( // State events. Delivery is gated on a transition -- see Suppressor. NodeDown Kind = "node.down" NodeUp Kind = "node.up" CoreCrash Kind = "core.crash" CoreUp Kind = "core.up" OutboundDown Kind = "outbound.down" OutboundUp Kind = "outbound.up" // One-shot events, rate limited per subject. ClientDepleted Kind = "client.depleted" ClientExpiring Kind = "client.expiring" CPUHigh Kind = "cpu.high" MemoryHigh Kind = "memory.high" // Login events, each handled differently -- see Suppressor.Decide. LoginSuccess Kind = "login.success" LoginFailed Kind = "login.failed" LoginBanned Kind = "login.banned" )
type LoginData ¶
LoginData accompanies the three login kinds. Failures is the number of attempts folded into this notification, which is 1 for the first alert in a window and higher when the limiter merged some -- see Suppressor.Decide.
type MetricData ¶
MetricData accompanies CPUHigh / MemoryHigh.
type Notifier ¶
type Notifier struct {
// contains filtered or unexported fields
}
Notifier ties the suppressor and the bus to a config source.
type OutboundData ¶
OutboundData accompanies OutboundDown / OutboundUp. Separate from NodeData despite the identical shape: the two describe different things, and merging them would make a later field that only one of them has read as if it applied to both.
type SMTPConfig ¶
type Suppressor ¶
type Suppressor struct {
// contains filtered or unexported fields
}
Suppressor decides which events actually reach the channels.
It sits in front of the bus rather than inside each subscriber, so every channel is told about the same events -- an operator comparing their Telegram history against a webhook receiver should not find them disagreeing.
All of its state is in memory. A restart therefore re-arms everything: a still-relevant expiry warning goes out a second time, and a node that was already known down announces itself again. Persisting it would mean a SQLite write per event to save at most one duplicate message per restart, which is the wrong trade -- but it does mean event sources whose underlying condition repeats every few seconds (the core restart loop, node probes) must debounce on their own rather than relying on this to be the only guard.
func NewSuppressor ¶
func NewSuppressor() *Suppressor
type TelegramConfig ¶
type TelegramConfig struct {
Token string
ChatIDs []string
// APIServer overrides https://api.telegram.org, for operators running their
// own Bot API server. On a censored network that is a steadier route than
// an HTTP proxy, which still has to reach Telegram's own edge.
APIServer string
}
type WebhookConfig ¶
type WebhookConfig struct {
URL string
}