// Copyright 2022-2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package micro import ( "encoding/json" "errors" "fmt" "regexp" "strings" "sync" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nuid" ) type ( // Service exposes methods to operate on a service instance. Service interface { // AddEndpoint registers endpoint with given name on a specific subject. AddEndpoint(string, Handler, ...EndpointOpt) error // AddGroup returns a Group interface, allowing for more complex endpoint topologies. // A group can be used to register endpoints with given prefix. AddGroup(string, ...GroupOpt) Group // Info returns the service info. Info() Info // Stats returns statistics for the service endpoint and all monitoring endpoints. Stats() Stats // Reset resets all statistics (for all endpoints) on a service instance. Reset() // Stop drains the endpoint subscriptions and marks the service as stopped. Stop() error // Stopped informs whether [Stop] was executed on the service. Stopped() bool } // Group allows for grouping endpoints on a service. // // Endpoints created using AddEndpoint will be grouped under common prefix (group name) // New groups can also be derived from a group using AddGroup. Group interface { // AddGroup creates a new group, prefixed by this group's prefix. AddGroup(string, ...GroupOpt) Group // AddEndpoint registers new endpoints on a service. // The endpoint's subject will be prefixed with the group prefix. AddEndpoint(string, Handler, ...EndpointOpt) error } EndpointOpt func(*endpointOpts) error GroupOpt func(*groupOpts) endpointOpts struct { subject string metadata map[string]string queueGroup string qgDisabled bool msgLimit int bytesLimit int } groupOpts struct { queueGroup string qgDisabled bool } // ErrHandler is a function used to configure a custom error handler for a service, ErrHandler func(Service, *NATSError) // DoneHandler is a function used to configure a custom done handler for a service. DoneHandler func(Service) // StatsHandler is a function used to configure a custom STATS endpoint. // It should return a value which can be serialized to JSON. StatsHandler func(*Endpoint) any // ServiceIdentity contains fields helping to identity a service instance. ServiceIdentity struct { Name string `json:"name"` ID string `json:"id"` Version string `json:"version"` Metadata map[string]string `json:"metadata"` } // Stats is the type returned by STATS monitoring endpoint. // It contains stats of all registered endpoints. Stats struct { ServiceIdentity Type string `json:"type"` Started time.Time `json:"started"` Endpoints []*EndpointStats `json:"endpoints"` } // EndpointStats contains stats for a specific endpoint. EndpointStats struct { Name string `json:"name"` Subject string `json:"subject"` QueueGroup string `json:"queue_group"` NumRequests int `json:"num_requests"` NumErrors int `json:"num_errors"` LastError string `json:"last_error"` ProcessingTime time.Duration `json:"processing_time"` AverageProcessingTime time.Duration `json:"average_processing_time"` Data json.RawMessage `json:"data,omitempty"` } // Ping is the response type for PING monitoring endpoint. Ping struct { ServiceIdentity Type string `json:"type"` } // Info is the basic information about a service type. Info struct { ServiceIdentity Type string `json:"type"` Description string `json:"description"` Endpoints []EndpointInfo `json:"endpoints"` } EndpointInfo struct { Name string `json:"name"` Subject string `json:"subject"` QueueGroup string `json:"queue_group"` Metadata map[string]string `json:"metadata"` } // Endpoint manages a service endpoint. Endpoint struct { EndpointConfig Name string service *service stats EndpointStats subscription *nats.Subscription } group struct { service *service prefix string queueGroup string queueGroupDisabled bool } // Verb represents a name of the monitoring service. Verb int64 // Config is a configuration of a service. Config struct { // Name represents the name of the service. Name string `json:"name"` // Endpoint is an optional endpoint configuration. // More complex, multi-endpoint services can be configured using // Service.AddGroup and Service.AddEndpoint methods. Endpoint *EndpointConfig `json:"endpoint"` // Version is a SemVer compatible version string. Version string `json:"version"` // Description of the service. Description string `json:"description"` // Metadata annotates the service Metadata map[string]string `json:"metadata,omitempty"` // QueueGroup can be used to override the default queue group name. QueueGroup string `json:"queue_group"` // QueueGroupDisabled disables the queue group for the service. QueueGroupDisabled bool `json:"queue_group_disabled"` // StatsHandler is a user-defined custom function. // used to calculate additional service stats. StatsHandler StatsHandler // DoneHandler is invoked when all service subscription are stopped. DoneHandler DoneHandler // ErrorHandler is invoked on any nats-related service error. ErrorHandler ErrHandler } EndpointConfig struct { // Subject on which the endpoint is registered. Subject string // Handler used by the endpoint. Handler Handler // Metadata annotates the service Metadata map[string]string `json:"metadata,omitempty"` // QueueGroup can be used to override the default queue group name. QueueGroup string `json:"queue_group"` // QueueGroupDisabled disables the queue group for the endpoint. QueueGroupDisabled bool `json:"queue_group_disabled"` } // NATSError represents an error returned by a NATS Subscription. // It contains a subject on which the subscription failed, so that // it can be linked with a specific service endpoint. NATSError struct { Subject string Description string err error } // service represents a configured NATS service. // It should be created using [Add] in order to configure the appropriate NATS subscriptions // for request handler and monitoring. service struct { // Config contains a configuration of the service Config m sync.Mutex id string endpoints []*Endpoint verbSubs map[string]*nats.Subscription started time.Time nc *nats.Conn natsHandlers handlers stopped bool asyncDispatcher asyncCallbacksHandler } handlers struct { closed nats.ConnHandler asyncErr nats.ErrHandler } asyncCallbacksHandler struct { cbQueue chan func() closed bool } ) const ( // Queue Group name used across all services DefaultQueueGroup = "q" // APIPrefix is the root of all control subjects APIPrefix = "$SRV" ) // Service Error headers const ( ErrorHeader = "Nats-Service-Error" ErrorCodeHeader = "Nats-Service-Error-Code" ) // Verbs being used to set up a specific control subject. const ( PingVerb Verb = iota StatsVerb InfoVerb ) const ( InfoResponseType = "io.nats.micro.v1.info_response" PingResponseType = "io.nats.micro.v1.ping_response" StatsResponseType = "io.nats.micro.v1.stats_response" ) var ( // this regular expression is suggested regexp for semver validation: https://semver.org/ semVerRegexp = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) nameRegexp = regexp.MustCompile(`^[A-Za-z0-9\-_]+$`) subjectRegexp = regexp.MustCompile(`^[^ >]*[>]?$`) ) // Common errors returned by the Service framework. var ( // ErrConfigValidation is returned when service configuration is invalid ErrConfigValidation = errors.New("validation") // ErrVerbNotSupported is returned when invalid [Verb] is used (PING, INFO, STATS) ErrVerbNotSupported = errors.New("unsupported verb") // ErrServiceNameRequired is returned when attempting to generate control subject with ID but empty name ErrServiceNameRequired = errors.New("service name is required to generate ID control subject") ) func (s Verb) String() string { switch s { case PingVerb: return "PING" case StatsVerb: return "STATS" case InfoVerb: return "INFO" default: return "" } } // AddService adds a microservice. // It will enable internal common services (PING, STATS and INFO). // Request handlers have to be registered separately using Service.AddEndpoint. // A service name, version and Endpoint configuration are required to add a service. // AddService returns a [Service] interface, allowing service management. // Each service is assigned a unique ID. func AddService(nc *nats.Conn, config Config) (Service, error) { if err := config.valid(); err != nil { return nil, err } if config.Metadata == nil { config.Metadata = map[string]string{} } id := nuid.Next() svc := &service{ Config: config, nc: nc, id: id, asyncDispatcher: asyncCallbacksHandler{ cbQueue: make(chan func(), 100), }, verbSubs: make(map[string]*nats.Subscription), endpoints: make([]*Endpoint, 0), } // Add connection event (closed, error) wrapper handlers. If the service has // custom callbacks, the events are queued and invoked by the same // goroutine, starting now. go svc.asyncDispatcher.run() svc.wrapConnectionEventCallbacks() if config.Endpoint != nil { opts := []EndpointOpt{WithEndpointSubject(config.Endpoint.Subject)} if config.Endpoint.Metadata != nil { opts = append(opts, WithEndpointMetadata(config.Endpoint.Metadata)) } if config.Endpoint.QueueGroup != "" { opts = append(opts, WithEndpointQueueGroup(config.Endpoint.QueueGroup)) } else if config.QueueGroup != "" { opts = append(opts, WithEndpointQueueGroup(config.QueueGroup)) } if err := svc.AddEndpoint("default", config.Endpoint.Handler, opts...); err != nil { svc.asyncDispatcher.close() return nil, err } } // Setup internal subscriptions. pingResponse := Ping{ ServiceIdentity: svc.serviceIdentity(), Type: PingResponseType, } handleVerb := func(verb Verb, valuef func() any) func(req Request) { return func(req Request) { response, _ := json.Marshal(valuef()) if err := req.Respond(response); err != nil { if err := req.Error("500", fmt.Sprintf("Error handling %s request: %s", verb, err), nil); err != nil && config.ErrorHandler != nil { svc.asyncDispatcher.push(func() { config.ErrorHandler(svc, &NATSError{Subject: req.Subject(), Description: err.Error(), err: err}) }) } } } } for verb, source := range map[Verb]func() any{ InfoVerb: func() any { return svc.Info() }, PingVerb: func() any { return pingResponse }, StatsVerb: func() any { return svc.Stats() }, } { handler := handleVerb(verb, source) if err := svc.addVerbHandlers(nc, verb, handler); err != nil { svc.asyncDispatcher.close() return nil, err } } svc.started = time.Now().UTC() return svc, nil } func (s *service) AddEndpoint(name string, handler Handler, opts ...EndpointOpt) error { var options endpointOpts for _, opt := range opts { if err := opt(&options); err != nil { return err } } subject := name if options.subject != "" { subject = options.subject } queueGroup, noQueue := resolveQueueGroup(options.queueGroup, s.Config.QueueGroup, options.qgDisabled, s.Config.QueueGroupDisabled) return addEndpoint(s, name, subject, handler, options.metadata, queueGroup, noQueue, options.msgLimit, options.bytesLimit) } func addEndpoint(s *service, name, subject string, handler Handler, metadata map[string]string, queueGroup string, noQueue bool, msgLimit, bytesLimit int) error { if !nameRegexp.MatchString(name) { return fmt.Errorf("%w: invalid endpoint name", ErrConfigValidation) } if !subjectRegexp.MatchString(subject) { return fmt.Errorf("%w: invalid endpoint subject", ErrConfigValidation) } if !subjectRegexp.MatchString(queueGroup) { return fmt.Errorf("%w: invalid endpoint queue group", ErrConfigValidation) } endpoint := &Endpoint{ service: s, EndpointConfig: EndpointConfig{ Subject: subject, Handler: handler, Metadata: metadata, QueueGroup: queueGroup, QueueGroupDisabled: noQueue, }, Name: name, } var sub *nats.Subscription var err error var options = endpointOpts{ msgLimit: msgLimit, bytesLimit: bytesLimit, } if !noQueue { sub, err = s.nc.QueueSubscribe( subject, queueGroup, func(m *nats.Msg) { s.reqHandler(endpoint, &request{msg: m}) }, ) } else { sub, err = s.nc.Subscribe( subject, func(m *nats.Msg) { s.reqHandler(endpoint, &request{msg: m}) }, ) } if err != nil { return err } // Apply pending limits if configured if options.msgLimit != 0 || options.bytesLimit != 0 { if err := sub.SetPendingLimits(options.msgLimit, options.bytesLimit); err != nil { return err } } s.m.Lock() endpoint.subscription = sub s.endpoints = append(s.endpoints, endpoint) endpoint.stats = EndpointStats{ Name: name, Subject: subject, QueueGroup: queueGroup, } s.m.Unlock() return nil } func (s *service) AddGroup(name string, opts ...GroupOpt) Group { var o groupOpts for _, opt := range opts { opt(&o) } queueGroup, noQueue := resolveQueueGroup(o.queueGroup, s.Config.QueueGroup, o.qgDisabled, s.Config.QueueGroupDisabled) return &group{ service: s, prefix: name, queueGroup: queueGroup, queueGroupDisabled: noQueue, } } // dispatch is responsible for calling any async callbacks func (ac *asyncCallbacksHandler) run() { for { f, ok := <-ac.cbQueue if !ok || f == nil { return } f() } } // dispatch is responsible for calling any async callbacks func (ac *asyncCallbacksHandler) push(f func()) { ac.cbQueue <- f } func (ac *asyncCallbacksHandler) close() { if ac.closed { return } close(ac.cbQueue) ac.closed = true } func (c *Config) valid() error { if !nameRegexp.MatchString(c.Name) { return fmt.Errorf("%w: service name: name should not be empty and should consist of alphanumerical characters, dashes and underscores", ErrConfigValidation) } if !semVerRegexp.MatchString(c.Version) { return fmt.Errorf("%w: version: version should not be empty should match the SemVer format", ErrConfigValidation) } if c.QueueGroup != "" && !subjectRegexp.MatchString(c.QueueGroup) { return fmt.Errorf("%w: queue group: invalid queue group name", ErrConfigValidation) } return nil } func (s *service) wrapConnectionEventCallbacks() { s.m.Lock() defer s.m.Unlock() s.natsHandlers.closed = s.nc.ClosedHandler() if s.natsHandlers.closed != nil { s.nc.SetClosedHandler(func(c *nats.Conn) { s.Stop() s.natsHandlers.closed(c) }) } else { s.nc.SetClosedHandler(func(c *nats.Conn) { s.Stop() }) } s.natsHandlers.asyncErr = s.nc.ErrorHandler() if s.natsHandlers.asyncErr != nil { s.nc.SetErrorHandler(func(c *nats.Conn, sub *nats.Subscription, err error) { if sub == nil { s.natsHandlers.asyncErr(c, sub, err) return } endpoint, match := s.matchSubscriptionSubject(sub.Subject) if !match { s.natsHandlers.asyncErr(c, sub, err) return } if s.Config.ErrorHandler != nil { s.Config.ErrorHandler(s, &NATSError{ Subject: sub.Subject, Description: err.Error(), err: err, }) } s.m.Lock() if endpoint != nil { endpoint.stats.NumErrors++ endpoint.stats.LastError = err.Error() } s.m.Unlock() if stopErr := s.Stop(); stopErr != nil { s.natsHandlers.asyncErr(c, sub, errors.Join(err, fmt.Errorf("stopping service: %w", stopErr))) } else { s.natsHandlers.asyncErr(c, sub, err) } }) } else { s.nc.SetErrorHandler(func(c *nats.Conn, sub *nats.Subscription, err error) { if sub == nil { return } endpoint, match := s.matchSubscriptionSubject(sub.Subject) if !match { return } if s.Config.ErrorHandler != nil { s.Config.ErrorHandler(s, &NATSError{ Subject: sub.Subject, Description: err.Error(), err: err, }) } s.m.Lock() if endpoint != nil { endpoint.stats.NumErrors++ endpoint.stats.LastError = err.Error() } s.m.Unlock() s.Stop() }) } } func unwrapConnectionEventCallbacks(nc *nats.Conn, handlers handlers) { if nc.IsClosed() { return } nc.SetClosedHandler(handlers.closed) nc.SetErrorHandler(handlers.asyncErr) } func (s *service) matchSubscriptionSubject(subj string) (*Endpoint, bool) { s.m.Lock() defer s.m.Unlock() for _, verbSub := range s.verbSubs { if verbSub.Subject == subj { return nil, true } } for _, e := range s.endpoints { if matchEndpointSubject(e.Subject, subj) { return e, true } } return nil, false } func matchEndpointSubject(endpointSubject, literalSubject string) bool { subjectTokens := strings.Split(literalSubject, ".") endpointTokens := strings.Split(endpointSubject, ".") if len(endpointTokens) > len(subjectTokens) { return false } for i, et := range endpointTokens { if i == len(endpointTokens)-1 && et == ">" { return true } if et != subjectTokens[i] && et != "*" { return false } } // Without a trailing ">", every subject token must be consumed; otherwise a // shorter endpoint would over-match a longer subject (e.g. "foo" vs // "foo.bar"). return len(endpointTokens) == len(subjectTokens) } // addVerbHandlers generates control handlers for a specific verb. // Each request generates 3 subscriptions, one for the general verb // affecting all services written with the framework, one that handles // all services of a particular kind, and finally a specific service instance. func (svc *service) addVerbHandlers(nc *nats.Conn, verb Verb, handler HandlerFunc) error { name := fmt.Sprintf("%s-all", verb.String()) if err := svc.addInternalHandler(nc, verb, "", "", name, handler); err != nil { return err } name = fmt.Sprintf("%s-kind", verb.String()) if err := svc.addInternalHandler(nc, verb, svc.Config.Name, "", name, handler); err != nil { return err } return svc.addInternalHandler(nc, verb, svc.Config.Name, svc.id, verb.String(), handler) } // addInternalHandler registers a control subject handler. func (s *service) addInternalHandler(nc *nats.Conn, verb Verb, kind, id, name string, handler HandlerFunc) error { subj, err := ControlSubject(verb, kind, id) if err != nil { if stopErr := s.Stop(); stopErr != nil { return errors.Join(err, fmt.Errorf("stopping service: %w", stopErr)) } return err } s.verbSubs[name], err = nc.Subscribe(subj, func(msg *nats.Msg) { handler(&request{msg: msg}) }) if err != nil { if stopErr := s.Stop(); stopErr != nil { return errors.Join(err, fmt.Errorf("stopping service: %w", stopErr)) } return err } return nil } // reqHandler invokes the service request handler and modifies service stats func (s *service) reqHandler(endpoint *Endpoint, req *request) { start := time.Now() endpoint.Handler.Handle(req) s.m.Lock() endpoint.stats.NumRequests++ endpoint.stats.ProcessingTime += time.Since(start) avgProcessingTime := endpoint.stats.ProcessingTime.Nanoseconds() / int64(endpoint.stats.NumRequests) endpoint.stats.AverageProcessingTime = time.Duration(avgProcessingTime) if req.respondError != nil { endpoint.stats.NumErrors++ endpoint.stats.LastError = req.respondError.Error() } s.m.Unlock() } // Stop drains the endpoint subscriptions and marks the service as stopped. func (s *service) Stop() error { s.m.Lock() defer s.m.Unlock() if s.stopped { return nil } // make a copy of s.endpoints to range over in order to stop // since *Endpoint.stop manipulates s.endpoints! endpointsToStop := append(make([]*Endpoint, 0, len(s.endpoints)), s.endpoints...) for _, e := range endpointsToStop { if err := e.stop(); err != nil { return err } } var keys []string for key, sub := range s.verbSubs { keys = append(keys, key) if err := sub.Drain(); err != nil { // connection is closed so draining is not possible if errors.Is(err, nats.ErrConnectionClosed) { break } return fmt.Errorf("draining subscription for subject %q: %w", sub.Subject, err) } } for _, key := range keys { delete(s.verbSubs, key) } unwrapConnectionEventCallbacks(s.nc, s.natsHandlers) s.stopped = true if s.DoneHandler != nil { s.asyncDispatcher.push(func() { s.DoneHandler(s) }) } s.asyncDispatcher.close() return nil } func (s *service) serviceIdentity() ServiceIdentity { return ServiceIdentity{ Name: s.Config.Name, ID: s.id, Version: s.Config.Version, Metadata: s.Config.Metadata, } } // Info returns information about the service func (s *service) Info() Info { s.m.Lock() defer s.m.Unlock() endpoints := make([]EndpointInfo, 0, len(s.endpoints)) for _, e := range s.endpoints { endpoints = append(endpoints, EndpointInfo{ Name: e.Name, Subject: e.Subject, QueueGroup: e.QueueGroup, Metadata: e.Metadata, }) } return Info{ ServiceIdentity: s.serviceIdentity(), Type: InfoResponseType, Description: s.Config.Description, Endpoints: endpoints, } } // Stats returns statistics for the service endpoint and all monitoring endpoints. func (s *service) Stats() Stats { s.m.Lock() defer s.m.Unlock() stats := Stats{ ServiceIdentity: s.serviceIdentity(), Endpoints: make([]*EndpointStats, 0), Type: StatsResponseType, Started: s.started, } for _, endpoint := range s.endpoints { endpointStats := &EndpointStats{ Name: endpoint.stats.Name, Subject: endpoint.stats.Subject, QueueGroup: endpoint.stats.QueueGroup, NumRequests: endpoint.stats.NumRequests, NumErrors: endpoint.stats.NumErrors, LastError: endpoint.stats.LastError, ProcessingTime: endpoint.stats.ProcessingTime, AverageProcessingTime: endpoint.stats.AverageProcessingTime, } if s.StatsHandler != nil { data, _ := json.Marshal(s.StatsHandler(endpoint)) endpointStats.Data = data } stats.Endpoints = append(stats.Endpoints, endpointStats) } return stats } // Reset resets all statistics on a service instance. func (s *service) Reset() { s.m.Lock() for _, endpoint := range s.endpoints { endpoint.reset() } s.started = time.Now().UTC() s.m.Unlock() } // Stopped informs whether [Stop] was executed on the service. func (s *service) Stopped() bool { s.m.Lock() defer s.m.Unlock() return s.stopped } func (e *NATSError) Error() string { return fmt.Sprintf("%q: %s", e.Subject, e.Description) } // Unwrap returns the underlying error if any. func (e *NATSError) Unwrap() error { return e.err } // Is reports whether the target error is equal to this error. func (e *NATSError) Is(target error) bool { if e == nil { return false } if t, ok := target.(*NATSError); ok { return e.Subject == t.Subject && e.Description == t.Description } return e.err != nil && errors.Is(e.err, target) } func (g *group) AddEndpoint(name string, handler Handler, opts ...EndpointOpt) error { var options endpointOpts for _, opt := range opts { if err := opt(&options); err != nil { return err } } subject := name if options.subject != "" { subject = options.subject } endpointSubject := fmt.Sprintf("%s.%s", g.prefix, subject) if g.prefix == "" { endpointSubject = subject } queueGroup, noQueue := resolveQueueGroup(options.queueGroup, g.queueGroup, options.qgDisabled, g.queueGroupDisabled) return addEndpoint(g.service, name, endpointSubject, handler, options.metadata, queueGroup, noQueue, options.msgLimit, options.bytesLimit) } func resolveQueueGroup(customQG, parentQG string, disabled, parentDisabled bool) (string, bool) { if disabled { return "", true } if customQG != "" { return customQG, false } if parentDisabled { return "", true } if parentQG != "" { return parentQG, false } return DefaultQueueGroup, false } func (g *group) AddGroup(name string, opts ...GroupOpt) Group { var o groupOpts for _, opt := range opts { opt(&o) } queueGroup, noQueue := resolveQueueGroup(o.queueGroup, g.queueGroup, o.qgDisabled, g.queueGroupDisabled) parts := make([]string, 0, 2) if g.prefix != "" { parts = append(parts, g.prefix) } if name != "" { parts = append(parts, name) } prefix := strings.Join(parts, ".") return &group{ service: g.service, prefix: prefix, queueGroup: queueGroup, queueGroupDisabled: noQueue, } } func (e *Endpoint) stop() error { // Drain the subscription. If the connection is closed, draining is not possible // but we should still remove the endpoint from the service. if err := e.subscription.Drain(); err != nil && !errors.Is(err, nats.ErrConnectionClosed) { return fmt.Errorf("draining subscription for request handler: %w", err) } for i := 0; i < len(e.service.endpoints); i++ { if e.service.endpoints[i].Subject == e.Subject { if i != len(e.service.endpoints)-1 { e.service.endpoints = append(e.service.endpoints[:i], e.service.endpoints[i+1:]...) } else { e.service.endpoints = e.service.endpoints[:i] } i++ } } return nil } func (e *Endpoint) reset() { e.stats = EndpointStats{ Name: e.stats.Name, Subject: e.stats.Subject, } } // ControlSubject returns monitoring subjects used by the Service. // Providing a verb is mandatory (it should be one of Ping, Info or Stats). // Depending on whether kind and id are provided, ControlSubject will return one of the following: // - verb only: subject used to monitor all available services // - verb and kind: subject used to monitor services with the provided name // - verb, name and id: subject used to monitor an instance of a service with the provided ID func ControlSubject(verb Verb, name, id string) (string, error) { verbStr := verb.String() if verbStr == "" { return "", fmt.Errorf("%w: %q", ErrVerbNotSupported, verbStr) } if name == "" && id != "" { return "", ErrServiceNameRequired } if name == "" && id == "" { return fmt.Sprintf("%s.%s", APIPrefix, verbStr), nil } if id == "" { return fmt.Sprintf("%s.%s.%s", APIPrefix, verbStr, name), nil } return fmt.Sprintf("%s.%s.%s.%s", APIPrefix, verbStr, name, id), nil } func WithEndpointSubject(subject string) EndpointOpt { return func(e *endpointOpts) error { e.subject = subject return nil } } func WithEndpointMetadata(metadata map[string]string) EndpointOpt { return func(e *endpointOpts) error { e.metadata = metadata return nil } } // WithEndpointMetadataKey adds a key-value pair to the endpoints's metadata. // Prefer using WithEndpointMetadata when you have all the key-value pairs you // want to add at once or when you want to replace any existing metadata. func WithEndpointMetadataKey(key, value string) EndpointOpt { return func(e *endpointOpts) error { if e.metadata == nil { e.metadata = map[string]string{} } e.metadata[key] = value return nil } } func WithEndpointQueueGroup(queueGroup string) EndpointOpt { return func(e *endpointOpts) error { e.queueGroup = queueGroup return nil } } func WithEndpointQueueGroupDisabled() EndpointOpt { return func(e *endpointOpts) error { e.qgDisabled = true return nil } } // WithEndpointPendingLimits sets the pending limits for the endpoint's // subscription. These limits how many messages and/or bytes can be buffered in // memory before the subscription is terminated with nats.ErrSlowConsumer. // Either limit can be set to -1 to indicate no limit. func WithEndpointPendingLimits(msgLimit, bytesLimit int) EndpointOpt { return func(e *endpointOpts) error { if msgLimit == 0 && bytesLimit == 0 { return fmt.Errorf("%w: at least one pending limit must be non-zero", ErrConfigValidation) } e.msgLimit = msgLimit e.bytesLimit = bytesLimit return nil } } func WithGroupQueueGroup(queueGroup string) GroupOpt { return func(g *groupOpts) { g.queueGroup = queueGroup } } func WithGroupQueueGroupDisabled() GroupOpt { return func(g *groupOpts) { g.qgDisabled = true } }