New service type
Mikros supports four built-ins (grpc, http, native, script).
When your runtime model doesn’t fit those shapes, you can add a custom service type that behaves like a first-class citizen: it participates in the same lifecycle, uses the same logging/error/env APIs, validates configuration early, and shuts down gracefully.
This page explains the process and provides a minimal, production-quality example.
What it means to add a service type
A custom service kind is a Go type that implements the plugin.Service interface and (optionally) plugin.ServiceSettings to load its own [services.<kind>] configuration from service.toml.
// Required
type Service interface {
Name() string
Info() []flogger.Attribute
Initialize(ctx context.Context, opt *ServiceOptions) error
Run(ctx context.Context, srv interface{}) error
Stop(ctx context.Context) error
}
// Optional
type ServiceSettings interface {
Definitions(path string) (definition.ExternalServiceEntry, error)
}Mikros passes a fully-populated ServiceOptions to your implementation:
type ServiceOptions struct {
Port service.ServerPort // If your kind binds a port (servers)
Type definition.ServiceType // The declared type (e.g., "cronjob")
Name service.Name // Logical service name (from service.toml)
Product string
Logger flogger.LoggerAPI // Structured logger
Errors ferrors.ErrorAPI // Standardized error API
ServiceContext *mcontext.ServiceContext // Shared runtime context
Tags map[string]string // Runtime tags
Service options.ServiceOptions // Framework-level knobs
Definitions *definition.Definitions // Parsed service.toml
Features *FeatureSet // Resolved features
ServiceHandler interface{} // Your handler/root object (if any)
Env fenv.EnvAPI // Env loader/validator
}The Run method receives an interface{} object which points to the service main structure, so the service implementation can deal with somehow, if needed.
Configuration shape
Declare your kind in types and place your settings under [services.<kind>]. (If your kind is not a server, omit the :port suffix; Go supports type:port for server kinds.)
name = "jobs"
language = "go"
product = "SDS"
version = "0.1.0"
types = ["cronjob"]
[services.cronjob]
frequency = "weekly"
scheduled_times = ["02:00", "14:00"]
days = ["monday"]Internally, Mikros validates that your type is supported. If you’re adding a brand-new string (e.g.,
"cronjob"), make sure your bootstrap path adds it to supported types before validation (see your application entrypoint).
Logging, errors, and validation
- Use
opt.Loggerfor structured logs. Prefer attributes (key/value) over ad-hoc strings for better searchability. - Use
opt.Errorsto wrap unexpected failures into framework errors (Internal,NotFound, etc.) with attributes. - Validate vigorously in
Definitions.Validate()andInitialize; fail fast rather than letting bad config reach the hot path.
Health & readiness
- If your kind binds sockets or depends on downstreams, only report readiness after the dependency graph is up.
- Native kinds that do background work should expose internal health signals (counters, last success timestamp) via your observability features.
- Scripts don’t need liveness; exit codes suffice.
Common patterns
- Server kinds (websocket, custom TCP): respect
ServiceOptions.Port. Bind listeners inInitializeorRun, and blockRunwhile serving;Stopshould close listeners and drain. - Background kinds (schedulers, consumers): keep
Runnon-blocking with a cancelable loop;Stopshould cancel and drain quickly. - Hybrid: if your service also has
httpintypes, put shared resources inInitializeand coordinate shutdown via the provided context.
Example: cronjob service type
A small service type that runs scheduled tasks at configured times. Here we demonstrate:
- Implementing
plugin.Service - Loading typed config with
plugin.ServiceSettings - Graceful start/stop using
context.Context - Logging and error APIs
Define typed settings
Implement definition.ExternalServiceEntry so Mikros can validate your block.
package cronjob
import (
"fmt"
)
// Config under: [services.cronjob]
type Defs struct {
Frequency string `toml:"frequency"`
ScheduledTimes []string `toml:"scheduled_times"` // "HH:MM" (24h)
Days []string `toml:"days"` // ["monday", ...]
}
func (d *Defs) Name() string {
return "cronjob"
}
// Validate is called by Mikros after decoding.
func (d *Defs) Validate() error {
if d.Frequency == "" {
return fmt.Errorf("frequency is required")
}
if len(d.ScheduledTimes) == 0 {
return fmt.Errorf("at least one scheduled time is required")
}
if len(d.Days) == 0 {
return fmt.Errorf("at least one day is required")
}
return nil
}Implement the service type
package cronjob
import (
"context"
"time"
"strings"
merrors "github.com/mikros-dev/mikros/apis/features/errors"
logger_api "github.com/mikros-dev/mikros/apis/features/logger"
"github.com/mikros-dev/mikros/components/definition"
"github.com/mikros-dev/mikros/components/logger"
"github.com/mikros-dev/mikros/components/plugin"
)
// An options structure example to be used the service is being created.
type Options struct {}
func (o *Options) Kind() definition.ServiceType {
t := definition.CreateServiceType("cronjob")
return t
}
type Service struct {
defs Defs
log logger_api.LoggerAPI
errs merrors.ErrorAPI
// internal state for shutdown
cancel context.CancelFunc
}
// Ensure interface compliance.
var _ plugin.Service = (*Service)(nil)
var _ plugin.ServiceSettings = (*Service)(nil)
func New() *Service {
return &Service{}
}
func (s *Service) Name() string {
return "cronjob"
}
func (s *Service) Info() []logger_api.Attribute {
return []logger_api.Attribute{
logger.String("service.kind", s.Name()),
logger.String("frequency", s.defs.Frequency),
}
}
// Load [services.cronjob] into s.defs as typed configuration.
func (s *Service) Definitions(path string) (definition.ExternalServiceEntry, error) {
type definitions struct {
Services struct {
Config Defs `toml:"cronjob"`
} `toml:"services"`
}
var defs definitions
if err := definition.ParseExternalDefinitions(path, &defs); err != nil {
return nil, err
}
s.defs = defs.Services.Config
return &s.defs, nil
}
func (s *Service) Initialize(ctx context.Context, opt *plugin.ServiceOptions) error {
s.log = opt.Logger
s.errs = opt.Errors
s.log.Info(ctx, "cronjob initialize",
logger.String("frequency", s.defs.Frequency),
logger.Any("times", len(s.defs.ScheduledTimes)),
logger.Any("days", len(s.defs.Days)),
)
return nil
}
func (s *Service) Run(ctx context.Context, _ interface{}) error {
// Create a child context we can cancel from Stop()
runCtx, cancel := context.WithCancel(ctx)
s.cancel = cancel
// Start scheduler loop (non-blocking example; adjust as needed)
go s.scheduler(runCtx)
// Block until parent context is done (Mikros handles signals)
<-runCtx.Done()
return nil
}
func (s *Service) Stop(ctx context.Context) error {
s.log.Info(ctx, "cronjob stopping")
if s.cancel != nil {
s.cancel()
}
// Drain/cleanup here if you hold resources
return nil
}
// --- internals ---
func (s *Service) scheduler(ctx context.Context) {
// A minimal loop. In real code, compute next fire time precisely.
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
s.log.Info(ctx, "cronjob scheduler started")
for {
select {
case <-ctx.Done():
s.log.Info(ctx, "cronjob scheduler stopped")
return
case <-ticker.C:
// Decide if "now" matches one of the configured times/days, then run the job.
if s.shouldRun(time.Now()) {
s.runJob(ctx)
}
}
}
}
func (s *Service) shouldRun(now time.Time) bool {
// Simplified: check weekday and HH:MM matches any configured time.
// Implement exact matching and time zone handling for production.
var (
weekday = strings.ToLower(now.Weekday().String()) // e.g., "monday"
hhmm = now.Format("15:04")
dayOK, timeOK bool
)
for _, d := range s.defs.Days {
if d == weekday {
dayOK = true
break
}
}
for _, t := range s.defs.ScheduledTimes {
if t == hhmm {
timeOK = true
break
}
}
return dayOK && timeOK
}
func (s *Service) runJob(ctx context.Context) {
// Your job logic here. Always honor ctx and bound latencies.
s.log.Info(ctx, "cronjob tick fired")
// On unexpected errors, wrap with the framework error API:
// err := doWork(ctx)
// if err != nil {
// _ = s.errs.Internal(err).Submit(ctx)
// }
}
// Register a set containing the custom service type to be registered inside
// mikros.
func Register() *plugin.ServiceSet {
services := plugin.NewServiceSet()
services.Register(New())
return services
}Notes
Runmay block (servers) or run non-blocking and return immediately while workers run in the background. Choose the model that makes sense for your kind; always honorctx.Done().- Keep
Stopidempotent and lightweight—cancel, then let loops finish quickly. Use timeouts where appropriate.
Using the new service type
Using Mikros’ extension injection, add the new service type to your service:
package myservice
import (
"context"
"github.com/mikros-dev/mikros"
"github.com/mikros-dev/mikros/components/options"
"github.com/your-org/your-repo/cronjob" // import your service package
)
type Service struct {}
func main() {
svc := mikros.NewService(&options.NewServiceOptions{
Service: map[string]options.ServiceOptions{
"cronjob": &cronjob.Options{},
},
}).WithExternalServices(cronjob.Register()) // Register the new service
svc.Start(&service{})
}