Settings > Sources: turn syslog reception on/off, choose UDP/TCP
- SyslogServer opens and closes the UDP/TCP listeners at runtime from the configuration saved in /data/syslog.json; a busy port no longer stops Logstream, the error is shown in Settings instead. - Sources tab: syslog section with an on/off switch, UDP and TCP labels, the live listening state and the published port (SYSLOG_PORT, passed as SYSLOG_PUBLIC_PORT for display; the mapping stays in docker-compose). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7beb791e44
commit
32593b9515
9 files changed
+278
-28
No files matched your search
@@ -28,6 +28,11 @@ Then open <http://localhost:8080>.
|
|||||||
- **Manual test**: `logger -n 127.0.0.1 -P 514 -d "hello error"` (util-linux) or
|
- **Manual test**: `logger -n 127.0.0.1 -P 514 -d "hello error"` (util-linux) or
|
||||||
`echo "<14>test ok" | nc -u -w1 127.0.0.1 514`.
|
`echo "<14>test ok" | nc -u -w1 127.0.0.1 514`.
|
||||||
|
|
||||||
|
**Settings > Sources > Syslog** turns syslog reception on or off and chooses the protocols
|
||||||
|
(UDP, TCP) without restarting; the choice is saved in `/data/syslog.json`. It also shows the
|
||||||
|
listening state (and the error if the port is already in use). The port itself is published
|
||||||
|
by docker-compose: change `SYSLOG_PORT` in `.env`, then run `docker compose up -d`.
|
||||||
|
|
||||||
Supported formats: RFC 3164 (BSD) and RFC 5424. Over TCP, both "one message per line"
|
Supported formats: RFC 3164 (BSD) and RFC 5424. Over TCP, both "one message per line"
|
||||||
and "octet counting" (RFC 6587) framing are accepted.
|
and "octet counting" (RFC 6587) framing are accepted.
|
||||||
|
|
||||||
@@ -222,6 +227,7 @@ To update one of them:
|
|||||||
| `rdns.go` | cached reverse DNS lookups |
|
| `rdns.go` | cached reverse DNS lookups |
|
||||||
| `export.go` | streamed CSV export |
|
| `export.go` | streamed CSV export |
|
||||||
| `docker.go` | Docker container logs (API, followers, positions, level detection) |
|
| `docker.go` | Docker container logs (API, followers, positions, level detection) |
|
||||||
|
| `syslogserver.go` | syslog listeners opened and closed from Settings > Sources |
|
||||||
| `tags.go` | color tag storage |
|
| `tags.go` | color tag storage |
|
||||||
| `api.go` | `/api/*` HTTP routes |
|
| `api.go` | `/api/*` HTTP routes |
|
||||||
| `web/` | UI (HTML, CSS, plain JavaScript, no build step), embedded in the binary; translations live in `web/app.js` (`I18N`) |
|
| `web/` | UI (HTML, CSS, plain JavaScript, no build step), embedded in the binary; translations live in `web/app.js` (`I18N`) |
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type API struct {
|
|||||||
allowPurge bool
|
allowPurge bool
|
||||||
exportMax int
|
exportMax int
|
||||||
docker *DockerManager // nil when DOCKER_LOGS is off
|
docker *DockerManager // nil when DOCKER_LOGS is off
|
||||||
|
syslog *SyslogServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) Routes(mux *http.ServeMux) {
|
func (a *API) Routes(mux *http.ServeMux) {
|
||||||
@@ -37,10 +38,32 @@ func (a *API) Routes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("GET /api/purge", a.purgeStatus)
|
mux.HandleFunc("GET /api/purge", a.purgeStatus)
|
||||||
mux.HandleFunc("POST /api/purge", a.purge)
|
mux.HandleFunc("POST /api/purge", a.purge)
|
||||||
mux.HandleFunc("GET /api/export.csv", a.exportCSV)
|
mux.HandleFunc("GET /api/export.csv", a.exportCSV)
|
||||||
|
mux.HandleFunc("GET /api/syslog", a.syslogStatus)
|
||||||
|
mux.HandleFunc("PUT /api/syslog", a.syslogConfigure)
|
||||||
mux.HandleFunc("GET /api/docker", a.dockerStatus)
|
mux.HandleFunc("GET /api/docker", a.dockerStatus)
|
||||||
mux.HandleFunc("PUT /api/docker", a.dockerConfigure)
|
mux.HandleFunc("PUT /api/docker", a.dockerConfigure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /api/syslog: syslog reception state (Settings > Sources).
|
||||||
|
func (a *API) syslogStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, http.StatusOK, a.syslog.Status())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/syslog {"enabled": bool, "udp": bool, "tcp": bool}
|
||||||
|
func (a *API) syslogConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var cfg syslogConfig
|
||||||
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&cfg); err != nil {
|
||||||
|
writeErr(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := a.syslog.Configure(cfg); err != nil {
|
||||||
|
writeErr(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("syslog reception changed from %s: %+v", r.RemoteAddr, cfg)
|
||||||
|
writeJSON(w, http.StatusOK, a.syslog.Status())
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/docker: Docker source state and containers (Settings > Sources).
|
// GET /api/docker: Docker source state and containers (Settings > Sources).
|
||||||
func (a *API) dockerStatus(w http.ResponseWriter, r *http.Request) {
|
func (a *API) dockerStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
if a.docker == nil {
|
if a.docker == nil {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ services:
|
|||||||
- "${HTTP_PORT:-8080}:8080"
|
- "${HTTP_PORT:-8080}:8080"
|
||||||
environment:
|
environment:
|
||||||
VLOGS_URL: http://victorialogs:9428
|
VLOGS_URL: http://victorialogs:9428
|
||||||
|
SYSLOG_PUBLIC_PORT: ${SYSLOG_PORT:-514} # shown in Settings > Sources (the mapping is set above)
|
||||||
TZ: ${TZ:-Europe/Paris}
|
TZ: ${TZ:-Europe/Paris}
|
||||||
AUTH_USER: ${AUTH_USER:-} # leave empty to disable authentication
|
AUTH_USER: ${AUTH_USER:-} # leave empty to disable authentication
|
||||||
AUTH_PASS: ${AUTH_PASS:-}
|
AUTH_PASS: ${AUTH_PASS:-}
|
||||||
|
|||||||
@@ -120,16 +120,15 @@ func main() {
|
|||||||
store.Enqueue(e)
|
store.Enqueue(e)
|
||||||
hub.Publish(e)
|
hub.Publish(e)
|
||||||
}
|
}
|
||||||
if err := StartSyslog(ctx, cfg.syslogAddr, sink); err != nil {
|
// Listening errors (port already used…) are shown in Settings > Sources.
|
||||||
log.Fatalf("syslog: %v", err)
|
syslogSrv := NewSyslogServer(ctx, cfg.syslogAddr, getenv("SYSLOG_PUBLIC_PORT", ""), cfg.dataDir, sink)
|
||||||
}
|
|
||||||
|
|
||||||
static, err := fs.Sub(webFS, "web")
|
static, err := fs.Sub(webFS, "web")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
api := &API{store: store, hub: hub, tags: tags, rdns: rdns, allowPurge: cfg.allowPurge, exportMax: cfg.exportMax}
|
api := &API{store: store, hub: hub, tags: tags, rdns: rdns, allowPurge: cfg.allowPurge, exportMax: cfg.exportMax, syslog: syslogSrv}
|
||||||
if cfg.dockerLogs {
|
if cfg.dockerLogs {
|
||||||
dm, err := NewDockerManager(cfg.dockerHost, cfg.dataDir, cfg.backfill, sink)
|
dm, err := NewDockerManager(cfg.dockerHost, cfg.dataDir, cfg.backfill, sink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -225,30 +225,6 @@ func parseHostTag(e *Entry, s string, withHost bool) {
|
|||||||
e.Message = s
|
e.Message = s
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartSyslog starts the UDP and TCP listeners on the same address.
|
|
||||||
func StartSyslog(ctx context.Context, addr string, sink func(*Entry)) error {
|
|
||||||
pc, err := net.ListenPacket("udp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ln, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
pc.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if uc, ok := pc.(*net.UDPConn); ok {
|
|
||||||
_ = uc.SetReadBuffer(8 << 20)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
pc.Close()
|
|
||||||
ln.Close()
|
|
||||||
}()
|
|
||||||
go serveUDP(ctx, pc, sink)
|
|
||||||
go serveTCP(ctx, ln, sink)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveUDP(ctx context.Context, pc net.PacketConn, sink func(*Entry)) {
|
func serveUDP(ctx context.Context, pc net.PacketConn, sink func(*Entry)) {
|
||||||
buf := make([]byte, 65536)
|
buf := make([]byte, 65536)
|
||||||
for {
|
for {
|
||||||
|
|||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// syslogConfig is saved in /data/syslog.json and edited in Settings > Sources.
|
||||||
|
type syslogConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
UDP bool `json:"udp"`
|
||||||
|
TCP bool `json:"tcp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyslogServer owns the UDP and TCP listeners and opens or closes them when
|
||||||
|
// the configuration changes, without restarting Logstream.
|
||||||
|
type SyslogServer struct {
|
||||||
|
parent context.Context
|
||||||
|
addr string // listening address inside the container, e.g. ":5514"
|
||||||
|
publicPort string // port published on the host by docker-compose (display only)
|
||||||
|
sink func(*Entry)
|
||||||
|
cfgPath string
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
cfg syslogConfig
|
||||||
|
cancel context.CancelFunc
|
||||||
|
pc net.PacketConn
|
||||||
|
ln net.Listener
|
||||||
|
errUDP string
|
||||||
|
errTCP string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSyslogServer(ctx context.Context, addr, publicPort, dataDir string, sink func(*Entry)) *SyslogServer {
|
||||||
|
s := &SyslogServer{
|
||||||
|
parent: ctx,
|
||||||
|
addr: addr,
|
||||||
|
publicPort: publicPort,
|
||||||
|
sink: sink,
|
||||||
|
cfgPath: filepath.Join(dataDir, "syslog.json"),
|
||||||
|
cfg: syslogConfig{Enabled: true, UDP: true, TCP: true},
|
||||||
|
}
|
||||||
|
if b, err := os.ReadFile(s.cfgPath); err == nil {
|
||||||
|
var cfg syslogConfig
|
||||||
|
if err := json.Unmarshal(b, &cfg); err == nil {
|
||||||
|
s.cfg = cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.apply()
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
s.mu.Lock()
|
||||||
|
s.closeLocked()
|
||||||
|
s.mu.Unlock()
|
||||||
|
}()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeLocked stops the listeners synchronously, so that the port is free
|
||||||
|
// before it is opened again; the caller holds the lock.
|
||||||
|
func (s *SyslogServer) closeLocked() {
|
||||||
|
if s.cancel != nil {
|
||||||
|
s.cancel() // also closes the open TCP connections
|
||||||
|
s.cancel = nil
|
||||||
|
}
|
||||||
|
if s.pc != nil {
|
||||||
|
s.pc.Close()
|
||||||
|
s.pc = nil
|
||||||
|
}
|
||||||
|
if s.ln != nil {
|
||||||
|
s.ln.Close()
|
||||||
|
s.ln = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply opens the listeners required by the configuration.
|
||||||
|
func (s *SyslogServer) apply() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.closeLocked()
|
||||||
|
s.errUDP, s.errTCP = "", ""
|
||||||
|
if !s.cfg.Enabled || s.parent.Err() != nil {
|
||||||
|
log.Printf("syslog: reception disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(s.parent)
|
||||||
|
s.cancel = cancel
|
||||||
|
var open []string
|
||||||
|
if s.cfg.UDP {
|
||||||
|
pc, err := net.ListenPacket("udp", s.addr)
|
||||||
|
if err != nil {
|
||||||
|
s.errUDP = err.Error()
|
||||||
|
log.Printf("syslog udp: %v", err)
|
||||||
|
} else {
|
||||||
|
if uc, ok := pc.(*net.UDPConn); ok {
|
||||||
|
_ = uc.SetReadBuffer(8 << 20)
|
||||||
|
}
|
||||||
|
s.pc = pc
|
||||||
|
go serveUDP(ctx, pc, s.sink)
|
||||||
|
open = append(open, "udp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.cfg.TCP {
|
||||||
|
ln, err := net.Listen("tcp", s.addr)
|
||||||
|
if err != nil {
|
||||||
|
s.errTCP = err.Error()
|
||||||
|
log.Printf("syslog tcp: %v", err)
|
||||||
|
} else {
|
||||||
|
s.ln = ln
|
||||||
|
go serveTCP(ctx, ln, s.sink)
|
||||||
|
open = append(open, "tcp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("syslog: listening on %s %v", s.addr, open)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure saves and applies a new configuration.
|
||||||
|
func (s *SyslogServer) Configure(cfg syslogConfig) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.cfg = cfg
|
||||||
|
s.mu.Unlock()
|
||||||
|
s.apply()
|
||||||
|
return writeJSONFile(s.cfgPath, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status is the state shown in Settings > Sources.
|
||||||
|
func (s *SyslogServer) Status() map[string]any {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return map[string]any{
|
||||||
|
"enabled": s.cfg.Enabled,
|
||||||
|
"udp": s.cfg.UDP,
|
||||||
|
"tcp": s.cfg.TCP,
|
||||||
|
"udpListening": s.pc != nil,
|
||||||
|
"tcpListening": s.ln != nil,
|
||||||
|
"udpError": s.errUDP,
|
||||||
|
"tcpError": s.errTCP,
|
||||||
|
"port": s.publicPort,
|
||||||
|
"internalAddr": s.addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -67,6 +67,12 @@ const I18N = {
|
|||||||
fHostName: 'host name (DNS)', fHostIp: 'host IP',
|
fHostName: 'host name (DNS)', fHostIp: 'host IP',
|
||||||
clickHost: 'Click to filter on this host', clickApp: 'Click to filter on this app',
|
clickHost: 'Click to filter on this host', clickApp: 'Click to filter on this app',
|
||||||
sourceAria: 'Source', srcAll: 'All sources', tabSources: 'Sources',
|
sourceAria: 'Source', srcAll: 'All sources', tabSources: 'Sources',
|
||||||
|
syslogEnabled: 'Receive syslog messages', syslogProtocols: 'Protocols', syslogPort: 'Port',
|
||||||
|
syslogListening: ({ port, protos }) => `Listening on port ${port} (${protos}).`,
|
||||||
|
syslogOff: 'Syslog reception is off: syslog messages are ignored (Docker logs are still collected).',
|
||||||
|
syslogNone: 'No protocol selected: syslog messages are ignored.',
|
||||||
|
syslogErr: (p) => `Cannot listen over ${p}: `,
|
||||||
|
syslogPortHelp: (i) => `Published by docker-compose (container port ${i}). To change it, set SYSLOG_PORT in .env, then run docker compose up -d.`,
|
||||||
dockerTitle: 'Docker containers',
|
dockerTitle: 'Docker containers',
|
||||||
dockerDefault: 'Follow new containers automatically',
|
dockerDefault: 'Follow new containers automatically',
|
||||||
dockerAll: 'Enable all', dockerNone: 'Disable all',
|
dockerAll: 'Enable all', dockerNone: 'Disable all',
|
||||||
@@ -171,6 +177,12 @@ const I18N = {
|
|||||||
fHostName: 'nom d\'hôte (DNS)', fHostIp: 'IP de l\'hôte',
|
fHostName: 'nom d\'hôte (DNS)', fHostIp: 'IP de l\'hôte',
|
||||||
clickHost: 'Cliquer pour filtrer sur cet hôte', clickApp: 'Cliquer pour filtrer sur cette appli',
|
clickHost: 'Cliquer pour filtrer sur cet hôte', clickApp: 'Cliquer pour filtrer sur cette appli',
|
||||||
sourceAria: 'Source', srcAll: 'Toutes les sources', tabSources: 'Sources',
|
sourceAria: 'Source', srcAll: 'Toutes les sources', tabSources: 'Sources',
|
||||||
|
syslogEnabled: 'Recevoir les messages syslog', syslogProtocols: 'Protocoles', syslogPort: 'Port',
|
||||||
|
syslogListening: ({ port, protos }) => `Écoute sur le port ${port} (${protos}).`,
|
||||||
|
syslogOff: 'Réception syslog désactivée : les messages syslog sont ignorés (les logs Docker sont toujours collectés).',
|
||||||
|
syslogNone: 'Aucun protocole sélectionné : les messages syslog sont ignorés.',
|
||||||
|
syslogErr: (p) => `Impossible d'écouter en ${p} : `,
|
||||||
|
syslogPortHelp: (i) => `Publié par docker-compose (port ${i} dans le conteneur). Pour le changer, modifiez SYSLOG_PORT dans .env, puis lancez docker compose up -d.`,
|
||||||
dockerTitle: 'Conteneurs Docker',
|
dockerTitle: 'Conteneurs Docker',
|
||||||
dockerDefault: 'Suivre automatiquement les nouveaux conteneurs',
|
dockerDefault: 'Suivre automatiquement les nouveaux conteneurs',
|
||||||
dockerAll: 'Tout activer', dockerNone: 'Tout désactiver',
|
dockerAll: 'Tout activer', dockerNone: 'Tout désactiver',
|
||||||
@@ -395,6 +407,7 @@ function setLang(next) {
|
|||||||
renderTimeSettings();
|
renderTimeSettings();
|
||||||
renderPurge();
|
renderPurge();
|
||||||
renderInterface();
|
renderInterface();
|
||||||
|
renderSyslog();
|
||||||
renderDocker();
|
renderDocker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1095,6 +1108,66 @@ $('#exportMenu').addEventListener('click', (ev) => {
|
|||||||
document.addEventListener('click', (ev) => { if (!ev.target.closest('.export')) setExportMenu(false); });
|
document.addEventListener('click', (ev) => { if (!ev.target.closest('.export')) setExportMenu(false); });
|
||||||
document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape') setExportMenu(false); });
|
document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape') setExportMenu(false); });
|
||||||
|
|
||||||
|
/* ================= Syslog source ================= */
|
||||||
|
|
||||||
|
let syslogState = null;
|
||||||
|
|
||||||
|
async function loadSyslog() {
|
||||||
|
try { syslogState = await api('/api/syslog'); } catch { syslogState = null; }
|
||||||
|
renderSyslog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSyslog() {
|
||||||
|
const s = syslogState;
|
||||||
|
const status = $('#syslogStatus');
|
||||||
|
if (!s) { status.textContent = ''; return; }
|
||||||
|
$('#syslogEnabled').checked = s.enabled;
|
||||||
|
for (const b of document.querySelectorAll('#syslogProtos [data-proto]')) {
|
||||||
|
const on = !!s[b.dataset.proto];
|
||||||
|
b.classList.toggle('on', on);
|
||||||
|
b.setAttribute('aria-pressed', String(on));
|
||||||
|
b.disabled = !s.enabled;
|
||||||
|
}
|
||||||
|
$('#syslogProtos').classList.toggle('off', !s.enabled);
|
||||||
|
$('#syslogPortVal').textContent = s.port || s.internalAddr;
|
||||||
|
const internal = String(s.internalAddr || '').replace(/^:/, '');
|
||||||
|
$('#syslogPortHelp').textContent = t('syslogPortHelp', internal);
|
||||||
|
|
||||||
|
const errs = [];
|
||||||
|
if (s.udpError) errs.push(t('syslogErr', 'UDP') + s.udpError);
|
||||||
|
if (s.tcpError) errs.push(t('syslogErr', 'TCP') + s.tcpError);
|
||||||
|
const protos = [s.udpListening && 'UDP', s.tcpListening && 'TCP'].filter(Boolean).join(' + ');
|
||||||
|
if (errs.length) {
|
||||||
|
status.textContent = errs.join(' · ');
|
||||||
|
status.className = 'docker-status bad';
|
||||||
|
} else if (!s.enabled) {
|
||||||
|
status.textContent = t('syslogOff');
|
||||||
|
status.className = 'docker-status muted';
|
||||||
|
} else if (!protos) {
|
||||||
|
status.textContent = t('syslogNone');
|
||||||
|
status.className = 'docker-status muted';
|
||||||
|
} else {
|
||||||
|
status.textContent = t('syslogListening', { port: s.port || internal, protos });
|
||||||
|
status.className = 'docker-status good';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configureSyslog(change) {
|
||||||
|
const s = syslogState || { enabled: true, udp: true, tcp: true };
|
||||||
|
const body = { enabled: s.enabled, udp: s.udp, tcp: s.tcp, ...change };
|
||||||
|
syslogState = { ...s, ...body };
|
||||||
|
renderSyslog();
|
||||||
|
try { syslogState = await api('/api/syslog', { method: 'PUT', body }); } catch (e) { toast(e.message); }
|
||||||
|
renderSyslog();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#syslogEnabled').addEventListener('change', (ev) => configureSyslog({ enabled: ev.target.checked }));
|
||||||
|
$('#syslogProtos').addEventListener('click', (ev) => {
|
||||||
|
const b = ev.target.closest('[data-proto]');
|
||||||
|
if (!b || b.disabled || !syslogState) return;
|
||||||
|
configureSyslog({ [b.dataset.proto]: !syslogState[b.dataset.proto] });
|
||||||
|
});
|
||||||
|
|
||||||
/* ================= Docker source ================= */
|
/* ================= Docker source ================= */
|
||||||
|
|
||||||
let dockerState = null;
|
let dockerState = null;
|
||||||
@@ -1370,6 +1443,7 @@ $('#settingsBtn').addEventListener('click', () => {
|
|||||||
renderTagList();
|
renderTagList();
|
||||||
renderInterface();
|
renderInterface();
|
||||||
loadPurgeStatus();
|
loadPurgeStatus();
|
||||||
|
loadSyslog();
|
||||||
loadDocker();
|
loadDocker();
|
||||||
showSettingsTab(store.get('settingsTab', 'locale'));
|
showSettingsTab(store.get('settingsTab', 'locale'));
|
||||||
$('#settingsDlg').showModal();
|
$('#settingsDlg').showModal();
|
||||||
|
|||||||
@@ -178,6 +178,24 @@
|
|||||||
|
|
||||||
<!-- Sources -->
|
<!-- Sources -->
|
||||||
<div class="set-panel" role="tabpanel" data-panel="sources" hidden>
|
<div class="set-panel" role="tabpanel" data-panel="sources" hidden>
|
||||||
|
<section class="set-section">
|
||||||
|
<h3>Syslog</h3>
|
||||||
|
<p id="syslogStatus" class="docker-status"></p>
|
||||||
|
<label class="switch-row">
|
||||||
|
<input id="syslogEnabled" type="checkbox" class="switch">
|
||||||
|
<span data-i18n="syslogEnabled">Receive syslog messages</span>
|
||||||
|
</label>
|
||||||
|
<div class="sl-grid">
|
||||||
|
<span class="lbl" data-i18n="syslogProtocols">Protocols</span>
|
||||||
|
<div id="syslogProtos" class="chip-field sl-protos">
|
||||||
|
<button type="button" class="chip" data-proto="udp" style="--h:205">UDP</button>
|
||||||
|
<button type="button" class="chip" data-proto="tcp" style="--h:265">TCP</button>
|
||||||
|
</div>
|
||||||
|
<span class="lbl" data-i18n="syslogPort">Port</span>
|
||||||
|
<div class="sl-port"><code id="syslogPortVal">514</code><span id="syslogPortHelp" class="muted small"></span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="set-section">
|
<section class="set-section">
|
||||||
<h3 data-i18n="dockerTitle">Docker containers</h3>
|
<h3 data-i18n="dockerTitle">Docker containers</h3>
|
||||||
<p id="dockerStatus" class="docker-status"></p>
|
<p id="dockerStatus" class="docker-status"></p>
|
||||||
|
|||||||
@@ -426,6 +426,14 @@ select.field:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0
|
|||||||
.docker-status.bad { color: var(--sev-err); }
|
.docker-status.bad { color: var(--sev-err); }
|
||||||
.switch-row { display: flex; align-items: center; gap: 10px; font-size: 13px; font-weight: 550; cursor: pointer; }
|
.switch-row { display: flex; align-items: center; gap: 10px; font-size: 13px; font-weight: 550; cursor: pointer; }
|
||||||
.dk-opts { display: flex; flex-direction: column; gap: 8px; }
|
.dk-opts { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.sl-grid { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 12px 14px; align-items: baseline; margin-top: 14px; }
|
||||||
|
.sl-protos { display: inline-flex; min-height: 0; padding: 5px; justify-self: start; }
|
||||||
|
.sl-protos.off { opacity: .5; }
|
||||||
|
.sl-port { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; }
|
||||||
|
.sl-port code {
|
||||||
|
font: 12px var(--mono); padding: 2px 8px; border-radius: 5px;
|
||||||
|
background: var(--panel-2); border: 1px solid var(--border);
|
||||||
|
}
|
||||||
.dk-toolbar { display: flex; align-items: center; gap: 12px; margin: 14px 0 6px; }
|
.dk-toolbar { display: flex; align-items: center; gap: 12px; margin: 14px 0 6px; }
|
||||||
.dk-filter {
|
.dk-filter {
|
||||||
flex: 1; min-width: 0; height: 28px; padding: 0 10px;
|
flex: 1; min-width: 0; height: 28px; padding: 0 10px;
|
||||||
|
|||||||
Reference in new issue
Block a user