itsy

package module
v0.0.0-...-cf2e893 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2024 License: MIT Imports: 9 Imported by: 0

README

Itsy service wrapper for NATS micro

Itsy makes it easy to create NATS micro services in Go code, with some boilerplate additions we use for our services.

Improvements over the base NATS micro interface:

  • Configuration struct that can be included in your own YAML/JSON config.
  • Configuration overrides through ITSY_* environment variables.
  • Handlers can return a Go error, which will be returned to the client.
  • Services subscribe to various topology subjects for easy targetting and debugging (see below).
  • Responses include various standard headers with information about the responding instance.
  • Easy ways to extend the service metadata through configuration or environment variables.
  • Sensible connection defaults for services, e.g. keep trying to reconnect.

Example service

A simple example service (error handling omitted).

func Run(ctx context.Context) {
	// Simple example configuration
	conf := itsy.Config{
		URL:    "nats://localhost:4222",
		Prefix: "example-services",
		Topologies: []string{
			"eu.nl.ams",  
			// ITSY_TOPO="k8s.<clusterid> foo.bar"
		},
		Meta: map[string]string{
			"foo": "bar",
			// ITSY_META_POD=foo-abc-1234
		},
	}

	s, _ := itsy.Start(itsy.Options{
		Config:        conf,
		VersionSemVer: "0.0.1",
		Name:          "itsy-example",
		Description:   "An example service",
	})

	s.MustAddHandler("echo", func(req itsy.Request) error {
		err := req.Respond(req.Data())
		return err // this will try to send an error response, if not nil
	}, nil)
	
	// Do other things here 
	// e.g. wait for the context to close 
	<-ctx.Done
}

Topologies

Topologies are extra subscription subject suffixes that allow you to target specific instances for debugging or operational needs.

The example service shown above will listen on the following NATS subjects:

  • example-services.itsy-example.echo
  • example-services.itsy-example.echo.all
  • example-services.itsy-example.echo.all.eu
  • example-services.itsy-example.echo.all.eu.nl
  • example-services.itsy-example.echo.all.eu.nl.ams
  • example-services.itsy-example.echo.any
  • example-services.itsy-example.echo.any.eu
  • example-services.itsy-example.echo.any.eu.nl
  • example-services.itsy-example.echo.any.eu.nl.ams
  • example-services.itsy-example.echo.id.VFAOuXPQKaJFU9SlETBVZ8

A service can have multiple dotted topologies and listens on all parts of the hierarchy.

When performing a request on the all subjects, all online instances that subscribe to the topic will respond.

When performing a request on the any subjects, id subject or the base subject, only one instance will respond.

For example, with two of these services running, you will see the following:

$ nats req example-services.itsy-example.echo.all --replies=2 'hello world'
15:18:07 Sending request on "example-services.itsy-example.echo.all"

15:18:07 Received with rtt 388.864µs
15:18:07 Service-Hostname: foo.local
15:18:07 Service-ID: lnmVhnFr9Ft6iNEgb3bKR9
15:18:07 Service-Name: itsy-example
15:18:07 Service-Topology: eu.nl.ams
15:18:07 Service-Version: 0.0.1

hello world


15:18:07 Received with rtt 425.988µs
15:18:07 Service-Hostname: foo.local
15:18:07 Service-ID: VFAOuXPQKaJFU9SlETBVZ8
15:18:07 Service-Name: itsy-example
15:18:07 Service-Topology: eu.nl.ams
15:18:07 Service-Version: 0.0.1

hello world

Also note the extra headers that Itsy automatically adds to all responses.

Documentation

Index

Examples

Constants

View Source
const EnvPrefix = "ITSY_"

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// URL is the NATS connection URL, e.g. "nats://user:pass@localhost:4222"
	URL string `yaml:"url" json:"url"`

	// Prefix is the service prefix, which defaults to "svc"
	Prefix string `yaml:"prefix" json:"prefix"`

	// Topologies is a list of dotted topologies to register the service under.
	// The services are registered under all parent levels, so there is no need
	// to explicitly add the parents of e.g. "eu.nl.ams".
	Topologies []string `yaml:"topologies" json:"topologies"`

	// Meta defines extra key-value string metadata that needs to be exposed in
	// the service metadata.
	Meta map[string]string `yaml:"meta" json:"meta"` // Extra metadata to expose in the service
}

Config defines a config structure that can be included in an application.

func (Config) AddEnviron

func (c Config) AddEnviron() Config

type ErrorResponse

type ErrorResponse struct {
	Code string // Code to be returned with the NATS error
	Err  error  // Actual error
}

ErrorResponse adds a NATS error response code to the error

func Wrap

func Wrap(err error, code string) ErrorResponse

Wrap wraps an error to add a custom NATS error code for error responses

func (ErrorResponse) Error

func (er ErrorResponse) Error() string

type HandlerFunc

type HandlerFunc func(req Request) error

HandlerFunc is a handler function. It differs from micro.Handler.

type HandlerOptions

type HandlerOptions struct {
	Global bool // Do not scope the subject with the service name
}

HandlerOptions are options that influence how a handler is registered. This struct is currently empty, but allows for future expansion.

type Name

type Name struct {
	Full  string // full name, e.g. "base.all.a.x"
	Base  string // base, e.g. "base"
	Extra string // extra name, e.g. "all.a.x"
	Topo  string // topology, e.g. "a.x"
	All   bool   // if this is an 'all' name for broadcast
}

func (Name) String

func (n Name) String() string

type NameList

type NameList []Name

func ExpandTopology

func ExpandTopology(base string, topo []string, id string) (result NameList)

ExpandTopology expand the base label with all levels of the provided topology strings and filters our any duplicates. The base label MUST NOT end with a dot.

Example: a base of "base" and topo ["a.b.c", "b"] with id "x123" is expanded to:

- "base" - "base.id.x123" - "base.all" - "base.all.a" - "base.all.a.x" - "base.all.a.x.y" - "base.all.b" - "base.any" - "base.any.a" - "base.any.a.x" - "base.any.a.x.y" - "base.any.b"

func (NameList) FullNames

func (n NameList) FullNames() []string

func (NameList) Len

func (n NameList) Len() int

func (NameList) Less

func (n NameList) Less(i, j int) bool

func (NameList) Lookup

func (n NameList) Lookup(fullName string) (name Name, ok bool)

func (NameList) Swap

func (n NameList) Swap(i, j int)

type Options

type Options struct {
	Config         Config
	Logger         *slog.Logger
	VersionSemVer  string // Must be SemVer compliant, if set. Consider "0.0.0+foo" for other versions.
	VersionFull    string // Can be any arbitrary string
	Name           string // Service name (required); also used for subject if SubjectName is not set
	SubjectName    string // Name part as used in subject if different from Name
	Description    string // Service description
	ConnectOptions []nats.Option
}

Options for the itsy microservice

type Request

type Request struct {
	// contains filtered or unexported fields
}

Request describes an Itsy service request. It is modelled on the NATS micro.Request interface

func (Request) Data

func (r Request) Data() []byte

Data returns request data.

func (Request) Error

func (r Request) Error(code, description string, data []byte, opts ...micro.RespondOpt) error

Error prepares and publishes error response from a handler. A response error should be set containing an error code and description. Optionally, data can be set as response payload.

func (Request) Headers

func (r Request) Headers() micro.Headers

Headers returns request headers.

func (Request) Reply

func (r Request) Reply() string

Reply returns underlying NATS message reply subject.

func (Request) Respond

func (r Request) Respond(msg []byte, opts ...micro.RespondOpt) error

Respond sends the response for the request. Additional headers can be passed using [WithHeaders] option.

func (Request) RespondErr

func (r Request) RespondErr(err error, opts ...micro.RespondOpt) error

RespondErr provides an easy way to return a Go error as error Use ErrorResponse to add a custom code. Wrap can create one for you.

func (Request) RespondJSON

func (r Request) RespondJSON(data any, opts ...micro.RespondOpt) error

RespondJSON marshals the given response value and responds to the request. Additional headers can be passed using [WithHeaders] option.

func (Request) Subject

func (r Request) Subject() string

Subject returns underlying NATS message subject.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the main object that describes the microservice

Example
package main

import (
	"fmt"
	"time"

	"github.com/PowerDNS/itsy"
	natsserver "github.com/nats-io/nats-server/v2/server"
	"github.com/nats-io/nats.go"
)

func main() {
	// Start an in-process NATS server for this example
	server, err := natsserver.NewServer(&natsserver.Options{
		DontListen: true,
	})
	check(err)
	server.Start()
	defer server.Shutdown()
	if !server.ReadyForConnections(time.Second * 5) {
		panic("NATS server didn't start")
	}

	// Simple test configuration
	conf := itsy.Config{
		Prefix: "test-itsy",
		Topologies: []string{
			"eu.nl.ams",
		},
	}
	s, err := itsy.Start(itsy.Options{
		Config:        conf,
		Logger:        nil,
		VersionSemVer: "0.0.1",
		Name:          "itsy-example",
		Description:   "An example service",
		ConnectOptions: []nats.Option{
			nats.InProcessServer(server),
		},
	})
	check(err)
	defer s.Stop()

	s.MustAddHandler("echo", func(req itsy.Request) error {
		err := req.Respond(req.Data())
		return err // this will try to send an error response, if not nil
	}, nil)

	// Connect with a client and send a request when ready
	nc, err := nats.Connect("", nats.InProcessServer(server))
	check(err)
	defer nc.Close()

	msg, err := nc.Request("test-itsy.echo.any.eu.nl.ams", []byte("hello world"), time.Second)
	check(err)
	fmt.Println("Received:", string(msg.Data))

	fmt.Println("Done")

}

func check(err error) {
	if err != nil {
		panic(err)
	}
}
Output:

Received: hello world
Done

func Start

func Start(opt Options) (*Service, error)

Start starts a new Itsy NATS Service in the background. The returned Service object can be used to register handlers and control the instance.

func (*Service) AddHandler

func (s *Service) AddHandler(name string, handler HandlerFunc, opts *HandlerOptions) error

AddHandler registers a HandlerFunc. It returns an error if the name or an option is valid.

func (*Service) Conn

func (s *Service) Conn() *nats.Conn

Conn returns the underlying NATS connection

func (*Service) ID

func (s *Service) ID() string

ID returns the unique service ID of this instance

func (*Service) MustAddHandler

func (s *Service) MustAddHandler(name string, handler HandlerFunc, opts *HandlerOptions)

MustAddHandler registers a HandlerFunc, and panics if anything goes wrong. Nothing will go wrong if the name and options are valid.

func (*Service) Stop

func (s *Service) Stop()

Stop stops the NATS service. Once stopped, the object cannot be used again.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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