first commit

This commit is contained in:
Adrian Zuercher
2025-08-23 22:22:22 +02:00
commit e36d393749
14 changed files with 1555 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

27
README.md Normal file
View File

@@ -0,0 +1,27 @@
# StatusServer
A lightweight **pub/sub server** built on WebSockets.
It is designed to broadcast **status updates** and **error messages** to connected clients in real-time.
No database, no persistence — just simple and fast messaging.
---
## 🚀 Features
- **Real-time updates** over WebSockets
- **Pub/Sub model**: subscribe to topics and receive broadcasts
- **Zero persistence**: messages are ephemeral and only delivered to connected clients
- **Lightweight**: no external dependencies beyond WebSockets
---
## 🗂️ Architecture
```
Client <--> StatusServer
^ |
| |-- publish(info/status/warning/error)
|<-- broadcasted messages
```
- **Publish**: send a message (info, status, warning or error) to the server.
- **Broadcast**: server pushes it to all connected subscribers.

45
go.mod Normal file
View File

@@ -0,0 +1,45 @@
module statusServer
go 1.24.0
require (
gitea.tecamino.com/paadi/pubSub v1.0.1
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1
gitea.tecamino.com/paadi/tecamino-json_data v0.1.0
gitea.tecamino.com/paadi/tecamino-logger v0.2.1
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/gorilla/websocket v1.5.3
)
require (
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

111
go.sum Normal file
View File

@@ -0,0 +1,111 @@
gitea.tecamino.com/paadi/pubSub v1.0.1 h1:Of91JAVaiaqXhzFjm+jwSJipPbST+A1WQz1DEEhLQm4=
gitea.tecamino.com/paadi/pubSub v1.0.1/go.mod h1:SBPTSD/JQWRbwqsSNoSPhV81IDTreP0TMyvLhmK3P2M=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 h1:vAq7mwUxlxJuLzCQSDMrZCwo8ky5usWi9Qz+UP+WnkI=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk=
gitea.tecamino.com/paadi/tecamino-json_data v0.1.0 h1:zp3L8qUvkVqzuesQdMh/SgZZZbX3pGD9NYa6jtz+JvA=
gitea.tecamino.com/paadi/tecamino-json_data v0.1.0/go.mod h1:/FKhbVYuhiNlQp4552rJJQIhynjLarDzfrgXpupkwZU=
gitea.tecamino.com/paadi/tecamino-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE=
gitea.tecamino.com/paadi/tecamino-logger v0.2.1/go.mod h1:FkzRTldUBBOd/iy2upycArDftSZ5trbsX5Ira5OzJgM=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

125
handlers/clientHandler.go Normal file
View File

@@ -0,0 +1,125 @@
package handlers
import (
"encoding/json"
"fmt"
"statusServer/models"
"sync"
json_dataModels "gitea.tecamino.com/paadi/tecamino-json_data/models"
"github.com/gin-gonic/gin"
)
// ClientHandler manages active websocket clients.
// - Keeps a map of connected clients (thread-safe).
// - Provides helper methods to connect, lookup, and send responses.
type ClientHandler struct {
sync.RWMutex // protects access to Clients map
clients models.Clients // map[string]*Client (id → client)
}
// SendBroadcast sends a raw message to all clients in the global Broadcast list.
// Note: uses `models.Broadcast` (separate global client slice).
func SendBroadcast(data []byte) {
for _, c := range models.Broadcast {
c.SendData(data)
}
}
// NewConnectionHandler creates and initializes a new ClientHandler
// with an empty client map.
func NewConnectionHandler() *ClientHandler {
return &ClientHandler{
clients: models.NewClients(),
}
}
// ConnectNewClient registers a new websocket client by ID.
// - If client with same ID already exists, returns the existing client.
// - Otherwise, creates a new connection and stores it in the handler map.
func (cH *ClientHandler) ConnectNewClient(id string, c *gin.Context) (client *models.Client, err error) {
// Check if client already exists (reuse connection).
if _, exists := cH.clients[id]; exists {
return cH.clients[id], nil
}
// Create a new websocket client instance.
client, err = models.ConnectNewClient(id, c)
if err != nil {
return nil, err
}
// Ensure cleanup: when client disconnects, remove from map.
client.OnClose = func(code int, reason string) {
delete(cH.clients, id)
}
// Add client to handler map.
cH.Lock()
cH.clients[id] = client
cH.Unlock()
return client, nil
}
// GetClient retrieves a client by ID.
// Returns nil if client does not exist.
func (c *ClientHandler) GetClient(id string) *models.Client {
if client, ok := c.clients[id]; ok {
return client
}
return nil
}
func (cH *ClientHandler) RemoveClient(id string) {
delete(cH.clients, id)
}
// SendResponse sends a structured JSON response to a given client by ID.
// - If client is not found, returns error.
// - Otherwise marshals the Response and sends via websocket.
func (c *ClientHandler) SendResponse(id string, r *json_dataModels.Response) error {
client, ok := c.clients[id]
if !ok {
return fmt.Errorf("client not found for id %s", id)
}
// Encode response to JSON.
b, err := json.Marshal(*r)
if err != nil {
return err
}
// Send JSON payload over websocket.
client.SendData(b)
return nil
}
// --- Sending helpers (proxy to server messaging system) ---
// SendInfo sends an "info" log message to the server.
func (c *ClientHandler) SendInfo(id string, data any) error {
return c.clients[id].SendInfo(data)
}
// SendInfo sends an "info" log message to the server.
func (c *ClientHandler) SendStatus(id string, data any) error {
return c.clients[id].SendStatus(data)
}
// SendDebug sends a "debug" log message to the server.
func (c *ClientHandler) SendDebug(id string, data any) error {
return c.clients[id].SendDebug(data)
}
// SendWarning sends a "warning" log message to the server.
func (c *ClientHandler) SendWarning(id string, data any) error {
return c.clients[id].SendWarning(data)
}
// SendError sends an "error" log message to the server.
func (c *ClientHandler) SendError(id string, data any) error {
return c.clients[id].SendError(data)
}

354
models/client.go Normal file
View File

@@ -0,0 +1,354 @@
package models
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"slices"
"time"
pubSubModels "gitea.tecamino.com/paadi/pubSub/models"
"gitea.tecamino.com/paadi/tecamino-logger/logging"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var Origins []string = []string{"*"}
var Broadcast Clients = make(Clients)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 30 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
)
type Client struct {
id string
connected bool
conn *websocket.Conn `json:"-"`
OnOpen func()
OnMessage func(data []byte)
OnClose func(code int, reason string)
OnError func(err error)
OnWarning func(warn string)
OnPing func()
OnPong func()
send chan []byte
unregister chan []byte
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if len(Origins) == 0 {
return false
} else if Origins[0] == "*" {
return true
}
return slices.Contains(Origins, r.Header.Get("Origin"))
},
EnableCompression: false,
}
func NewClient(ip, id string, port uint, logger *logging.Logger) (*Client, error) {
u := url.URL{Scheme: "ws", Host: fmt.Sprintf("%s:%d", ip, port), Path: "status", RawQuery: "id=" + id}
c := &Client{
id: id,
connected: true,
send: make(chan []byte, 512),
}
dialer := websocket.DefaultDialer
conn, resp, err := dialer.Dial(u.String(), nil)
if err != nil {
if c.OnError != nil {
c.OnError(err)
}
return nil, fmt.Errorf("dial error %v (status %v)", err, resp)
}
c.conn = conn
//Setup control handlers
logger.Debug("NewClient", "set PingHandler")
conn.SetPingHandler(func(appData string) error {
if c.OnPing != nil {
c.OnPing()
}
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetWriteDeadline(time.Now().Add(pongWait))
if err := conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(2*pongWait)); err != nil {
c.OnError(err)
return err
}
return nil
})
logger.Debug("NewClient", "set PongHandler")
conn.SetPongHandler(func(appData string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
if c.OnPong != nil {
c.OnPong()
}
return nil
})
// Start reading messages from client
logger.Debug("NewClient", "start read goroutine")
go c.Read()
logger.Debug("NewClient", "start write goroutine")
go c.Write()
return c, nil
}
func ConnectNewClient(id string, c *gin.Context) (*Client, error) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return nil, fmt.Errorf("websocket upgrade error: %w", err)
}
client := &Client{
id: id,
connected: true,
conn: conn,
send: make(chan []byte, 512),
unregister: make(chan []byte, 256),
}
Broadcast[client.id] = client
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPingHandler(func(appData string) error {
if client.OnPing != nil {
client.OnPing()
}
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := client.conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(pongWait)); err != nil {
client.OnError(err)
}
return nil
})
conn.SetPongHandler(func(appData string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
if client.OnPong != nil {
client.OnPong()
}
return nil
})
// Start reading messages from client
go client.Read()
go client.Write()
go client.PingInterval(pingPeriod)
return client, nil
}
func (c *Client) Read() {
if c.OnOpen != nil {
c.OnOpen()
}
c.conn.SetReadDeadline(time.Now().Add(writeWait))
for c.connected {
msgType, msg, err := c.conn.ReadMessage()
if err != nil {
c.handleError(fmt.Errorf("read error (id:%s): %w", c.id, err))
return
}
switch msgType {
case websocket.CloseMessage:
c.Close(websocket.CloseNormalClosure, "Client closed")
return
case websocket.TextMessage:
if c.OnMessage != nil {
c.OnMessage(msg)
} else {
log.Printf("Received message but no handler set (id:%s): %s", c.id, string(msg))
}
default:
log.Printf("Unhandled message type %d (id:%s)", msgType, c.id)
}
}
}
func (c *Client) Write() {
defer c.conn.Close()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel.
if err := c.conn.WriteMessage(websocket.CloseMessage, []byte("ping")); err != nil {
c.handleError(err)
return
}
c.handleError(fmt.Errorf("server %s closed channel", c.id))
return
} else {
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
c.handleError(err)
return
}
}
case message := <-c.unregister:
c.conn.WriteMessage(websocket.CloseMessage, message)
c.connected = false
close(c.send)
delete(Broadcast, c.id)
close(c.unregister)
return
}
}
}
func (c *Client) PingInterval(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
if err := c.conn.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(pongWait)); err != nil {
c.OnError(err)
return
}
for range ticker.C {
if c.OnPing != nil {
c.OnPing()
}
if err := c.conn.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(pongWait)); err != nil {
c.OnError(err)
return
}
}
}
func (c *Client) SendData(data []byte) {
if !c.connected {
return
}
select {
case c.send <- data:
// sent successfully
default:
// channel full, drop or log
if c.OnWarning != nil {
c.OnWarning("Dropping message: channel full")
}
}
}
func (c *Client) Close(code int, reason string) error {
closeMsg := websocket.FormatCloseMessage(code, reason)
select {
case c.unregister <- closeMsg: // Attempt to send
default: // If the channel is full, this runs
return fmt.Errorf("attempt close client socket failed")
}
if c.OnClose != nil {
c.OnClose(code, reason)
}
return nil
}
func (c *Client) handleError(err error) {
if c.OnError != nil {
c.OnError(err)
}
if err := c.Close(websocket.CloseInternalServerErr, err.Error()); err != nil {
if c.OnError != nil {
c.OnError(err)
} else {
fmt.Println("error: ", err)
}
}
}
func (c *Client) SendInfo(data any) error {
var payload pubSubModels.Data
payload.Action = "publish"
payload.Topic = fmt.Sprintf("%s/info", c.id)
payload.Data = data
b, err := json.Marshal(payload)
if err != nil {
return err
}
c.SendData(b)
return nil
}
func (c *Client) SendStatus(data any) error {
var payload pubSubModels.Data
payload.Action = "publish"
payload.Topic = fmt.Sprintf("%s/status", c.id)
payload.Data = data
b, err := json.Marshal(payload)
if err != nil {
return err
}
c.SendData(b)
return nil
}
func (c *Client) SendDebug(data any) error {
var payload pubSubModels.Data
payload.Action = "publish"
payload.Topic = fmt.Sprintf("%s/debug", c.id)
payload.Data = data
b, err := json.Marshal(payload)
if err != nil {
return err
}
c.SendData(b)
return nil
}
func (c *Client) SendWarning(data any) error {
var payload pubSubModels.Data
payload.Action = "publish"
payload.Topic = fmt.Sprintf("%s/warning", c.id)
payload.Data = data
b, err := json.Marshal(payload)
if err != nil {
return err
}
c.SendData(b)
return nil
}
func (c *Client) SendError(data any) error {
var payload pubSubModels.Data
payload.Action = "publish"
payload.Topic = fmt.Sprintf("%s/error", c.id)
payload.Data = data
b, err := json.Marshal(payload)
if err != nil {
return err
}
c.SendData(b)
return nil
}

7
models/clients.go Normal file
View File

@@ -0,0 +1,7 @@
package models
type Clients map[string]*Client
func NewClients() Clients {
return make(Clients)
}

42
models/config.go Normal file
View File

@@ -0,0 +1,42 @@
package models
import (
"gitea.tecamino.com/paadi/tecamino-logger/logging"
)
type Config struct {
AllowOrigins []string
Ip string
Port int
Log logging.Config
PubSub PubSub
}
type PubSub struct {
Workers int
MaxMessages int
}
func (c *Config) Default() {
if len(c.AllowOrigins) == 0 {
c.AllowOrigins = []string{"*"}
}
if c.Ip == "" {
c.Ip = "0.0.0.0"
}
if c.Log.MaxSize == 0 {
c.Log.MaxSize = 3
}
if c.Log.MaxBackup == 0 {
c.Log.MaxBackup = 3
}
if c.Log.MaxAge == 0 {
c.Log.MaxAge = 30
}
if c.PubSub.Workers == 0 {
c.PubSub.Workers = 5
}
if c.PubSub.MaxMessages == 0 {
c.PubSub.MaxMessages = 256
}
}

150
statusClient.go Normal file
View File

@@ -0,0 +1,150 @@
package statusServer
import (
"encoding/json"
"fmt"
"statusServer/models"
pubSubModels "gitea.tecamino.com/paadi/pubSub/models"
logging "gitea.tecamino.com/paadi/tecamino-logger/logging"
)
// StatusServerClient wraps a websocket client connection to the StatusServer.
// - `id` is the client identifier (service name)
// - `client` is the underlying websocket client
// - `subs` is a map of topic → callback function, used when subscribed messages are received
type StatusClient struct {
id string
client *models.Client
subs map[string]func(data any)
}
// NewCStatuslient connects to a StatusServer websocket endpoint and sets up callbacks.
// serviceName: unique ID/name for this client
// ip, port: target server address
// debug: enable debug logging
func NewStatusClient(serviceName, ip string, port uint, debug bool) (*StatusClient, error) {
config := models.Config{Log: logging.Config{Debug: debug}}
config.Default()
// create logger for this client
logger, err := logging.NewLogger("statusServer.log", &config.Log)
if err != nil {
return nil, err
}
logger.Debug("NewClient", fmt.Sprintf("initialize new client id:%s to %s:%d", serviceName, ip, port))
// build client wrapper
sc := StatusClient{
id: serviceName,
subs: map[string]func(data any){}, // no subscriptions yet
}
// connect underlying websocket client
sc.client, err = models.NewClient(ip, serviceName, port, logger)
if err != nil {
return nil, err
}
// --- EVENT HANDLERS ---
// connection opened
sc.client.OnOpen = func() {
logger.Info("Client", fmt.Sprintf("id:%s connected to %s:%d", serviceName, ip, port))
}
// message received from server
sc.client.OnMessage = func(data []byte) {
var p pubSubModels.Data
err := json.Unmarshal(data, &p)
if err != nil {
logger.Error("OnMessage", err.Error())
return
}
// if we have a subscription callback for this topic, call it
if f, ok := sc.subs[p.Topic]; ok {
f(p.Data)
}
}
// warning received
sc.client.OnWarning = func(warn string) {
logger.Warning("Client", warn)
}
// error received
sc.client.OnError = func(err error) {
logger.Error("Client", err.Error())
}
// connection closed
sc.client.OnClose = func(code int, reason string) {
logger.Info("Client", fmt.Sprintf("id:%s closed connection to %s:%d", serviceName, ip, port))
}
return &sc, err
}
// Subscribe sends a "subscribe" action for a topic to the server and registers
// a local callback to handle messages for that topic.
func (sc *StatusClient) Subscribe(topic string, cb func(any)) error {
var data pubSubModels.Data
data.Action = "subscribe"
data.Topic = topic
// send subscribe request to server
b, err := json.Marshal(data)
if err != nil {
return err
}
sc.client.SendData(b)
// register callback for topic
sc.subs[topic] = cb
return nil
}
// Publish sends a "publish" action with a payload to the server.
// The server will then deliver this message to all subscribers of the topic.
func (sc *StatusClient) Publish(topic string, data any) error {
var payload pubSubModels.Data
payload.Action = "publish"
payload.Topic = topic
payload.Data = data
b, err := json.Marshal(payload)
if err != nil {
return err
}
sc.client.SendData(b)
return nil
}
// --- Sending helpers (proxy to server messaging system) ---
// SendInfo sends an "info" message to the server.
func (sc *StatusClient) SendInfo(data any) error {
return sc.client.SendInfo(data)
}
// SendInfo sends an "status" message to the server.
func (sc *StatusClient) SendStatus(data any) error {
return sc.client.SendStatus(data)
}
// SendDebug sends a "debug" message to the server.
func (sc *StatusClient) SendDebug(data any) error {
return sc.client.SendDebug(data)
}
// SendWarning sends a "warning" message to the server.
func (sc *StatusClient) SendWarning(data any) error {
return sc.client.SendWarning(data)
}
// SendError sends an "error" message to the server.
func (sc *StatusClient) SendError(data any) error {
return sc.client.SendError(data)
}

164
statusServer.go Normal file
View File

@@ -0,0 +1,164 @@
package statusServer
import (
"context"
"fmt"
"net/http"
"runtime"
"statusServer/utils"
"strings"
"time"
"statusServer/models"
logging "gitea.tecamino.com/paadi/tecamino-logger/logging"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// StatusServer wraps an HTTP server with PubSub over websockets.
// It manages HTTP routes, PubSub broadcasting, and runtime metrics publishing.
type StatusServer struct {
Ip string // listen address (defaults to all interfaces)
Port int // server port
Routes *gin.Engine // Gin router for HTTP endpoints
pubSub *StatusWebsocket // websocket-enabled PubSub system
log *logging.Logger // application logger
}
// NewStatusServer initializes and configures a new StatusServer instance.
//
// Setup steps:
// - Apply default values to the config.
// - Initialize logging.
// - Configure CORS middleware (including localhost + detected local IP).
// - Create PubSubWebsocket with worker/message limits.
// - Register the /status websocket endpoint.
//
// Returns the server instance or an error if initialization fails.
func NewStatusServer(config models.Config) (*StatusServer, error) {
config.Default()
// initialize logger
logger, err := logging.NewLogger("statusServer.log", &config.Log)
if err != nil {
return nil, err
}
logger.Debug("NewStatusServer", "initialize new server with allowOrigins: "+
strings.Join(config.AllowOrigins, ", ")+" listening on port: "+fmt.Sprint(config.Port))
r := gin.Default()
// always allow local dev origins (http://localhost:PORT)
config.AllowOrigins = append(config.AllowOrigins, fmt.Sprintf("http://localhost:%d", config.Port))
// detect and allow local IP in CORS origins
localIP, err := utils.GetLocalIP()
if err != nil {
logger.Error("NewStatusServer", "get local ip: "+err.Error())
} else {
config.AllowOrigins = append(config.AllowOrigins, fmt.Sprintf("http://%s:%d", localIP, config.Port))
}
// configure CORS middleware
r.Use(cors.New(cors.Config{
AllowOrigins: config.AllowOrigins,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// build the server
s := &StatusServer{
Ip: "0.0.0.0", // listen on all interfaces
Port: config.Port,
Routes: r,
log: logger,
}
// initialize PubSub websocket service
logger.Debug("NewStatusServer", fmt.Sprintf(
"initialize new PubSubWebsocket service with %d workers %d messages",
config.PubSub.Workers, config.PubSub.MaxMessages,
))
s.pubSub, err = NewStatusWebsocket(config.PubSub.Workers, config.PubSub.MaxMessages, logger)
if err != nil {
logger.Error("NewPubSubWebsocket", err.Error())
}
// register websocket endpoint
r.GET("/status", s.pubSub.NewConection)
r.GET("/info", s.pubSub.NewConection)
return s, nil
}
// ServeHttp starts the HTTP server and continuously publishes runtime metrics
// (memory allocation + GC count) to the "info" topic.
//
// It blocks until the HTTP server stops. Resources are cleaned up on shutdown:
// PubSub is closed and a shutdown message is logged.
func (s *StatusServer) ServeHttp() error {
s.log.Debug("ServeHttp", "start publishing runtime metrics (memory + GC count)")
// create cancellable context for metrics goroutine
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// metrics publisher goroutine
go func() {
for {
var init bool
var m runtime.MemStats
var oldM uint64
var oldGc uint32
var info struct {
Memory uint64 `json:"memory"`
GC uint32 `json:"gc"`
}
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// stop publishing if context is canceled
return
case <-ticker.C:
// read runtime memory statistics
runtime.ReadMemStats(&m)
info.GC = m.NumGC
// publish only when values changed or first run
if oldGc != m.NumGC || oldM != m.Alloc/1024 || !init {
info.Memory = m.Alloc / 1024
s.pubSub.Publish("info", info)
oldGc = m.NumGC
oldM = m.Alloc / 1024
init = true
}
}
}
}
}()
// log startup
s.log.Info("ServeHttp", fmt.Sprintf("start listening on %s:%d", s.Ip, s.Port))
// configure HTTP server
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", s.Ip, s.Port),
Handler: s.Routes,
}
// log shutdown + close PubSub on exit
defer s.log.Info("ServeHttp", fmt.Sprintf("close server listening on %s:%d", s.Ip, s.Port))
defer s.pubSub.Close()
// blocking call
return srv.ListenAndServe()
}

132
statusWebsocket.go Normal file
View File

@@ -0,0 +1,132 @@
package statusServer
import (
"fmt"
"statusServer/handlers"
logging "gitea.tecamino.com/paadi/tecamino-logger/logging"
"github.com/gin-gonic/gin"
"encoding/json"
"gitea.tecamino.com/paadi/pubSub"
pubSubModels "gitea.tecamino.com/paadi/pubSub/models"
"gitea.tecamino.com/paadi/tecamino-dbm/auth"
)
// StatusWebsocket wraps a PubSub instance with websocket support.
// It manages connected clients, handles their subscriptions, and
// relays messages via a PubSub publish/subscribe mechanism.
type StatusWebsocket struct {
*pubSub.Pubsub // core publish/subscribe engine
clientHandler *handlers.ClientHandler // manages websocket clients
*logging.Logger // application logger
}
// NewStatusWebsocket initializes a new PubSubWebsocket server.
// - worker: number of worker goroutines processing messages.
// - maxMessage: size of the message queue.
// - logger: optional custom logger (creates default if nil).
func NewStatusWebsocket(worker, maxMessage int, logger *logging.Logger) (ws *StatusWebsocket, err error) {
if logger == nil {
// Create default logger if none provided.
logger, err = logging.NewLogger("PubSubWebsocket.log", logging.DefaultConfig())
if err != nil {
return nil, err
}
}
ws = &StatusWebsocket{
Pubsub: pubSub.NewPubsub(worker, maxMessage),
clientHandler: handlers.NewConnectionHandler(),
Logger: logger,
}
return
}
// NewConection upgrades an HTTP request to a websocket connection
// and registers the client with the PubSubWebsocket server.
//
// Each connected client can:
// - subscribe to topics (receives messages published on them),
// - unsubscribe from topics,
// - publish messages to topics (delivered to all subscribers).
func (ws *StatusWebsocket) NewConection(c *gin.Context) {
// Authenticate and extract client ID from query parameters.
id, err := auth.GetIDFromQuery(c)
if err != nil {
ws.Error("Websocket", "error GetIDFromQuery: "+err.Error())
return
}
ws.Debug("Websocket", "authorization id token: "+id)
// Create or reuse a websocket client.
client, err := ws.clientHandler.ConnectNewClient(id, c)
if err != nil {
ws.Error("Websocket", err.Error())
return
}
// ---------------- Event Handlers ----------------
// Handle incoming messages from the client.
client.OnMessage = func(data []byte) {
// Decode request into PubSub message model.
request, err := readJsonData(data)
if err != nil {
client.SendData([]byte(`{"error":"read json: ` + err.Error() + `"}`))
ws.Error("Websocket", "read json: "+err.Error())
return
}
// Route the action type.
switch request.Action {
case "subscribe":
ws.Debug("Websocket", "subscribe id:"+id+" topic:"+request.Topic)
// Register subscription callback.
ws.Subscribe(id, request.Topic, func(data any) {
b, err := json.Marshal(data)
if err != nil {
return
}
client.SendData(b)
})
case "unsubscribe":
ws.Debug("Websocket", "unsubscribe id:"+id+" topic:"+request.Topic)
ws.Unsubscribe(id, request.Topic)
case "publish":
ws.Publish(request.Topic, request.Data)
default:
// Invalid or unsupported action.
ws.Error("Websocket", "action type '"+request.Action+"' not supported")
client.SendData([]byte(`{"error":"action type '` + request.Action + `' not supported"}`))
}
}
// Handle warnings raised by the client connection.
client.OnWarning = func(msg string) {
ws.Warning("Websocket", "warning on websocket connection: "+msg)
}
// Handle errors raised by the client connection.
client.OnError = func(err error) {
ws.Error("Websocket", "error on websocket connection: "+err.Error())
}
// Cleanup when the client disconnects.
client.OnClose = func(code int, reason string) {
ws.Debug("Websocket", fmt.Sprintf("onClose id:%s code:%d reason:%s", id, code, reason))
ws.UnsubscribeAll(id) // free all subscriptions for this client
ws.clientHandler.RemoveClient(id)
}
}
// readJsonData decodes raw websocket bytes into a PubSub Data struct.
func readJsonData(data []byte) (request pubSubModels.Data, err error) {
err = json.Unmarshal(data, &request)
return
}

60
test/server_test.go Normal file
View File

@@ -0,0 +1,60 @@
package test
import (
"statusServer"
"statusServer/models"
"testing"
"time"
"gitea.tecamino.com/paadi/tecamino-logger/logging"
)
func TestServer(t *testing.T) {
var err error
server, err := statusServer.NewStatusServer(models.Config{Port: 8080})
if err != nil {
t.Fatal(err)
}
go func() {
err = server.ServeHttp()
}()
if err != nil {
t.Fatal(err)
}
}
func TestConnection(t *testing.T) {
s, err := statusServer.NewStatusServer(models.Config{Port: 8080, Log: logging.Config{Debug: true}})
if err != nil {
t.Fatal(err)
}
go s.ServeHttp()
time.Sleep(time.Second)
client1, err := statusServer.NewStatusClient("adrian", "127.0.0.1", 8080, true)
if err != nil {
t.Fatal(err)
}
client, err := statusServer.NewStatusClient("test", "127.0.0.1", 8080, true)
if err != nil {
t.Fatal(err)
}
client1.Subscribe("test/info", func(a any) {
t.Log("test", a)
})
var info struct {
Message string
Count int
}
info.Message = "test count"
for range 10 {
info.Count++
client.SendInfo(info)
client.SendInfo("info")
time.Sleep(time.Second)
}
}

316
test/statusServer.log Normal file
View File

@@ -0,0 +1,316 @@
{"level":"info","timestamp":"2025-08-23T10:24:46.703","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T10:26:29.040","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T10:26:29.040","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T10:26:29.040","msg":"initialize new PubSub service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T10:26:29.040","msg":"start example variables like memory usage and garbage collection count","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T10:26:29.040","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T10:26:29.041","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.041","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.043","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.043","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.043","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.043","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T10:26:30.043","msg":"id:adrian connected to 127.0.0.1:8080","caller":"Client"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.043","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.044","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.044","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.044","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:26:30.044","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:36.617","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T10:28:36.618","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T10:28:36.618","msg":"initialize new PubSub service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T10:28:36.618","msg":"start example variables like memory usage and garbage collection count","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T10:28:36.618","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T10:28:36.619","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.619","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.621","msg":"authorization id token: adrian","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.622","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.622","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.622","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.622","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.622","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.622","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.623","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.623","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.623","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.623","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T10:28:37.624","msg":"subscribe id:adrian topic:test/info","caller":"Websocket"}
{"level":"info","timestamp":"2025-08-23T20:48:20.110","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:48:20.110","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:48:20.111","msg":"initialize new status server server","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:48:20.111","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:48:20.111","msg":"start example variables like memory usage and garbage collection count","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:48:20.111","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:48:20.111","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.111","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.114","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:48:21.115","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:32.958","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:54:32.958","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:54:32.959","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:54:32.959","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:54:32.959","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:54:32.960","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.960","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T20:54:33.961","msg":"id:adrian connected to 127.0.0.1:8080","caller":"Client"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:33.961","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:52.574","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:54:52.574","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:54:52.575","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:54:52.575","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:54:52.575","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:54:52.575","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.311","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.576","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.577","msg":"authorization id token: adrian","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.577","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.577","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.577","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.577","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.577","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:54:53.577","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:00.048","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:55:00.048","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:55:00.049","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:55:00.049","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:55:00.049","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:55:00.049","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.050","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.052","msg":"authorization id token: adrian","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.052","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.052","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.052","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.052","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T20:55:01.052","msg":"id:adrian connected to 127.0.0.1:8080","caller":"Client"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.052","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.053","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.053","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.053","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.053","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.053","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:01.053","msg":"subscribe id:adrian topic:test/info","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:02.176","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:04.610","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:05.111","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:05.391","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:06.443","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:07.210","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:08.226","msg":"authorization id token: test","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:55:47.859","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:55:47.859","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:55:47.859","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:55:47.860","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:55:47.860","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:55:47.860","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.860","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.862","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.862","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.862","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.862","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.862","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.863","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.863","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.863","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:55:48.863","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T20:55:48.863","msg":"id:test connected to 127.0.0.1:8080","caller":"Client"}
{"level":"error","timestamp":"2025-08-23T20:56:05.391","msg":"error on websocket connection: read error (id:test34): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:56:39.215","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:56:39.215","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:56:39.215","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:56:39.216","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:56:39.216","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:56:39.216","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.216","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.218","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.218","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.218","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.218","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T20:56:40.218","msg":"id:adrian connected to 127.0.0.1:8080","caller":"Client"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.218","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.219","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.219","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.219","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:56:40.219","msg":"start write goroutine","caller":"NewClient"}
{"level":"error","timestamp":"2025-08-23T20:56:40.877","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:42.110","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:43.644","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:48.094","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:48.545","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:49.027","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:58.724","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:59.092","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:59.292","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:59.424","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:56:59.608","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:57:00.443","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:57:00.626","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:57:00.809","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:57:00.991","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:57:01.157","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:58:03.738","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:58:03.739","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:58:03.739","msg":"initialize new PubSub service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:58:03.739","msg":"start example variables like memory usage and garbage collection count","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:58:03.739","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:58:03.740","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.740","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.741","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.741","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.741","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.741","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.741","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.742","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.742","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.742","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:04.742","msg":"start write goroutine","caller":"NewClient"}
{"level":"error","timestamp":"2025-08-23T20:58:05.278","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:58:06.860","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:58:10.226","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:58:10.994","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:58:11.561","msg":"error GetIDFromQuery: id missing","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:58:47.241","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:58:47.241","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:58:47.242","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:58:47.242","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:58:47.242","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:58:47.242","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.242","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.244","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.245","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.245","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.245","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.245","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.246","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.246","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.246","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:58:48.246","msg":"start write goroutine","caller":"NewClient"}
{"level":"error","timestamp":"2025-08-23T20:59:02.224","msg":"read json: invalid character '\\n' in string literal","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T20:59:46.456","msg":"error on websocket connection: read error (id:234): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T20:59:58.891","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T20:59:58.891","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:59:58.892","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T20:59:58.892","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:59:58.892","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T20:59:58.892","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.893","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.895","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.895","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.895","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.895","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T20:59:59.895","msg":"id:adrian connected to 127.0.0.1:8080","caller":"Client"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.895","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.896","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.896","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.896","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T20:59:59.896","msg":"start write goroutine","caller":"NewClient"}
{"level":"error","timestamp":"2025-08-23T21:00:06.689","msg":"error on websocket connection: read error (id:234): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T21:00:52.670","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T21:00:52.670","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:00:52.671","msg":"initialize new PubSub service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T21:00:52.671","msg":"start example variables like memory usage and garbage collection count","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:00:52.671","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:00:52.671","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.671","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.674","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.674","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.674","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.674","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.674","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.675","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.675","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.675","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:00:53.675","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T21:00:53.675","msg":"id:test connected to 127.0.0.1:8080","caller":"Client"}
{"level":"error","timestamp":"2025-08-23T21:01:01.672","msg":"error on websocket connection: read error (id:234): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T21:02:03.657","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T21:02:03.657","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:02:03.658","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T21:02:03.658","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:02:03.658","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:02:03.659","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.659","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:02:04.661","msg":"start write goroutine","caller":"NewClient"}
{"level":"error","timestamp":"2025-08-23T21:02:13.655","msg":"error on websocket connection: read error (id:235): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T21:07:39.871","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T21:07:39.871","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:07:39.872","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T21:07:39.872","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:07:39.872","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:07:39.872","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.872","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.874","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.874","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.874","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.874","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.874","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.875","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.875","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.875","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:07:40.875","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T21:07:40.875","msg":"id:test connected to 127.0.0.1:8080","caller":"Client"}
{"level":"error","timestamp":"2025-08-23T21:07:48.105","msg":"error on websocket connection: read error (id:235): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T21:08:02.336","msg":"error on websocket connection: read error (id:235): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T21:08:10.976","msg":"error on websocket connection: websocket: close sent","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T21:08:17.158","msg":"error on websocket connection: websocket: close sent","caller":"Websocket"}
{"level":"debug","timestamp":"2025-08-23T21:39:47.673","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T21:39:47.673","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:39:47.674","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T21:39:47.674","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:39:47.674","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:39:47.674","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.674","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.676","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.676","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.676","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.676","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.676","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.677","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.677","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.677","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:39:48.677","msg":"start write goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:24.365","msg":"initialize new server with allowOrigins: * listening on port: 8080","caller":"NewStatusServer"}
{"level":"info","timestamp":"2025-08-23T21:41:24.365","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:41:24.365","msg":"initialize new PubSubWebsocket service with 5 workers 256 messages","caller":"NewStatusServer"}
{"level":"debug","timestamp":"2025-08-23T21:41:24.365","msg":"start publishing runtime metrics (memory + GC count)","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:41:24.365","msg":"start listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"info","timestamp":"2025-08-23T21:41:24.366","msg":"close server listening on 0.0.0.0:8080","caller":"ServeHttp"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.366","msg":"initialize new client id:adrian to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.368","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.368","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.368","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.368","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T21:41:25.368","msg":"id:adrian connected to 127.0.0.1:8080","caller":"Client"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.368","msg":"initialize new client id:test to 127.0.0.1:8080","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.369","msg":"set PingHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.369","msg":"set PongHandler","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.369","msg":"start read goroutine","caller":"NewClient"}
{"level":"debug","timestamp":"2025-08-23T21:41:25.369","msg":"start write goroutine","caller":"NewClient"}
{"level":"info","timestamp":"2025-08-23T21:41:25.369","msg":"id:test connected to 127.0.0.1:8080","caller":"Client"}
{"level":"error","timestamp":"2025-08-23T21:42:15.297","msg":"error on websocket connection: read error (id:238): websocket: close 1000 (normal)","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T21:42:25.420","msg":"error on websocket connection: websocket: close sent","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T21:42:26.597","msg":"error on websocket connection: read error (id:234): websocket: close 1006 (abnormal closure): unexpected EOF","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T21:42:26.597","msg":"error on websocket connection: read error (id:238): websocket: close 1006 (abnormal closure): unexpected EOF","caller":"Websocket"}
{"level":"error","timestamp":"2025-08-23T21:42:27.701","msg":"error on websocket connection: websocket: close sent","caller":"Websocket"}

22
utils/utils.go Normal file
View File

@@ -0,0 +1,22 @@
package utils
import (
"fmt"
"net"
)
func GetLocalIP() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
return ipNet.IP.String(), nil
}
}
}
return "", fmt.Errorf("no local IP address found")
}