eslgo is a FreeSWITCH™ ESL (Event Socket Library) client/server library for GoLang, written from the ground up in idiomatic Go. It supports both inbound and outbound ESL connections and is designed to handle production traffic at thousands of calls per second.
This is a fork of percipia/eslgo adding bounded outbound
linger/close-delay and — for inbound clients — TCP keepalive, an optional heartbeat, and transparent auto-reconnect (see What's different in this fork). All credit for the original library goes to Percipia and Andrew Querol — see Credits.
Long-lived inbound clients need to survive network blips, FreeSWITCH restarts and NAT drops. Enable it via InboundOptions:
opts := eslgo.DefaultInboundOptions
opts.Password = "ClueCon"
opts.Heartbeat = 20 * time.Second // active `api status` probe; 0 = off
opts.AutoReconnect = true // re-dial + re-auth after any drop
opts.OnReconnect = func() { /* optional extra per-connection setup */ }
conn, err := opts.Dial("127.0.0.1:8021")
// The conn you hold stays valid across reconnects — keep using it.What you get:
- TCP keepalive — on by default (30s;
TCPKeepAliveto tune, negative to disable). Detects half-open connections (peer crash / NAT reclaim with no FIN/RST) so a dead socket surfaces as an error instead of hanging forever. - Heartbeat (optional, configurable) — when
Heartbeat > 0, a lightweightapi statusis sent every interval and must reply withinHeartbeatTimeout; a miss drops the connection (and reconnects if enabled). Catches application-level hangs that TCP keepalive can't see. - Transparent auto-reconnect — the same
*Connis revived (swap socket, re-auth) with exponential backoff (ReconnectMinBackoff/ReconnectMaxBackoff/ReconnectMaxAttempts).OnReconnectfires after each successful reconnect. - No re-subscription in your code — on reconnect eslgo automatically re-issues your event subscriptions to FreeSWITCH, and your registered event handlers stay attached to the same
*Conn. The business layer keeps receiving events as before without subscribing again. (Events FreeSWITCH emits during the brief downtime aren't replayed — by design, since replaying stale call events is meaningless.)
A single inbound connection is a serial request/reply channel — replies aren't multiplexed, so concurrent SendCommand calls queue behind each other (event delivery is unaffected). For command-heavy workloads, Pool keeps several command connections to one FreeSWITCH and dispatches each command to the least-busy one, plus one dedicated connection for events (received once, not once per connection):
opts := eslgo.PoolOptions{CommandConns: 4, WithEventConn: true}
opts.Password = "ClueCon"
opts.AutoReconnect = true
pool, _ := eslgo.NewPool("127.0.0.1:8021", opts)
defer pool.Close()
pool.OnEvent(func(e *eslgo.Event) { /* events, delivered once */ })
pool.EnableEvents(ctx)
pool.SendCommand(ctx, command.API{Command: "status"}) // runs on the least-busy connection
conn, _ := pool.Dedicated() // or grab an isolated connection for a long commandCluster manages one resilient Pool per FreeSWITCH across many servers, aggregating every server's events into one host-tagged callback, with broadcast and per-host routing:
cl := eslgo.NewCluster()
defer cl.Close()
cl.OnEvent(func(host string, e *eslgo.Event) { /* events from every server, tagged */ })
cl.Add("10.0.0.1:8021", opts)
cl.Add("10.0.0.2:8021", opts)
cl.EnableEvents(ctx)
cl.Broadcast(ctx, command.API{Command: "status"}) // to every server
cl.On("10.0.0.1:8021").SendCommand(ctx, cmd) // to one server
// Originate a new call: the cluster load-balances (default round-robin; override
// with SetBalancer) and returns the chosen host to route the call's later commands.
host, resp, _ := cl.OriginateCall(ctx, true, aLeg, bLeg, vars)Where does
hostcome from? You never guess it. For calls you originate,OriginateCallreturns it. For inbound calls, it arrives with every event viaOnEvent(host, e). Store it alongside your own per-call state and route later commands withOn(host)— no globaluuid -> hostmap to maintain (the library keeps no per-call state, and routing by UUID would require exactly such a map).
FreeSWITCH's linger lets a socket stay open after a channel hangs up so you can still receive trailing events (CHANNEL_HANGUP_COMPLETE, CDR events, etc.). This fork adds two related pieces on top of upstream:
1. linger <seconds> command — ask FreeSWITCH to linger for a bounded number of seconds instead of the all-or-nothing linger / nolinger:
// linger for 30 seconds; pass a time.Duration
_, _ = conn.SendCommand(ctx, command.Linger{Enabled: true, Seconds: 30 * time.Second})command.Linger |
ESL sent |
|---|---|
{Enabled: true, Seconds: 30 * time.Second} |
linger 30 |
{Enabled: true} (zero duration) |
linger |
{Enabled: false} |
nolinger |
2. Conn.SetCloseDelay for outbound connections — control how long eslgo waits after FreeSWITCH disconnects before it closes the socket, giving your handler a grace window to finish processing:
conn.SetCloseDelay(30 * time.Second) // close 30s after disconnect
// SetCloseDelay(0) -> close immediately (default)
// SetCloseDelay(<0) -> never auto-close on disconnect (caller-managed)Note: both
Linger.SecondsandSetCloseDelaytake atime.Durationand are honoured at second granularity, so pass durations like30 * time.Second.
go get github.com/luoyumin/eslgo
import "github.com/luoyumin/eslgo"- Inbound ESL connection (client)
- Outbound ESL server
- Event listeners by UUID or for all events
Unique-IdApplication-UUIDJob-UUID
- Plain, JSON or XML event formats —
EnableEvents(ctx, "json")(or"xml"); all are parsed into the sameEvent(useGetName()/GetHeader(...)regardless of format) contextsupport for cancelling requests- All command types abstracted out
- Send custom data by implementing the
Commandinterface —BuildMessage() string
- Send custom data by implementing the
- Bounded
lingerand configurable close-delay for outbound connections (this fork) - Inbound TCP keepalive, optional heartbeat, and transparent auto-reconnect with subscription replay (this fork)
- Connection
Pool(concurrent commands to one server) andCluster(many servers, event aggregation, broadcast) (this fork) - Basic helpers for common tasks
- DTMF
- Call origination
- Call answer / hangup
- Audio playback
Buildable examples live under the example directory — including resilient (heartbeat + auto-reconnect), pool (concurrent commands to one server), and cluster (many servers). They are illustrative only — they expect a running FreeSWITCH and are not run or verified by the test suite; see example/README.md.
package main
import (
"context"
"fmt"
"log"
"github.com/luoyumin/eslgo"
)
func main() {
// Start listening. This is a blocking call.
log.Fatalln(eslgo.ListenAndServe(":8084", handleConnection))
}
func handleConnection(ctx context.Context, conn *eslgo.Conn, response *eslgo.RawResponse) {
fmt.Printf("Got connection! %#v\n", response)
// Place a foreground (api) call to user/100 and play an audio file as the bLeg, no exported variables.
response, err := conn.OriginateCall(
ctx,
false,
eslgo.Leg{CallURL: "user/100"},
eslgo.Leg{CallURL: "&playback(misc/ivr-to_hear_screaming_monkeys.wav)"},
map[string]string{},
)
fmt.Println("Call Originated: ", response, err)
}package main
import (
"context"
"fmt"
"time"
"github.com/luoyumin/eslgo"
)
func main() {
// Connect to FreeSWITCH.
conn, err := eslgo.Dial("127.0.0.1:8021", "ClueCon", func() {
fmt.Println("Inbound Connection Disconnected")
})
if err != nil {
fmt.Println("Error connecting", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// Place a background (bgapi) call to user/100 and play an audio file as the bLeg, no exported variables.
response, err := conn.OriginateCall(
ctx,
true,
eslgo.Leg{CallURL: "user/100"},
eslgo.Leg{CallURL: "&playback(misc/ivr-to_hear_screaming_monkeys.wav)"},
map[string]string{},
)
fmt.Println("Call Originated: ", response, err)
// Close the connection after a short sleep.
time.Sleep(60 * time.Second)
conn.ExitAndClose()
}This project is a fork of percipia/eslgo, originally written by Andrew Querol and Percipia. The original license and copyright notices are preserved in every source file and in LICENSE. Huge thanks to the upstream authors — this fork only adds the linger / close-delay features described above on top of their work.
eslgo is licensed under the Mozilla Public License 2.0 (MPL-2.0), the same license as the upstream project. See LICENSE for the full text.