{"id":43836282,"url":"https://github.com/eloylp/goomerang","last_synced_at":"2026-02-06T04:10:11.582Z","repository":{"id":44407627,"uuid":"385026502","full_name":"eloylp/goomerang","owner":"eloylp","description":"A small communications library based on protocol buffers over websockets","archived":false,"fork":false,"pushed_at":"2022-10-01T23:37:38.000Z","size":815,"stargazers_count":40,"open_issues_count":6,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-11-21T13:22:36.854Z","etag":null,"topics":["async","broadcast","concurrent","go","golang","graceful-shutdown","handlers","middleware","protobuf","protocol-buffers","pubsub","websocket-server","websockets"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eloylp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-07-11T18:37:59.000Z","updated_at":"2024-06-28T21:33:49.000Z","dependencies_parsed_at":"2022-08-12T11:10:32.471Z","dependency_job_id":null,"html_url":"https://github.com/eloylp/goomerang","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/eloylp/goomerang","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eloylp%2Fgoomerang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eloylp%2Fgoomerang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eloylp%2Fgoomerang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eloylp%2Fgoomerang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eloylp","download_url":"https://codeload.github.com/eloylp/goomerang/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eloylp%2Fgoomerang/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29149875,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-06T02:39:25.012Z","status":"ssl_error","status_checked_at":"2026-02-06T02:37:22.784Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","broadcast","concurrent","go","golang","graceful-shutdown","handlers","middleware","protobuf","protocol-buffers","pubsub","websocket-server","websockets"],"created_at":"2026-02-06T04:10:10.813Z","updated_at":"2026-02-06T04:10:11.574Z","avatar_url":"https://github.com/eloylp.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Goomerang\n\nA [protocol buffers](https://developers.google.com/protocol-buffers/) over [websocket](https://datatracker.ietf.org/doc/html/rfc6455)\ncommunications library.\n\u003cbr\u003e\n\u003cbr\u003e\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://raw.githubusercontent.com/lidiackr/gophers/main/eloylp/goomerang/head.png\" alt=\"goomerang\" width=\"550\"/\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://github.com/eloylp/goomerang/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/eloylp/goomerang/actions/workflows/ci.yml/badge.svg\" alt=\"ci status\"\u003e\u003c/a\u003e\n\u003ca href=\"https://goreportcard.com/report/go.eloylp.dev/goomerang\"\u003e\u003cimg src=\"https://goreportcard.com/badge/go.eloylp.dev/goomerang\" alt=\"code quality\"\u003e\u003c/a\u003e\n\u003ca href=\"https://godoc.org/go.eloylp.dev/goomerang\"\u003e\u003cimg src=\"https://godoc.org/go.eloylp.dev/goomerang?status.svg\" alt=\"godoc\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"right\" style=\"color:silver\"\u003e\nGopher art by \u003ca href=\"https://github.com/lidiackr\"\u003e@lidiackr\u003c/a\u003e\n\u003c/p\u003e\n\n## Status\n\nThis project is still a proof of concept. It has none or very little production experience. But that doesnt mean it wasnt made with ❤ .\nMaintainers of the project reserve the right of\nbreaking the public API in new versions.\n\n## Motivation\n\nGoomerang is an effort to provide an easy, application friendly way for communicating clients and servers. Because some of us only need to\nsend some protos over a fast pipe. Here is a high level of the idea (bar napkin drawing):\n\n![idea](docs/idea.drawio.svg)\n\nPossible status transitions for servers:\n\n```mermaid\nstateDiagram-v2\n direction LR\n [*] --\u003e NEW\n NEW --\u003e RUNNING\n RUNNING --\u003e CLOSING\n CLOSING --\u003e CLOSED  \n CLOSED --\u003e [*]\n```\n\nPossible status transitions for clients:\n\n```mermaid\nstateDiagram-v2\ndirection LR\n[*] --\u003e NEW\nNEW --\u003e RUNNING\nRUNNING --\u003e CLOSING\nCLOSING --\u003e CLOSED\nCLOSED --\u003e RUNNING\nCLOSED --\u003e [*]\n```\n\n## Main features\n\n* Simple, modular, embeddable. Use only the parts of interest. Have full control over the dependency.\n* Websockets as underlying transport (currently built on top of [gorilla/websocket](https://github.com/gorilla/websocket)).\n* [Protocol buffers](https://developers.google.com/protocol-buffers/) as first class citizen. Register [message handlers](#message-handlers)\n  at clients and servers.\n* [Middleware support](#middlewares), inspired by the Go HTTP standard lib.\n* [Message Broadcasting](#broadcasts) feature.\n* [Publish and subscribe](#publish-and-subscribe) model.\n* Send [synchronous messages](#synchronous-sends) from the client side, following a request/response pattern.\n* Support for concurrency at a message handler level.\n* Support for configurable [hooks](#hooks) for certain actions.\n* [Graceful shutdown](#graceful-shutdown) of the websocket connection/processing.\n* [Ping/pong](https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2) messages out of the box in clients and server. Keep alive\n  connections.\n* Customizable TLS configuration.\n* Ability to introduce [custom HTTP routes](#custom-http-routes).\n* Custom errors are exposed, ability to build retry systems on top of the public API.\n* [Observability tools](#observability), that will help instrumenting clients and servers.\n\n## Installation\n\n```bash\ngo get go.eloylp.dev/goomerang\n```\n\n## Basic usage\n\nIf you are unfamiliar with protocol buffers, all the examples on this readme plus a little guide can be\nfound [here](example/protos/README.md).\n\nLet's create a basic server. We just need to import the server package from the library. The server has\nmultiple [available options](server/opts.go), we encourage the user to review them.\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/server\"\n)\n\nfunc main() {\n\ts, err := server.New(\n\t\tserver.WithListenAddr(\"127.0.0.1:8080\"),\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := s.Run(); err != nil { // Will block the program till exit.\n\t\tlog.Fatal(err)\n\t}\n\t//...\n}\n```\n\nThe client side package its quite symmetric with the server one. The client has also multiple [available options](client/opts.go), Let's\nsee an example:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/client\"\n)\n\nfunc main() {\n\t//...\n\tc, err := client.New(\n\t\tclient.WithServerAddr(\"127.0.0.1:8080\"),\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := c.Connect(context.TODO()); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t//...\n}\n```\n\nThat's it ! we just have connected a client and a server ! But not in a very useful way. Both, client and server have support for handlers.\nSee the next sections for more information.\n\n## Messages\n\nGoomerang facilitates the creation of custom message protocols. Messages can have 2 parts, `headers` and a `payload`. Headers are just\na `map[string]string`. The payload allows holding any type which accomplishes\nthe [proto.Message](https://github.com/protocolbuffers/protobuf-go/blob/v1.28.0/proto/proto.go#L24) interface, so any protocol buffer\nmessage type.\n\n```go\npackage main\n\nimport (\n\t\"go.eloylp.dev/goomerang/message\"\n\n\t\"go.eloylp.dev/goomerang/example/protos\"\n)\n\nfunc main() {\n\tmsg := message.New().\n\t\tSetHeader(\"Key\", \"Value\").\n\t\tSetPayload(\u0026protos.MessageV1{\n\t\t\tMessage: \"my message !\",\n\t\t})\n}\n```\n\nIt's **encouraged to use the message builder**, so all complexities and needed data will be fulfilled.\n\nThe `message.Message` type it's normally used in message handlers. It can also be used\nin the public API of client and server instances for simple operations like `Send(msg)`. See more in the following documentation.\n\n### Messages registration\n\nBoth parts, clients and servers, need to know all kind of messages they are going to receive beforehand. There are 2 ways of registering\nmessage types:\n\n* Using the [handler registration](#message-handlers) functions.\n* In clients, the convenient `client.RegisterMessage(msg)` can be used.\n\n**They are exclusive**. If a message is already registered in the moment of the handler registration, such message doesn't need to be\nregistered\nby other methods.\n\n## An history of handlers and middlewares\n\nThere's support for handlers and middlewares for both, client and server sides. This part of the project comes inspired by the\nGo [HTTP standard library](https://pkg.go.dev/net/http#HandlerFunc). The intention is to make this part familiar and versatile.\n\nHandlers and middlewares **can only be registered before starting** the client or the server.\n\n### Message Handlers\n\nLet's see how to register a message handler.\n\n```go\npackage main\n\nimport (\n\t\"go.eloylp.dev/goomerang/message\"\n\t\"go.eloylp.dev/goomerang/server\"\n\t\"go.eloylp.dev/goomerang/example/protos\"\n)\n\nfunc main() {\n\n\t// Create the server\n\ts, _ := server.New(server.WithListenAddr(\"127.0.0.1:8080\"))\n\n\t// protos.MessageV1 represents a hypothetical message in your proto repo.\n\ts.Handle(\u0026protos.MessageV1{}, message.HandlerFunc(func(sender message.Sender, msg *message.Message) {\n\t\t// We need to cast the message, from the proto interface, to the concrete message type.\n\t\tmsgT := msg.Payload.(*protos.MessageV1)\n\n\t\t// Do whatever with your message,\n\t\t// your handling logic goes here.\n\n\t\t// Alternatively, reply with another message.\n\t\tpayload := \u0026protos.ReplyV1{}\n\t\treply := message.New().\n\t\t\tSetPayload(payload).\n\t\t\tSetHeader(\"status\", \"200\")\n\n\t\t_, err := sender.Send(reply) // Replies to the client connection.\n\t\t// check for errors ...\n\t}))\n}\n```\n\nSymmetrically, the same handler registering signature can be found at the client public API.\n\nAs handlers only need to accomplish the `message.Handler` interface, It's very common to use structs for holding handler dependencies,\nwhich need to be thread safe:\n\n```go\npackage main\n\nimport (\n\t\"database/sql\"\n\n\t\"go.eloylp.dev/goomerang/message\"\n)\n\ntype Handler struct {\n\tDB *sql.DB\n}\n\nfunc (m *Handler) Handle(sender message.Sender, msg *message.Message) {\n\t//  handling logic goes there.\n}\n```\n\n### Middlewares\n\nMiddlewares are just message handlers that always get executed no matter the kind of message, alternatively providing the ability to execute\nthe next\nhandler in the chain. They are executed just before the matched message handler, bringing the opportunity to add preprocessing or\npostprocessing\nlogic to the message handling operation. i.e metrics, logging or panic handlers. Let's see how to register a middleware:\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"go.eloylp.dev/goomerang/message\"\n\t\"go.eloylp.dev/goomerang/server\"\n)\n\nfunc main() {\n\ts, _ := server.New(server.WithListenAddr(\"127.0.0.1:8080\"))\n\ts.Middleware(func(h message.Handler) message.Handler {\n\t\treturn message.HandlerFunc(func(sender message.Sender, msg *message.Message) {\n\t\t\tfmt.Printf(\"received message of kind: %q\", msg.Metadata.Kind)\n\t\t\th.Handle(sender, msg) // Continue with the next handler in chain.\n\t\t})\n\t})\n}\n```\n\nFor many of you that sounds really familiar ! Yes, Goomerang tries to preserve same aspects of the standard library. Again, the same\nsymmetric interface\ncan be found in the client public API.\n\nIt's important to note that **any number of middlewares can be registered**. The **order of registration will drive the order of execution**\n. So for example, if we had:\n\n```go\npackage main\n\nimport (\n\t\"go.eloylp.dev/goomerang/message\"\n\t\"go.eloylp.dev/goomerang/server\"\n)\n\nfunc main() {\n\ts, _ := server.New(server.WithListenAddr(\"127.0.0.1:8080\"))\n\ts.Handle(Handler())\n\ts.Middleware(Middleware1())\n\ts.Middleware(Middleware2())\n}\n```\n\nThe order of execution would be:\n\n```mermaid\ngraph LR;\nMiddleware1--\u003eMiddleware2;\nMiddleware2--\u003eHandler;\n```\n\nThis library facilitates some middleware [implementations](middleware) that could be used directly by library users. Let's see the example\nof the panic\nhandler one:\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"go.eloylp.dev/goomerang/message\"\n\t\"go.eloylp.dev/goomerang/middleware\"\n\t\"go.eloylp.dev/goomerang/server\"\n)\n\nfunc main() {\n\ts, _ := server.New(server.WithListenAddr(\"127.0.0.1:8080\"))\n\ts.Middleware(middleware.Panic(func(p interface{}) {\n\t\tfmt.Printf(\"panic detected: %v\", p)\n\t}))\n}\n```\n\nAs a rule of thumb, it's recommended to implement the panic middleware as first in the chain (so it protects the rest of handlers), in\norder to not crash the entire process if a panic it's raised at some point in the handler chain.\n\n## Broadcasts\n\nThis library allows sending a message to all connected clients via the broadcast interface. This interfaces can be found in both\nimplementations, the client and the server.\n\nFrom the client side perspective, the only thing to do is to call the `c.Broadcast(msg)` method:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/client\"\n\t\"go.eloylp.dev/goomerang/example/protos\"\n\t\"go.eloylp.dev/goomerang/message\"\n)\n\nfunc main() {\n\t//...\n\tc, err := client.New(\n\t\tclient.WithServerAddr(\"127.0.0.1:8080\"),\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := c.Connect(context.TODO()); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tmsg := message.New().SetPayload(\u0026protos.MessageV1{\n\t\tMessage: \"my message !\",\n\t})\n\tif _, err := c.Broadcast(msg); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n```\n\nThe above call to `c.Broadcast(msg)` sends a command to the server, which will send the message to all connected clients. That means\nthe message being broadcasted needs to be properly [registered](#messages-registration) in all peers.\n\n```mermaid\ngraph LR;\nClient1--\u003eServer;\nServer--\u003eClient1;\nServer--\u003eClient2;\nServer--\u003eClient3;\n```\n\nFrom the server side perspective, it's also possible to send a message to all connected clients. This can be useful for a number of cases,\nlike\npush notifications. Here's and example on how to do it from the server side:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/example/protos\"\n\t\"go.eloylp.dev/goomerang/message\"\n\t\"go.eloylp.dev/goomerang/server\"\n)\n\nfunc main() {\n\ts, _ := server.New(server.WithListenAddr(\"127.0.0.1:8080\"))\n\n\tmsg := message.New().SetPayload(\u0026protos.MessageV1{\n\t\tMessage: \"a message for everyone !\",\n\t})\n\n\t_, err := s.BroadCast(context.TODO(), msg)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n```\n\nIn contrast with client side broadcasts, server side invoked broadcasts will start the broadcasting operation immediately.\n\n```mermaid\ngraph LR;\nServer--\u003eClient1;\nServer--\u003eClient2;\nServer--\u003eClient3;\n```\n\n## Publish and subscribe\n\nClients can send asynchronous messages among them by publishing and subscribing to topics. Currently, **each topic will conform a diffusion\ndomain**, which\nmeans that any message published in a given topic will be sent to all subscribed clients, without taking place any kind of load balancing.\n\nOn the client side, all messages intended to be received by a subscription, should be properly [handled](#message-handlers).\n\nThe server must be aware of all message kinds going through the\npub/sub system. See [message registration](#messages-registration) for more details.\n\nLet's check and example of the client side API:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/client\"\n\t\"go.eloylp.dev/goomerang/example/protos\"\n\t\"go.eloylp.dev/goomerang/message\"\n)\n\nfunc main() {\n\t//...\n\tc, err := client.New(\n\t\tclient.WithServerAddr(\"127.0.0.1:8080\"),\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := c.Connect(context.TODO()); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// Client subscribes to a topic. Now it will receive all messages\n\t// for that topic.\n\tif err := c.Subscribe(\"topic.a\"); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// At any moment, any client can publish a message to a specific topic.\n\tmsg := message.New().SetPayload(\u0026protos.MessageV1{})\n\tif _, err := c.Publish(\"topic.a\", msg); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// Finally, client can unsubscribe from a topic at any moment, the\n\t// client will stop receiving messages for that topic.\n\tif err := c.Unsubscribe(\"topic.a\"); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n```\n\nWe can notice the `Subscribe(topic string)`, `Publish(topic string, msg *message.Message)` and `Unsubscribe(topic string)` interfaces.\nThis three calls will send a command to the server. After that, the server will accomplish the required operation.\n\nFrom the server side API, there's also a method `Publish(topic string, msg *message.Message)`:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/message\"\n\t\"go.eloylp.dev/goomerang/server\"\n\t\"go.eloylp.dev/goomerang/example/protos\"\n)\n\nfunc main() {\n\t// Create the server\n\ts, _ := server.New(server.WithListenAddr(\"127.0.0.1:8080\"))\n\n\tmsg := message.New().SetPayload(\u0026protos.MessageV1{\n\t\tMessage: \"a message for everyone !\",\n\t})\n\t// Once clients are connected, server can start sending messages\n\t// to specific topics.\n\tif err := s.Publish(\"topic.a\", msg); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n```\n\nA typical use case for server side publications , would be to consume from a queue and fan out the messages to the different topics, in\nwhich\nclients would be\nsubscribed.\n\n```mermaid\ngraph LR;\nMessageBroker--\u003eServer;\nServer--\u003eTopicA;\nServer--\u003eTopicB;\nServer--\u003eTopicC;\nTopicA--\u003eClient1;\nTopicA--\u003eClient2;\nTopicB--\u003eClient2;\nTopicC--\u003eClient3;\n```\n\n(Graph of Message flow)\n\nThe **internal state** of the pub/sub engine its **not replicated across server replicas**. That means the server can't scale\nhorizontally, yet.\n\n## Synchronous sends\n\nAll the message sending interfaces from clients and servers, **are completely asynchronous**. They work as a\n\"fire and forget\" send system. That means the user of the library should design a way to ensure the message was received and processed by\nthe server before removing it from its internal state. Unless of course, the intrinsic value of the data expires very quick and the client\ncan afford its lost.\n\nIn response to this issue, clients on this library have a `c.SyncSend(ctx, msg)` method implemented. This allows clients sending messages\nand wait for the respective server reply.\n\nHere is an example of its use:\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/client\"\n\t\"go.eloylp.dev/goomerang/message\"\n\t\"go.eloylp.dev/goomerang/example/protos\"\n)\n\nfunc main() {\n\tc, err := client.New(client.WithServerAddr(\"127.0.0.1:8080\"))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// The client needs to be aware of the reply messages \n\t// beforehand. \n\tc.RegisterMessage(\u0026protos.SuccessReplyV1{})\n\tc.RegisterMessage(\u0026protos.BadReplyV1{})\n\n\tmsg := message.New().SetPayload(\u0026protos.MessageV1{})\n\t// Will block till reply from the server is received or\n\t// the context is cancelled.\n\t_, respProto, err := c.SendSync(context.TODO(), msg)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tif respProto.GetHeader(\"status\") != \"OK\" {\n\t\tresp := respProto.Payload.(*protos.BadReplyV1)\n\t\t// Do something with the bad reply\n\t\treturn\n\t}\n\tresp := respProto.Payload.(*protos.SuccessReplyV1)\n\t// Do something with the success reply\n}\n```\n\nThis is just a high level request/response pattern built on the top of the \"fire and forget\"\npreviously commented send system. It probably can be useful some users, when the reply is\ncrucial for completing the operation. In this cases, is good to remember that we are sharing the same pipe here, for all the messages, so\nthis channel can get contested.\n\n### Some extra notes\n\nAsynchronous methods just write to the TCP socket send buffer. They are only going to block the call if the other peer stops ACKing\nTCP packets, so the pre-negotiated TCP window size is exceeded. That way the sender knows when to stop sending messages to the other peer,\nuntil it starts performing more TCP acks again.\n\nThe following command can give us minimum, default and maximum receive buffers size in bytes in a Linux machine.\n\n```bash\n$ cat /proc/sys/net/ipv4/tcp_rmem \n4096\t131072\t6291456\n```\n\n## Custom HTTP routes\n\nMany times we want to run more HTTP handler logic in the same port of websockets.\nThis library provides such ability, allowing users to register custom handlers at the\nbeginning of the server setup. Here's an example of a health endpoint:\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"go.eloylp.dev/goomerang/server\"\n)\n\nfunc main() {\n\ts, _ := server.New(server.WithListenAddr(\"127.0.0.1:8080\"))\n\t// Obtain the router + normal handler registration\n\ts.Router().Handle(\"/health\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Do more checks\n\t\t_, _ = w.Write([]byte(\"ok\"))\n\t}))\n}\n```\n\n⚠ As an important note, currently the paths `/ws` and `/wss` are reserved for library use.\n\nThe exposed router is the one provided by the [gorilla mux project](https://github.com/gorilla/mux).\n\n## Hooks\n\nSometimes getting feedback from internal parts of the system its difficult. Specially in processing loops, where in case of errors we cannot\nreturn them to the user, and we do not want to make decisions on their place. To deal with this,\nin both parts (client and server) the user can register function hooks. A complete list of them can be found in the configuration options of\nboth, [client](client/opts.go) and [server](server/opts.go). Let's take a look on how to register an error hook:\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\n\t\"go.eloylp.dev/goomerang/server\"\n)\n\nfunc main() {\n\ts, _ := server.New(\n\t\tserver.WithListenAddr(\"127.0.0.1:8080\"),\n\t\tserver.WithOnErrorHook(func(err error) {\n\t\t\tlog.Printf(\"logging error: %v\", err)\n\t\t}),\n\t\tserver.WithOnErrorHook(func(err error) {\n\t\t\t// Do a second action with err.\n\t\t}),\n\t)\n}\n```\n\nThis error hooks are very handy to log errors, so the user can use custom loggers/metrics registries. All the hooks can be\nregistered multiple times. The logic behind will just execute the hooks in order.\n\n## Graceful shutdown\n\nThe current implementation supports a graceful shutdown\nas described\nin [RFC6455](https://datatracker.ietf.org/doc/html/rfc6455#section-1.4). One side of the connections initiates the shutdown procedure,\nsignaling it's not going to send more messages and waiting for the same reply from the other peer. Once all processing logic (like message\nhandlers)\nend, the library just returns the control to the user. This implementation works in a best-effort way, as many things can go wrong in the\nmiddle of the process. Users of the library are encouraged to not rely on this shutdown procedure regarding data integrity.\n\nLet's see the shutdown sequence when a **client initiates** the shutdown procedure:\n\n```mermaid\nsequenceDiagram\nparticipant C1 as Client-1\nparticipant S as Server(c-1)\nNote left of C1: Status = Running\nNote left of C1: Status = Closing\nC1-\u003e\u003eS: Close signal\nNote right of S: Status = Running\nalt if server is responsive\nS-\u003e\u003eC1: Close signal\npar\nNote right of S: Status = Running\nS-\u003e\u003eS: Processing shutdown for c1\nS-\u003e\u003eC1: Connection close for c1\nend\nend\nC1-\u003e\u003eC1: Processing shutdown\nalt in case server is unresponsive (no previous close signal received)\nC1-\u003e\u003eS: Connection close\nend\nNote left of C1: Status = Closed\nNote right of S: Status = Running\nC1-\u003e\u003eC1: Returns to user\n```\n\nThe server never changes its status, as the initiator of the shutdown was only one client. So only the \"connection slot\" and processing for\nthat client is removed from the server.\n\nLet's check now the sequence when the **server initiates** the shutdown process:\n\n```mermaid\nsequenceDiagram\nparticipant S as Server\nparticipant C1 as Client-1\nparticipant C2 as Client-2\nNote left of S: Status = Running\nNote left of S: Status = Closing\npar\nNote right of C1: Status = Running\nS-\u003e\u003eC1: Close signal\nNote right of C1: Status = Closing\nNote right of C2: Status = Running\nS-\u003e\u003eC2: Close signal\nNote right of C2: Status = Closing\nend\nalt clients are responsive (some of them could timeout)\npar\nC1-\u003e\u003eS: Close signal\nC1-\u003e\u003eC1: Processing shutdown\nNote right of C1: Status = Closed\n\nC2-\u003e\u003eS: Close signal\nC2-\u003e\u003eC2: Processing shutdown \nNote right of C2: Status = Closed\n\nend\nend\nS-\u003e\u003eS: Processing shutdown \nNote left of S: Status = Closed\nS-\u003e\u003eS: Return to user\n```\n\nAs we can see the process is similar, but in this case, the shutdown process its executed over all clients connections at the same time, in\nparallel.\n\n## Observability\n\n### Prometheus metrics\n\nBy default, this library assumes no metrics. However, it provides tools for a quick instrumentation with\n[Prometheus](https://prometheus.io/) metrics for both, [clients](metrics/client.go) and [servers](metrics/server.go). Let's see an\nexample of a metered client:\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\n\t\"go.eloylp.dev/goomerang/client\"\n\t\"go.eloylp.dev/goomerang/metrics\"\n)\n\nfunc main() {\n\t// Metrics creation, with the default config.\n\tm := metrics.NewClientMetrics(metrics.DefaultClientConfig())\n\n\t// Register the metrics in Prometheus registry. This time, the global one.\n\tm.Register(prometheus.DefaultRegisterer)\n\n\t// Use the same options as in a normal client constructor.\n\tc, err := client.NewMetered(m,\n\t\tclient.WithServerAddr(\"127.0.0.1:9090\"),\n\t\tclient.WithMaxConcurrency(5),\n\t\t// ...\n\t)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// ...\n}\n```\n\nNote in the above example, the `client.NewMetered(...)` accepts as second argument the same type of [options](client/opts.go) exposed in the\n[basic example](#basic-usage).\n\nAs commented in the [middleware section](#middlewares), the user also has access to all low level observability middlewares in\nthe [middleware package](middleware). Its encouraged for users to take a look there in case further customization its needed.\n\nThe same, symmetric interface can be found in the server public API.\n\n### Grafana dashboard\n\n![img.png](docs/grafana-dashboard.png)\n\nThis library provides an\nout-of-the-box [Grafana](https://grafana.com/) [dashboard](internal/lab/grafana/provisioning/dashboards/goomerang.json) which should cover\nthe\nbasic usage. Users of the library are encouraged to adapt it at discretion.\n\n## Contributing\n\nContributions are welcome ! If you think something could be improved, request a new feature or just want to leave some feedback,\nplease check our [contributing](CONTRIBUTING.md) guide. ","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feloylp%2Fgoomerang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feloylp%2Fgoomerang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feloylp%2Fgoomerang/lists"}