commit e36d39374972658b6069367d9e287de3ae4e35b7 Author: Adrian Zuercher Date: Sat Aug 23 22:22:22 2025 +0200 first commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4a01e7 --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..deeeaa0 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..52e4f5f --- /dev/null +++ b/go.sum @@ -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= diff --git a/handlers/clientHandler.go b/handlers/clientHandler.go new file mode 100644 index 0000000..eaed356 --- /dev/null +++ b/handlers/clientHandler.go @@ -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) +} diff --git a/models/client.go b/models/client.go new file mode 100644 index 0000000..23d9c77 --- /dev/null +++ b/models/client.go @@ -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 +} diff --git a/models/clients.go b/models/clients.go new file mode 100644 index 0000000..6408ecd --- /dev/null +++ b/models/clients.go @@ -0,0 +1,7 @@ +package models + +type Clients map[string]*Client + +func NewClients() Clients { + return make(Clients) +} diff --git a/models/config.go b/models/config.go new file mode 100644 index 0000000..d2b83c0 --- /dev/null +++ b/models/config.go @@ -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 + } +} diff --git a/statusClient.go b/statusClient.go new file mode 100644 index 0000000..35417d8 --- /dev/null +++ b/statusClient.go @@ -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) +} diff --git a/statusServer.go b/statusServer.go new file mode 100644 index 0000000..53899d6 --- /dev/null +++ b/statusServer.go @@ -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() +} diff --git a/statusWebsocket.go b/statusWebsocket.go new file mode 100644 index 0000000..8e64864 --- /dev/null +++ b/statusWebsocket.go @@ -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 +} diff --git a/test/server_test.go b/test/server_test.go new file mode 100644 index 0000000..e9ada3e --- /dev/null +++ b/test/server_test.go @@ -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) + } +} diff --git a/test/statusServer.log b/test/statusServer.log new file mode 100644 index 0000000..648a8ed --- /dev/null +++ b/test/statusServer.log @@ -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"} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..71f5ebb --- /dev/null +++ b/utils/utils.go @@ -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") +}