Skip to content

Observability

TL;DR. wun.server.telemetry emits one structured event per significant operation (connect, intent, heartbeat, backpressure, upload, csrf miss, rate-limit block…) through a tiny sink protocol. Default sink writes to tools.logging at info level. Plug Prometheus / OpenTelemetry / your shop’s stack at startup.

Event vocabulary

Stable contract — adding a new event key is a public-API change.

:wun/connect {:conn-id :screen-key :caps :fmt :resumed?}
:wun/disconnect {:conn-id :reason}
:wun/intent.received {:conn-id :intent :id}
:wun/intent.applied {:conn-id :intent :id :duration-ms}
:wun/intent.rejected {:conn-id :intent :id :reason}
:wun/intent.dedup {:conn-id :intent :id}
:wun/intent.framework {:conn-id :intent :id :status}
:wun/heartbeat.tick {:active-conns}
:wun/heartbeat.miss {:conn-id :since-ms}
:wun/backpressure.drop {:conn-id}
:wun/backpressure.resync {:conn-id}
:wun/snapshot.resync {:conn-id :reason}
:wun/csrf.miss {:conn-id :reason}
:wun/rate-limit.block {:conn-id :ip :scope :limit}
:wun/upload.start {:conn-id :upload-id :size}
:wun/upload.progress {:conn-id :upload-id :received}
:wun/upload.complete {:conn-id :upload-id}
:wun/upload.error {:conn-id :upload-id :reason}
:wun/pubsub.publish {:topic :n-subscribers}
:wun/presence.join {:topic :conn-id}
:wun/presence.leave {:topic :conn-id}

Wiring Prometheus

(require '[wun.server.telemetry :as t]
'[iapetos.core :as p])
(def registry
(-> (p/collector-registry)
(p/register
(p/counter :wun/intents-total {:labels [:intent :status]})
(p/histogram :wun/intent-duration-ms {:labels [:intent]})
(p/gauge :wun/active-conns {})
(p/counter :wun/csrf-blocks {})
(p/counter :wun/rate-limit-blocks {:labels [:scope]}))))
(defn prometheus-sink [k attrs]
(case k
:wun/intent.applied
(do (p/inc registry :wun/intents-total
{:intent (str (:intent attrs)) :status "ok"})
(p/observe registry :wun/intent-duration-ms
{:intent (str (:intent attrs))} (:duration-ms attrs)))
:wun/intent.rejected
(p/inc registry :wun/intents-total
{:intent (str (:intent attrs)) :status "rejected"})
:wun/heartbeat.tick
(p/set registry :wun/active-conns {} (:active-conns attrs))
:wun/csrf.miss
(p/inc registry :wun/csrf-blocks {})
:wun/rate-limit.block
(p/inc registry :wun/rate-limit-blocks {:scope (str (:scope attrs))})
nil))
(t/register-sink! prometheus-sink)

iapetos is just an example — the contract is “any fn that takes (event-key, attrs-map)”; plug whatever client your shop uses.

Wiring OpenTelemetry

(require '[steffan-westcott.clj-otel.api.metrics.instrument :as inst])
(def intents-counter
(inst/instrument {:name "wun.intents.total"
:instrument-type :counter}))
(defn otel-sink [k attrs]
(when (= k :wun/intent.applied)
(inst/add! intents-counter
{:value 1
:attributes {:intent (str (:intent attrs))}})))
(wun.server.telemetry/register-sink! otel-sink)

Logging hardening

The default sink uses tools.logging which Wun bridges to logback-classic. The default config logs at INFO; production typically raises to WARN globally and unmutes wun.server.* to keep audit trails:

<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%logger{32}] %msg%n</pattern>
</encoder>
</appender>
<logger name="wun.server" level="INFO" />
<logger name="org.eclipse.jetty" level="WARN" />
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

What NOT to ship to your metrics backend

  • :state blobs from intent envelopes — they may contain user data.
  • Free-text reasons (e.g. :wun/csrf.miss :reason) — fine in logs (high cardinality, brief retention) but not as metric label values (low cardinality, long retention).
  • Filenames from :wun/upload.* events — log them, don’t aggregate them as labels.

The general rule: anything user-controlled goes in logs, not in metric labels.

  • Reconnect & retry — the events that fire during a connection lifecycle.
  • Security — what each :wun/csrf.miss and :wun/rate-limit.block event means operationally.