leaderless gossip-based service discovery w/ consul compatibility
5

Configure Feed

Select the types of activity you want to include in your feed.

in the beginning there was darkness

Jes Olson (Feb 17, 2023, 7:36 PM -0800) e32d6443

+323
+1
.gitignore
··· 1 + ,*
+6
README
··· 1 + cascade 2 + ----------- 3 + 4 + cascade is a cli-based fully 5 + masterless distributed computing 6 + thing
+135
agent.go
··· 1 + package main 2 + 3 + import ( 4 + "log" 5 + "os" 6 + "sync" 7 + "time" 8 + 9 + "github.com/hashicorp/memberlist" 10 + "github.com/hashicorp/serf/serf" 11 + ) 12 + 13 + // Agent starts and manages a Serf & adds 14 + // service discovery 15 + type Agent struct { 16 + conf *Config 17 + serfConf *serf.Config 18 + eventCh chan serf.Event 19 + 20 + // i doubt we care about handling events, 21 + // but i'm leaving this here just in case 22 + // eventHandlerList []EventHandler 23 + // eventHandlersLock sync.Mutex 24 + 25 + // This is the underlying Serf we are wrapping 26 + serf *serf.Serf 27 + 28 + // this channel receiving a signal = shutdown 29 + shutdown bool 30 + shutdownCh chan struct{} 31 + shutdownLock sync.Mutex 32 + } 33 + 34 + func Create(conf *Config, serfConf *serf.Config) (*Agent) { 35 + // Create a channel to listen for events from Serf 36 + eventCh := make(chan serf.Event, 64) 37 + serfConf.EventCh = eventCh 38 + 39 + // Setup the agent 40 + agent := &Agent{ 41 + conf: conf, 42 + serfConf: serfConf, 43 + eventCh: eventCh, 44 + shutdownCh: make(chan struct{}), 45 + } 46 + 47 + return agent 48 + } 49 + 50 + // setupAgent calls Create 51 + func SetupAgent(config *Config) *Agent { 52 + bindIP, bindPort, err := config.AddrParts(config.BindAddr) 53 + if err != nil { 54 + log.Panic(err) 55 + } 56 + 57 + serfConfig := serf.DefaultConfig() 58 + if os.Getenv("CASCADE_BIND") != "" { 59 + config.BindAddr = os.Getenv("CASCADE_BIND") 60 + } 61 + serfConfig.MemberlistConfig.BindAddr = bindIP 62 + serfConfig.MemberlistConfig.BindPort = bindPort 63 + serfConfig.MemberlistConfig.AdvertiseAddr = "" 64 + serfConfig.MemberlistConfig.AdvertisePort = 0 65 + serfConfig.ProtocolVersion = uint8(serf.ProtocolVersionMax) 66 + serfConfig.CoalescePeriod = 3 * time.Second 67 + serfConfig.QuiescentPeriod = time.Second 68 + serfConfig.QueryResponseSizeLimit = 1024 69 + serfConfig.QuerySizeLimit = 1024 70 + serfConfig.UserEventSizeLimit = 512 71 + serfConfig.UserCoalescePeriod = 3 * time.Second 72 + serfConfig.UserQuiescentPeriod = time.Second 73 + // TODO: look at reconnect/tombstone settings w more scrutiny 74 + serfConfig.ReconnectInterval = 0 75 + serfConfig.ReconnectTimeout = 0 76 + serfConfig.TombstoneTimeout = 0 77 + serfConfig.BroadcastTimeout = 0 78 + // TODO: what are the implications of true here o_O 79 + serfConfig.EnableNameConflictResolution = true 80 + 81 + 82 + // hardcode DefaultWANConfig because cascade is designed to be 83 + // used as a single global system. 84 + serfConfig.MemberlistConfig = memberlist.DefaultWANConfig() 85 + serfConfig.MemberlistConfig.BindAddr = bindIP 86 + serfConfig.MemberlistConfig.BindPort = bindPort 87 + 88 + agent := Create(config, serfConfig) 89 + 90 + return agent 91 + } 92 + 93 + func (a *Agent) eventLoop() { 94 + serfShutdownCh := a.serf.ShutdownCh() 95 + for { 96 + select { 97 + case e := <-a.eventCh: 98 + log.Printf("[INFO] agent: Received event: %s", e.String()) 99 + 100 + case <-serfShutdownCh: 101 + log.Printf("[WARN] agent: Serf shutdown detected, quitting") 102 + a.Shutdown() 103 + return 104 + 105 + case <-a.shutdownCh: 106 + return 107 + } 108 + } 109 + } 110 + 111 + // Shutdown closes this agent and all of its processes. Should be preceded 112 + // by a Leave for a graceful shutdown. 113 + func (a *Agent) Shutdown() error { 114 + a.shutdownLock.Lock() 115 + defer a.shutdownLock.Unlock() 116 + 117 + if a.shutdown { 118 + return nil 119 + } 120 + 121 + if a.serf == nil { 122 + goto EXIT 123 + } 124 + 125 + log.Println("[INFO] agent: requesting serf shutdown") 126 + if err := a.serf.Shutdown(); err != nil { 127 + return err 128 + } 129 + 130 + EXIT: 131 + log.Println("[INFO] agent: shutdown complete") 132 + a.shutdown = true 133 + close(a.shutdownCh) 134 + return nil 135 + }
+53
config.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "net" 6 + 7 + "github.com/hashicorp/serf/serf" 8 + ) 9 + 10 + const DefaultBindPort int = 4449 11 + 12 + func DefaultConfig() *Config { 13 + return &Config{ 14 + BindAddr: "0.0.0.0", 15 + } 16 + } 17 + 18 + // Services are built on top of serf 19 + // tags. The tag format under the hood 20 + // is service=name=<name>,port=<port> 21 + type Service struct { 22 + name string 23 + port int 24 + } 25 + 26 + type Config struct { 27 + BindAddr string 28 + Services []Service 29 + SerfConf *serf.Config 30 + } 31 + 32 + // lifted from serf, could be simplified 33 + func (c *Config) AddrParts(address string) (string, int, error) { 34 + checkAddr := address 35 + 36 + START: 37 + _, _, err := net.SplitHostPort(checkAddr) 38 + if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { 39 + checkAddr = fmt.Sprintf("%s:%d", checkAddr, DefaultBindPort) 40 + goto START 41 + } 42 + if err != nil { 43 + return "", 0, err 44 + } 45 + 46 + // Get the address 47 + addr, err := net.ResolveTCPAddr("tcp", checkAddr) 48 + if err != nil { 49 + return "", 0, err 50 + } 51 + 52 + return addr.IP.String(), addr.Port, nil 53 + }
+21
go.mod
··· 1 + module git.j3s.sh/cascade 2 + 3 + go 1.20 4 + 5 + require github.com/hashicorp/serf v0.10.1 6 + 7 + require ( 8 + github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect 9 + github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect 10 + github.com/hashicorp/errwrap v1.0.0 // indirect 11 + github.com/hashicorp/go-immutable-radix v1.0.0 // indirect 12 + github.com/hashicorp/go-msgpack v0.5.3 // indirect 13 + github.com/hashicorp/go-multierror v1.1.0 // indirect 14 + github.com/hashicorp/go-sockaddr v1.0.0 // indirect 15 + github.com/hashicorp/golang-lru v0.5.0 // indirect 16 + github.com/hashicorp/memberlist v0.5.0 // indirect 17 + github.com/miekg/dns v1.1.41 // indirect 18 + github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect 19 + golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect 20 + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect 21 + )
+89
go.sum
··· 1 + github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 2 + github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= 3 + github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 4 + github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 5 + github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 6 + github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 7 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 + github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 10 + github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 11 + github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= 12 + github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 13 + github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 14 + github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 15 + github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 16 + github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 17 + github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= 18 + github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 19 + github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 20 + github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= 21 + github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 22 + github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= 23 + github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 24 + github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 25 + github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 26 + github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 27 + github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 28 + github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 29 + github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 30 + github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= 31 + github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= 32 + github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= 33 + github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= 34 + github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= 35 + github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 36 + github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 37 + github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 38 + github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 39 + github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 40 + github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 41 + github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 42 + github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 43 + github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= 44 + github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= 45 + github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= 46 + github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 47 + github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 48 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 + github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 50 + github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 51 + github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 52 + github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= 53 + github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 54 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 56 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 57 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 58 + golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 59 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 + golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 62 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 63 + golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc= 64 + golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 65 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 + golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 69 + golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 70 + golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 + golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 + golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 + golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 + golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= 79 + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 81 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 82 + golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 83 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 + golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 85 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 86 + golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 87 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 88 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+18
main.go
··· 1 + // design touchstones 2 + // configured entirely via environment variables 3 + // minimal configurable options 4 + // a single global cluster 5 + // easy cluster formation 6 + // todo 7 + // dns resolver for services 8 + 9 + package main 10 + 11 + import "log" 12 + 13 + func main() { 14 + // agent does everything tbh 15 + agent := SetupAgent(DefaultConfig()) 16 + log.Println(agent) 17 + // agent.Run() 18 + }