sleuth

package module
v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2021 License: MIT Imports: 12 Imported by: 2

README

sleuth

API documentation Coverage Status

sleuth is a Go library that provides master-less peer-to-peer autodiscovery and RPC between HTTP services that reside on the same network. It works with minimal configuration and provides a mechanism to join a local network both as a client that offers no services and as any service that speaks HTTP. Its primary use case is for microservices on the same network that make calls to one another.

For a full introduction and tutorial, check out: Service autodiscovery in Go with sleuth

Installation

sleuth is dependent on libzmq, which can be installed either from source or from binaries. For more information, please refer to ØMQ: "Get the Software" or the libzmq repository.

Another option is to use a Docker container that comes with Go and ZeroMQ.

Once libzmq is available on a system, sleuth can be installed like any other Go library:

go get -u github.com/FLAGlab/sleuth

API

The sleuth API documentation is available on GoDoc or you can simply run:

godoc github.com/FLAGlab/sleuth

Examples

Example (1): The echo-service is a toy service that merely echoes back anything in an HTTP request body. It has made itself available on a sleuth network:

package main

import (
  "io/ioutil"
  "net/http"

  "github.com/FLAGlab/sleuth"
)

type echoHandler struct{}

func (h *echoHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
  body, _ := ioutil.ReadAll(req.Body)
  res.Write(body)
}

func main() {
  handler := new(echoHandler)
  // In the real world, the Interface field of the sleuth.Config object
  // should be set so that all services are on the same subnet.
  config := &sleuth.Config{
    Handler: handler,
    LogLevel: "debug",
    Service: "echo-service",
  }
  server, err := sleuth.New(config)
  if err != nil {
    panic(err.Error())
  }
  defer server.Close()
  http.ListenAndServe(":9873", handler)
}

And here is a trivial client that waits until it has connected to the network and found the echo-service to make a request before it exits. Note that the *sleuth.Client works as a drop-in replacement for an *http.Client when making requests using the Do() method:

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/FLAGlab/sleuth"
)

func main() {
	service := "echo-service"
	// In the real world, the Interface field of the sleuth.Config object
	// should be set so that all services are on the same subnet.
	config := &sleuth.Config{LogLevel: "debug"}
	client, err := sleuth.New(config)
	if err != nil {
		panic(err.Error())
	}
	defer client.Close()
	client.WaitFor(service)
	input := "This is the value I am inputting."
	body := bytes.NewBuffer([]byte(input))
	request, _ := http.NewRequest("POST", "sleuth://"+service+"/", body)
	response, err := client.Do(request)
	if err != nil {
		panic(err.Error())
	}
	output, _ := ioutil.ReadAll(response.Body)
	if string(output) == input {
		fmt.Println("It works.")
	} else {
		fmt.Println("It doesn't work.")
	}
}

Example (2): sleuth-example is a fuller example of two services on a sleuth network that need to communicate with each other.

A complete tutorial based on that example can be found here: Service autodiscovery in Go with sleuth.

Test

go test -cover github.com/FLAGlab/sleuth

Q & A

Q: How does it work? I understand what sleuth does, but I want to know how it does it.

A: Services that instantiate a sleuth.Client create an ad hoc Gyre network. Gyre is the Go port of the Zyre project, which is built on top of ØMQ (ZeroMQ). Nodes in the network discover each other using a UDP beacon on port 5670. The actual communication between nodes happens on ephemeral TCP connections. What sleuth does is to manage this life cycle:

  • A peer joins the Gyre network as a member of the group SLEUTH-v1. If the peer offers a service, i.e., if it has an http.Handler, it notifies the rest of the network when it announces itself. The peer might have no service to offer, thus operating in client-only mode, or it may offer one service.
  • The peer finds other peers on the network. If you have asked the sleuth client to WaitFor() one or more services to appear before continuing, that call will block until it has found those services.
  • If the peer is offering a service, sleuth automatically listens for incoming requests in a separate goroutine and responds to incoming requests by invoking the http.Handler that was passed in during instantiation.
  • When you make a request to an available service, sleuth marshals the request, sends it to one of the available peers that offers that service, and waits for a response. If the response succeeds, it returns an http.Response; if it times out, it returns an error. The sleuth client Do() method has the same signature as the http client Do() method in order to operate as a drop-in replacement.
  • When you want to leave the network, e.g., when the application is quitting, the sleuth client Close() method immediately notifies the rest of the network that the peer is leaving. This is not strictly necessary because peers regularly check in to make sure the network knows they are alive, so the network automatically knows if a service has disappeared; but it is a good idea.

Q: What is the messaging protocol sleuth uses?

A: Under the hood, sleuth marshals HTTP requests and responses into plain JSON objects and then compresses them via gzip. Instead of adding another dependency on something like Protocol Buffers, sleuth depends on the fact that most API responses between microservices will be fairly small and it leaves the door open to ports in a wide variety of languages and environments. One hard dependency seemed quite enough.


Q: What if I have multiple instances of the same service?

A: Great! sleuth will automatically round-robin the requests each client makes to all services that share the same name.


Q: What happens if a service goes offline?

A: Whenever possible, a service should call its client's Close() method before exiting to notify the network of its departure. But even if a service fails to do that, the sleuth network's underlying Gyre network will detect within about one second that a peer has disappeared. All requests to that service will be routed to other peers offering the same service. If no peers exist for that service, then requests (which are made by calling the sleuth client Do() method) will return an unknown service error (code 919), which means that if you're already handling errors when making requests, you're covered.


Q: It doesn't work.

A: That's not a question. But have you checked to make sure your firewall allows UDP traffic on port 5670?


Q: It still doesn't work.

A: That's still not a question. But have you set the Interface field of your sleuth.Config object? The services you want to connect need to be on the same network and if you leave that field blank, the underlying Gyre network may not reside where you think it does. If you run ifconfig you'll get a list of available interfaces on your system.


Q: Why is it called sleuth?

A: Because "sleuth" is the collective noun for a group of bears: the original reason for writing this library was to connect a group of bear/forest services. Also because a sleuth searches for things and discovers them. Hence the logo:

License

sleuth is licensed under the MIT License.

The underlying libraries that sleuth relies on, Gyre and libzmq, are licensed under the LGPL. In effect, users who do not plan on modifying Gyre or libzmq can release their own applications under any license they see fit.

Resources

Documentation

Overview

Package sleuth provides master-less peer-to-peer autodiscovery and RPC between HTTP services that reside on the same network. It works with minimal configuration and provides a mechanism to join a local network both as a client that offers no services and as any service that speaks HTTP. Its primary use case is for microservices on the same network that make calls to one another.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// Timeout is the duration to wait before an outstanding request times out.
	// By default, it is set to 500ms.
	Timeout time.Duration
	// contains filtered or unexported fields
}

Client is the peer on the sleuth network that makes requests and, if a handler has been provided, responds to peer requests.

func New

func New(config *Config) (*Client, error)

New is the entry point to the sleuth package. It returns a reference to a Client object that has joined the local network. If the config argument is nil, sleuth will use sensible defaults. If the Handler attribute of the config object is not set, sleuth will operate in client-only mode.

func (*Client) Close

func (c *Client) Close() error

Close leaves the sleuth network and stops the Gyre node. It can only be called once, even if it returns an error the first time it is called.

func (*Client) Do

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do sends an HTTP request to a service and returns an HTTP response. URLs for requests use the following format:

sleuth://service-name/requested-path

For example, a request to the path /bar?baz=qux of a service called foo-service would have the URL:

sleuth://foo-service/bar?baz=qux

func (*Client) WaitFor

func (c *Client) WaitFor(services ...string) error

WaitFor blocks until the required services are available to the client.

type Config

type Config struct {

	// Handler is the HTTP handler for a service made available via sleuth.
	Handler http.Handler `json:"-"`

	// Interface is the system network interface sleuth should use, e.g. "en0".
	Interface string `json:"interface,omitempty"`

	// LogLevel is the ursiform.Logger level for sleuth. The default is "silent".
	// The options, in order of increasing verbosity, are:
	// "silent"    No log output at all.
	// "error"     Only errors are logged.
	// "blocked"   Blocking calls and lower are logged.
	// "unblocked" Unblocked notifications and lower are logged.
	// "warn"      Warnings and lower are logged.
	// "reject"    Rejections (e.g., in a firewall) and lower are logged.
	// "listen"    Listeners and lower are logged.
	// "install"   Install notifications and lower are logged.
	// "init"      Initialization notifications and lower are logged.
	// "request"   Incoming requests and lower are logged.
	// "info"      Info output and lower are logged.
	// "debug"     All log output is shown.
	LogLevel string `json:"loglevel,omitempty"`

	// Port is the UDP port that sleuth should broadcast on. The default is 5670.
	Port int `json:"port,omitempty"`

	// Service is the name of the service being offered if a Handler exists.
	Service string `json:"service,omitempty"`

	// Version is the optional version string of the service being offered.
	Version string `json:"version,omitempty"`
	// contains filtered or unexported fields
}

Config is the configuration specification for sleuth client instantiation. It has JSON tag values defined for all public fields except Handler in order to allow users to store sleuth configuration in JSON files. All fields are optional, but Interface is particularly important to guarantee all peers reside on the same subnet.

type Error

type Error struct {
	// Codes contains the list of error codes that led to a specific error.
	Codes []int
	// contains filtered or unexported fields
}

Error is the type all sleuth errors can be asserted as in order to query the error code trace that resulted in any particular error.

Example
package main

import (
	"fmt"

	"github.com/FLAGlab/sleuth"
)

func main() {
	config := &sleuth.Config{Interface: "bad"}
	if _, err := sleuth.New(config); err != nil {
		fmt.Printf("%v", err.(*sleuth.Error).Codes)
	}
}
Output:

[905 901]

func (*Error) Error

func (e *Error) Error() string

Error returns an error string.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL