Package: tomodachi
tomodachi⌁a lightweight µservice lib⌁for Python 3tomodachi[友達]means friends — 🦊🐶🐻🐯🐮🐸🐍 — a suitable name for microservices working together.✨✨eventsmessagingapipubsubsns+sqsamqphttpqueueshandlersschedulingtasksmicroservicetomodachitomodachiis a library designed to make it easy for devs to build
microservices usingasyncioon Python.Includes ready implementations to support handlers built for HTTP
requests, websockets, AWS SNS+SQS and RabbitMQ / AMQP for 🚀 event based
messaging, 🔗 intra-service communication and 🐶 watchdog handlers.HTTP request handlers (API endpoints) are sent requests via theaiohttpserver library. 🪢Events and message handlers are hooked into a message bus, such as a queue, from for example AWS (Amazon Web Services) SNS+SQS (aiobotocore), RabbitMQ / AMQP (aioamqp), etc. 📡Using the provided handler managers, the need for devs to interface with
low-level libs directly should be lower, making it more of a breeze to
focus on building the business logic. 🪄tomodachihas a featureset to meet most basic needs, for example...🦸⋯ Graceful termination of consumers, listeners and tasks to ensure smooth deployments.⏰⋯ Scheduled function execution (cron notation / time interval) for building watchdog handlers.🍔⋯ Execution middleware interface for incoming HTTP requests and received messages.💌⋯ Simple envelope building and parsing for both receiving and publishing messages.📚⋯ Logging support viastructlogwith template loggers for both "dev console" and JSON output.⛑️⋯ Loggers and handler managers built to support exception tracing, from for example Sentry.📡⋯ SQS queues with filter policies for SNS topic subscriptions filtering messages on message attributes.📦⋯ Supports SQS dead-letter queues via redrive policy -- infra orchestration from service optional.🌱⋯ Designed to be extendable -- most kinds of transport layers or event sources can be added.Quicklinks to the documentation 📖This documentation README includes information on how to get started
with services, what built-in functionality exists in this library, lists
of available configuration parameters and a few examples of service
code.Visithttps://tomodachi.dev/for
additional documentation. 📔Getting started / installationHandler types / endpoint built-ins. 🛍️HTTP and WebSocket endpointsAWS SNS+SQS event messagingRabbitMQ / AMQP messagingScheduled functions and cronService options to tweak handler managers. 🛠️Options and configuration parametersUse the features you need. 🌮Middleware functionalityFunction signature keywordsLogging and log formattersOpenTelemetry instrumentationRecommendations and examples. 🧘Good practices for running services in productionExample code and template servicesPlease note -- this library is a work in progress.🐣Considertomodachias beta software. This library follows an unregular
release schedule. There may be breaking changes between0.xversions.Usagetomodachiis used to execute service code via command line interface
or within container images. It will be installed automatically when the
package is installed in the environment.The CLI endpointtomodachiis then used to run services defined astomodachiservice classes.Start a service with its class definition defined in./service/app.pyby runningtomodachi run service/app.py. Finally stop the service with
the keyboard interrupt<ctrl+c>.The run command has some options available that can be specified with
arguments to the CLI.Most options can also be set as an environment variable value.For example setting environmentTOMODACHI_LOGGER=jsonwill yield the
same change to the logger as if running the service using the argument--logger json.🧩--loop [auto|asyncio|uvloop]🖥️TOMODACHI_LOOP=...The value for--loopcan either be set toasyncio,uvlooporauto. Theuvloopvalue can only be used if uvloop is installed in
the execution environment. Note that the defaultautovalue will
currently end up using the event loop implementation that is preferred
by the Python interpreter, which in most cases will beasyncio.🧩--production🖥️TOMODACHI_PRODUCTION=1Use--productionto disable the file watcher that restarts the service
on file changes and to hide the startup info banner.⇢recommendation✨👀⇢Highly recommended to enable this option for built docker images and
for builds of services that are to be released to any environment. The
only time you should run without the--productionoption is during
development and in local development environment.🧩--log-level [debug|info|warning|error|critical]🖥️TOMODACHI_LOG_LEVEL=...Set the minimum log level for which the loggers will emit logs to their
handlers with the--log-leveloption. By default the minimum log level
is set toinfo(which includesinfo,warning,errorandcritical, resulting in only thedebuglog records to be filtered
out).🧩--logger [console|json|python|disabled]🖥️TOMODACHI_LOGGER=...Apply the--loggeroption to change the log formatter that is used by
the library. The default valueconsoleis mostly suited for local
development environments as it provides a structured and colorized view
of log records. The console colors can be disabled by setting the env
valueNO_COLOR=1.⇢recommendation✨👀⇢For released services / images it's recommended to use thejsonoption so that you can set up structured log collection via for
example Logstash, Fluentd, Fluent Bit, Vector, etc.If you prefer to disable log output from the library you can usedisabled(and presumably add a log handler with another
implementation).Thepythonoption isn't recommended, but available if required to use
the loggers from Python's built-inloggingmodule. Note that the
built-inloggingmodule will be used any way. as the library's
loggers are both added as handlers tologging.rootand has propagation
of records through tologgingas well.🧩--custom-logger <module.attribute|module>🖥️TOMODACHI_CUSTOM_LOGGER=...If the template loggers from the option above doesnt' cut it or if you
already have your own logger (preferably astructloglogger) and
processor chain set up, you can specify a--custom-loggerwhich will
also maketomodachiuse your logger set up. This is suitable also if
your app is using a custom logging setup that would differ in output
from what thetomodachiloggers outputs.If your logger is initialized in for example the moduleyourapp.loggingand the initialized (structlog) logger is aptly
namedlogger, then use--custom-logger yourapp.logging.logger(or
set as an env valueTOMODACHI_CUSTOM_LOGGER=yourapp.logging.logger).The path to the logger attribute in the module you're specifying must
implementdebug,info,warning,error,exception,criticaland preferably alsonew(context: Dict[str, Any]) -> Logger(as that is
what primarily will be called to create (or get) a logger).Although non-nativestructlogloggers can be used as custom loggers,
it's highly recommended to specify a path that has been assigned a
value fromstructlog.wrap_loggerorstructlog.get_logger.🧩--opentelemetry-instrument🖥️TOMODACHI_OPENTELEMETRY_INSTRUMENT=1Use--opentelemetry-instrumentto enable OpenTelemetry auto
instrumentation of the service and libraries for which the environment
has installed instrumentors.Iftomodachiis installed in the environment, using the argument--opentelemetry-instrument(or setting theTOMODACHI_OPENTELEMETRY_INSTRUMENT=1env variable value) is mostly
equivalent to starting the service using theopentelemetry-instrumentCLI -- OTEL distros, configurators and instrumentors will be loaded
automatically andOTEL_*environment values will be processed in the
same way.Getting started 🏃First off -- installation usingpoetryis fully supported and battle-tested (pipworks just as fine)Installtomodachiin your preferred way, wether it bepoetry,pip,pipenv, etc. Installing the distribution will give your environment
access to thetomodachipackage for imports as well as a shortcut to
the CLI alias, which later is used to run the microservices you build.local~$pipinstalltomodachi
>...
>Installingcollectedpackages:...,...,...,tomodachi
>Successfullyinstalled.........tomodachi-x.x.xxlocal~$tomodachi--version
>tomodachix.xx.xxtomodachican be installed together with a set of "extras" that will
install a set of dependencies that are useful for different purposes.
The extras are:uvloop: for the possibility to start services with the--loop uvloopoption.protobuf: for protobuf support in envelope transformation and message serialization.aiodns: to useaiodnsas the DNS resolver foraiohttp.brotli: to usebrotlicompression inaiohttp.opentelemetry: for OpenTelemetry instrumentation support.opentelemetry-exporter-prometheus: to use the experimental OTEL meter provider for Prometheus.Services and their dependencies, together with runtime utilities liketomodachi, should preferably always be installed and run in isolated
environments like Docker containers or virtual environments.Building blocks for a service class and microservice entrypointimport tomodachiand create a class that inheritstomodachi.Service, it can be called anything... or justServiceto keep it simple.Add anameattribute to the class and give it a string value. Having anameattribute isn't required, but good practice.Define an awaitable function in the service class -- in this example we'll use it as an entrypoint to trigger code in the service by decorating it with one of the available invoker decorators. Note that a service class must have at least one decorated function available to even be recognized as a service bytomodachi run.Decide on how to trigger the function -- for example using HTTP, pub/sub or on a timed interval, then decorate your function with one of these trigger / subscription decorators, which also invokes what capabilities the service initially has.Further down you'll find a desciption of how each of the built-in
invoker decorators work and which keywords and parameters you can use to
change their behaviour.Note: Publishing and subscribing to events and messages may require
user credentials or hosting configuration to be able to access queues
and topics.For simplicity, let's do HTTP:On each POST request to/sheep, the service will wait for up to one whole second (pretend that it's performing I/O -- waiting for response on a slow sheep counting database modification, for example) and then issue a 200 OK with some data.It's also possible to query the amount of times the POST tasks has run by doing aGETrequest to the same url,/sheep.By using@tomodachi.httpan HTTP server backed byaiohttpwill be started on service start.tomodachiwill act as a middleware to route requests to the correct handlers, upgrade websocket connections and then also gracefully await connections with still executing tasks, when the service is asked to stop -- up until a configurable amount of time has passed.importasyncioimportrandomimporttomodachiclassService(tomodachi.Service):name="sleepy-sheep-counter"_sheep_count=0@tomodachi.http("POST",r"/sheep")asyncdefadd_to_sheep_count(self,request):awaitasyncio.sleep(random.random())self._sheep_count+=1return200,str(self._sheep_count)@tomodachi.http("GET",r"/sheep")asyncdefreturn_sheep_count(self,request):return200,str(self._sheep_count)Run services with:local~/code/service$tomodachirunservice.pyBeside the currently existing built-in ways of interfacing with a
service, it's possible to build additional function decorators to suit
the use-cases one may have.To give a few possible examples / ideas of functionality that could be
coded to call functions with data in similar ways:Using Redis as a task queue with configurable keys to push or pop onto.Subscribing to Kinesis or Kafka event streams and act on the data received.An abstraction around otherwise complex functionality or to unify API design.As an example to above sentence; GraphQL resolver functionality with built-in tracability and authentication management, with a unified API to application devs.Additional examples will follow with different ways to trigger functions in the serviceOf course the different ways can be used within the same class, for
example the very common use-case of having a service listening on HTTP
while also performing some kind of async pub/sub tasks.Basic HTTP based service 🌟Code for a simple service which would service data over HTTP, pretty
similar, but with a few more concepts added.importtomodachiclassService(tomodachi.Service):name="http-example"# Request paths are specified as regex for full flexibility@tomodachi.http("GET",r"/resource/(?P<id>[^/]+?)/?")asyncdefresource(self,request,id):# Returning a string value normally means 200 OKreturnf"id ={id}"@tomodachi.http("GET",r"/health")asyncdefhealth_check(self,request):# Return can also be a tuple, dict or even an aiohttp.web.Response# object for more complex responses - for example if you need to# send byte data, set your own status code or define own headersreturn{"body":"Healthy","status":200,}# Specify custom 404 catch-all response@tomodachi.http_error(status_code=404)asyncdeferror_404(self,request):return"error 404"RabbitMQ or AWS SNS+SQS event based messaging service 🐰Example of a service that calls a function when messages are published
on an AMQP topic exchange.importtomodachiclassService(tomodachi.Service):name="amqp-example"# The "message_envelope" attribute can be set on the service class to build / parse data.# message_envelope = ...# A route / topic on which the service will subscribe to via RabbitMQ / AMQP@tomodachi.amqp("example.topic")asyncdefexample_func(self,message):# Received message, fordarding the same message as response on another route / topicawaittomodachi.amqp_publish(self,message,routing_key="example.response")AMQP – Publish to exchange / routing key –tomodachi.amqp_publishawaittomodachi.amqp_publish(service,message,routing_key=routine_key,exchange_name=...)serviceis the instance of the service class (from within a handler, useself)messageis the message to publish before any potential envelope transformationrouting_keyis the routing key to use when publishing the messageexchange_nameis the exchange name for publishing the message (default: "amq.topic")For more advanced workflows, it's also possible to specify overrides for the routing key prefix or message enveloping class.AWS SNS+SQS event based messaging service 📡Example of a service using AWS SNS+SQS managed pub/sub messaging. AWS
SNS and AWS SQS together brings managed message queues for
microservices, distributed systems, and serverless applications hosted
on AWS.tomodachiservices can customize their enveloping
functionality to both unwrap incoming messages and/or to produce
enveloped messages for published events / messages. Pub/sub patterns are
great for scalability in distributed architectures, when for example
hosted in Docker on Kubernetes.importtomodachiclassService(tomodachi.Service):name="aws-example"# The "message_envelope" attribute can be set on the service class to build / parse data.# message_envelope = ...# Using the @tomodachi.aws_sns_sqs decorator to make the service create an AWS SNS topic,# an AWS SQS queue and to make a subscription from the topic to the queue as well as start# receive messages from the queue using SQS.ReceiveMessages.@tomodachi.aws_sns_sqs("example-topic",queue_name="example-queue")asyncdefexample_func(self,message):# Received message, forwarding the same message as response on another topicawaittomodachi.aws_sns_sqs_publish(self,message,topic="another-example-topic")AWS – Publish message to SNS –tomodachi.aws_sns_sqs_publishawaittomodachi.aws_sns_sqs_publish(service,message,topic=topic)serviceis the instance of the service class (from within a handler, useself)messageis the message to publish before any potential envelope transformationtopicis the non-prefixed name of the SNS topic used to publish the messageAdditional function arguments can be supplied to also includemessage_attributes, and / orgroup_id+deduplication_id.For more advanced workflows, it's also possible to specify overrides for the SNS topic name prefix or message enveloping class.AWS – Send message to SQS –tomodachi.sqs_send_messageawaittomodachi.sqs_send_message(service,message,queue_name=queue_name)serviceis the instance of the service class (from within a handler, useself)messageis the message to publish before any potential envelope transformationqueue_nameis the SQS queue url, queue ARN or non-prefixed queue name to be usedAdditional function arguments can be supplied to also includemessage_attributes, and / orgroup_id+deduplication_id.For more advanced workflows, it's also possible to set delay seconds, define a custom message body formatter, or to specify overrides for the SNS topic name prefix or message enveloping class.Scheduling, inter-communication between services, etc. ⚡️There are other examples available with code of how to use services with
self-invoking methods called on a specified interval or at specific
times / days, as well as additional examples for inter-communication
pub/sub between different services on both AMQP or AWS SNS+SQS as shown
above. See more at theexamples
folder.Run the service 😎# cli alias is set up automatically on installationlocal~/code/service$tomodachirunservice.py# alternatively using the tomodachi.run modulelocal~/code/service$python-mtomodachi.runservice.pyDefaults to output startup banner on stdout and log output on stderr.HTTP service acts like a normal web server.local~$curl-v"http://127.0.0.1:9700/resource/1234"# > HTTP/1.1 200 OK# > Content-Type: text/plain; charset=utf-8# > Server: tomodachi# > Content-Length: 9# > Date: Sun, 16 Oct 2022 13:38:02 GMT# ># > id = 1234Getting an instance of a serviceIf the a Service instance is needed outside the Service class itself, it
can be acquired withtomodachi.get_service. If multiple Service
instances exist within the same event loop, the name of the Service can
be used to get the correct one.importtomodachi# Get the instance of the active Service.service=tomodachi.get_service()# Get the instance of the Service by service name.service=tomodachi.get_service(service_name)Stopping the serviceStopping a service can be achieved by either sending aSIGINT<ctrl+c> orSIGTERMsignal to to thetomodachiPython process, or
by invoking thetomodachi.exit()function, which will initiate the
termination processing flow. Thetomodachi.exit()call can
additionally take an optional exit code as an argument, which otherwise
will default to use exit code 0.SIGINTsignal (equivalent to using <ctrl+c>)SIGTERMsignaltomodachi.exit()ortomodachi.exit(exit_code)The process' exit code can also be altered by changing the value oftomodachi.SERVICE_EXIT_CODE, however usingtomodachi.exitwith an
integer argument will override any previous value set totomodachi.SERVICE_EXIT_CODE.All above mentioned ways of initiating the termination flow of the
service will perform a graceful shutdown of the service which will try
to await open HTTP handlers and await currently running tasks using
tomodachi's scheduling functionality as well as await tasks processing
messages from queues such as AWS SQS or RabbitMQ.Some tasks may timeout during termination according to used
configuration (see options such ashttp.termination_grace_period_seconds) if they are long running tasks.
Additionally container handlers may impose additional timeouts for how
long termination are allowed to take. If no ongoing tasks are to be
awaited and the service lifecycle can be cleanly terminated the shutdown
usually happens within milliseconds.Function hooks for service lifecycle changesTo be able to initialize connections to external resources or to perform
graceful shutdown of connections made by a service, there's a few
functions a service can specify to hook into lifecycle changes of a
service.Magic function nameWhen is the function called?What is suitable to put here_start_serviceCalled before invokers / servers have started.Initialize connections to databases, etc._started_serviceCalled after invokers / server have started.Start reporting or start tasks to run once._stopping_serviceCalled on termination signal.Cancel eventual internal long-running tasks._stop_serviceCalled after tasks have gracefully finished.Close connections to databases, etc.Changes to a service settings / configuration (by for example modifying
theoptionsvalues) should be done in the__init__function instead
of in any of the lifecycle function hooks.Good practice -- in general, make use of the_start_service(for
setting up connections) in addition to the_stop_service(to close
connections) lifecycle hooks. The other hooks may be used for more
uncommon use-cases.Lifecycle functions are defined as class functions and will be called
by the tomodachi process on lifecycle changes:importtomodachiclassService(tomodachi.Service):name="example"asyncdef_start_service(self):# The _start_service function is called during initialization,# before consumers or an eventual HTTP server has started.# It's suitable to setup or connect to external resources here.returnasyncdef_started_service(self):# The _started_service function is called after invoker# functions have been set up and the service is up and running.# The service is ready to process messages and requests.returnasyncdef_stopping_service(self):# The _stopping_service function is called the moment the# service is instructed to terminate - usually this happens# when a termination signal is received by the service.# This hook can be used to cancel ongoing tasks or similar.# Note that some tasks may be processing during this time.returnasyncdef_stop_service(self):# Finally the _stop_service function is called after HTTP server,# scheduled functions and consumers have gracefully stopped.# Previously ongoing tasks have been awaited for completion.# This is the place to close connections to external services and# clean up eventual tasks you may have started previously.returnExceptions raised in_start_serviceor_started_servicewill
gracefully terminate the service.Graceful termination of a service (SIGINT/SIGTERM)When the service process receives aSIGINTorSIGTERMsignal (ortomodachi.exit()is called) the service begins the process for graceful termination, which in practice means:The service'_stopping_servicemethod, if implemented, is called immediately upon the received signal.The service stops accepting new HTTP connections and closes keep-alive HTTP connections at the earliest.Already established HTTP connections for which a handler call is awaited called are allowed to finish their work before the service stops (up tooptions.http.termination_grace_period_secondsseconds, after which the open TCP connections for those HTTP connections will be forcefully closed if still not completed).Any AWS SQS / AMQP handlers (decorated with@aws_sns_sqsor@amqp) will stop receiving new messages. However handlers already processing a received message will be awaited to return their result. Unlike the HTTP handler connections there is no grace period for these queue consuming handlers.Currently running scheduled handlers will also be awaited to fully complete their execution before the service will terminates. No new scheduled handlers will be started.When all HTTP connections are closed, all scheduled handlers has completed and all pub-sub handlers have been awaited, the service'_stop_servicemethod is finally called (if implemented), where for example database connections can be closed. When the_stop_servicemethod returns (or immediately after completion of handler invocations if any_stop_serviceisn't implemented), the service will finally terminate.It's recommended to use ahttp.termination_grace_period_secondsoptions value of around 30 seconds to allow for the graceful termination of HTTP connections. This value can be adjusted based on the expected time it takes for the service to complete the processing of incoming request.Make sure that the orchestration engine (such as Kubernetes) waits at least 30 seconds from sending theSIGTERMto remove the pod. For extra compatibility when operating services in k8s and to get around most kind of edge-cases of intermittent timeouts and problems with ingress connections, (and unless your setup includes long running queue consuming handler calls which requires an even longer grace period), set the pod specterminationGracePeriodSecondsto90seconds and use apreStoplifecycle hook of 20 seconds.Keep thehttp.termination_grace_period_secondsoptions value lower than the pod spec'sterminationGracePeriodSecondsvalue, as the latter is a hard limit for how long the pod will be allowed to run after receiving aSIGTERMsignal.In a setup where long running queue consuming handler calls commonly occurs, any grace period the orchestration engine uses will have to take that into account. It's generally advised to split work up into sizeable chunks that can quickly complete or if handlers are idempotent, apply the possibility to cancel long running handlers as part of the_stopping_serviceimplementation.Example of a microservice containerized in Docker 🐳A great way to distribute and operate microservices are usually to run
them in containers or even more interestingly, in clusters of compute
nodes. Here follows an example of getting atomodachibased service up
and running in Docker.We're building the service' container image using just two small
files, theDockerfileand the actual code for the microservice,service.py. In reality a service would probably not be quite this
small, but as a template to get started.DockerfileFROMpython:3.10-bullseyeRUNpipinstalltomodachiRUNmkdir/appWORKDIR/appCOPYservice.py.ENVPYTHONUNBUFFERED=1CMD["tomodachi","run","service.py"]service.pyimportjsonimporttomodachiclassService(tomodachi.Service):name="example"options=tomodachi.Options(http=tomodachi.Options.HTTP(port=80,content_type="application/json; charset=utf-8",),)_healthy=True@tomodachi.http("GET",r"/")asyncdefindex_endpoint(self,request):# tomodachi.get_execution_context() can be used for# debugging purposes or to add additional service context# in logs or alerts.execution_context=tomodachi.get_execution_context()returnjson.dumps({"data":"hello world!","execution_context":execution_context,})@tomodachi.http("GET",r"/health/?",ignore_logging=True)asyncdefhealth_check(self,request):ifself._healthy:return200,json.dumps({"status":"healthy"})else:return503,json.dumps({"status":"not healthy"})@tomodachi.http_error(status_code=400)asyncdeferror_400(self,request):returnjson.dumps({"error":"bad-request"})@tomodachi.http_error(status_code=404)asyncdeferror_404(self,request):returnjson.dumps({"error":"not-found"})@tomodachi.http_error(status_code=405)asyncdeferror_405(self,request):returnjson.dumps({"error":"method-not-allowed"})Building and running the container, forwarding host's port 31337 to port 80local~/code/service$dockerbuild.-ttomodachi-microservice# > Sending build context to Docker daemon  9.216kB# > Step 1/7 : FROM python:3.10-bullseye# > 3.10-bullseye: Pulling from library/python# > ...# >  ---> 3f7f3ab065d4# > Step 7/7 : CMD ["tomodachi", "run", "service.py"]# >  ---> Running in b8dfa9deb243# > Removing intermediate container b8dfa9deb243# >  ---> 8f09a3614da3# > Successfully built 8f09a3614da3# > Successfully tagged tomodachi-microservice:latestlocal~/code/service$dockerrun-ti-p31337:80tomodachi-microserviceMaking requests to the running containerlocal~$curlhttp://127.0.0.1:31337/|jq# {#   "data": "hello world!",#   "execution_context": {#     "tomodachi_version": "x.x.xx",#     "python_version": "3.x.x",#     "system_platform": "Linux",#     "process_id": 1,#     "init_timestamp": "2022-10-16T13:38:01.201509Z",#     "event_loop": "asyncio",#     "http_enabled": true,#     "http_current_tasks": 1,#     "http_total_tasks": 1,#     "aiohttp_version": "x.x.xx"#   }# }local~$curlhttp://127.0.0.1:31337/health-i# > HTTP/1.1 200 OK# > Content-Type: application/json; charset=utf-8# > Server: tomodachi# > Content-Length: 21# > Date: Sun, 16 Oct 2022 13:40:44 GMT# ># > {"status": "healthy"}local~$curlhttp://127.0.0.1:31337/no-route-i# > HTTP/1.1 404 Not Found# > Content-Type: application/json; charset=utf-8# > Server: tomodachi# > Content-Length: 22# > Date: Sun, 16 Oct 2022 13:41:18 GMT# ># > {"error": "not-found"}It's actually as easy as that to get something spinning. The hard
part is usually to figure out (or decide) what to build next.Other popular ways of running microservices are of course to use them as
serverless functions, with an ability of scaling to zero (Lambda, Cloud
Functions, Knative, etc. may come to mind). Currentlytomodachiworks
best in a container setup and until proper serverless supporting
execution context is available in the library, it should be adviced to
hold off and use other tech for those kinds of deployments.Available built-ins used as endpoints 🚀As shown, there's different ways to trigger your microservice function
in which the most common ones are either directly via HTTP or via event
based messaging (for example AMQP or AWS SNS+SQS). Here's a list of the
currently available built-ins you may use to decorate your service
functions.HTTP endpoints@tomodachi.http@tomodachi.http(method,url,ignore_logging=[200])defhandler(self,request,*args,**kwargs):...Sets up anHTTP endpointfor the specifiedmethod(GET,PUT,POST,DELETE) on the regexpurl. Optionally specifyignore_loggingas a dict or tuple containing the status codes you
do not wish to log the access of.Can also be set toTrueto
ignore everything except status code 500.@tomodachi.http_static@tomodachi.http_static(path,url)defhandler(self,request,*args,**kwargs):# nooppassSets up anHTTP endpoint for static contentavailable asGETHEADfrom thepathon disk on the base regexpurl.@tomodachi.websocket@tomodachi.websocket(url)defhandler(self,request,*args,**kwargs):asyncdef_receive(data:Union[str,bytes])->None:...asyncdef_close()->None:...return_receive,_closeSets up awebsocket endpointon the regexpurl. The invoked
function is called upon websocket connection and should return a two
value tuple containing callables for a function receiving frames
(first callable) and a function called on websocket close (second
callable).The passed arguments to the function beside the class
object is first thewebsocketresponse connection which can be
used to send frames to the client, and optionally also therequestobject.@tomodachi.http_error@tomodachi.http_error(status_code)defhandler(self,request,*args,**kwargs):...A function which will be called if theHTTP request would result
in a 4XXstatus_code. You may use this for example to set up a
custom handler on "404 Not Found" or "403 Forbidden" responses.AWS SNS+SQS messaging@tomodachi.aws_sns_sqs@tomodachi.aws_sns_sqs(topic=None,competing=True,queue_name=None,filter_policy=FILTER_POLICY_DEFAULT,visibility_timeout=VISIBILITY_TIMEOUT_DEFAULT,dead_letter_queue_name=DEAD_LETTER_QUEUE_DEFAULT,max_receive_count=MAX_RECEIVE_COUNT_DEFAULT,fifo=False,max_number_of_consumed_messages=MAX_NUMBER_OF_CONSUMED_MESSAGES**kwargs,)defhandler(self,data,*args,**kwargs):...Topic and QueueThis would set up anAWS SQS queue, subscribing to messages on
theAWS SNS topictopic(if atopicis specified),
whereafter it will start consuming messages from the queue. The value
can be omitted in order to make the service consume messages from an existing
queue, without setting up an SNS topic subscription.Thecompetingvalue is used when the same queue name should be
used for several services of the same type and thus "compete" for
who should consume the message. Sincetomodachiversion 0.19.x
this value has a changed default value and will now default toTrueas this is the most likely use-case for pub/sub in
distributed architectures.Unlessqueue_nameis specified an auto generated queue name will
be used. Additional prefixes to bothtopicandqueue_namecan be
assigned by setting theoptions.aws_sns_sqs.topic_prefixandoptions.aws_sns_sqs.queue_name_prefixdict values.FIFO queues + max number of consumed messagesAWS supports two types of queues and topics, namelystandardandFIFO. The major difference between these is that the latter
guarantees correct ordering and at-most-once delivery. By default,
tomodachi createsstandardqueues and topics. To create them asFIFOinstead, setfifotoTrue.Themax_number_of_consumed_messagessetting determines how many
messages should be pulled from the queue at once. This is useful if
you have a resource-intensive task that you don't want other
messages to compete for. The default value is 10 forstandardqueues and 1 forFIFOqueues. The minimum value is 1, and the
maximum value is 10.Filter policyThefilter_policyvalue of specified as a keyword argument will be
applied on the SNS subscription (for the specified topic and queue)
as the"FilterPolicyattribute. This will apply a filter on SNS
messages using the chosen "message attributes" and/or their values
specified in the filter. Make note that the filter policy dict
structure differs somewhat from the actual message attributes, as
values to the keys in the filter policy must be a dict (object) or
list (array).Example: A filter policy value of{"event": ["order_paid"], "currency": ["EUR", "USD"]}would set up
the SNS subscription to receive messages on the topic only where the
message attribute"event"is"order_paid"and the"currency"value is either"EUR"or"USD".Iffilter_policyis not specified as an argument (default), the
queue will receive messages on the topic as per already specified if
using an existing subscription, or receive all messages on the topic
if a new subscription is set up (default). Changing thefilter_policyon an existing subscription may take several minutes
to propagate.Read more about the filter policy format on AWS:https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.htmlRelated to the above mentioned filter policy, thetomodachi.aws_sns_sqs_publish(which is used for publishing
messages to SNS) andtomodachi.sqs_send_message(which sends
messages directly to SQS) functions, can specify "message
attributes" using themessage_attributeskeyword argument. Values
should be specified as a simpledictwith keys and values.Example:{"event": "order_paid", "paid_amount": 100, "currency": "EUR"}.Visibility timeoutThevisibility_timeoutvalue will set the queue attributeVisibilityTimeoutif specified. To use already defined values for
a queue (default), do not supply any value to thevisibility_timeoutkeyword --tomodachiwill then not modify the
visibility timeout.DLQ: Dead-letter queueSimilarly the values fordead_letter_queue_namein tandem with themax_receive_countvalue will modify the queue attributeRedrivePolicyin regards to the potential use of a dead-letter
queue to which messages will be delivered if they have been picked
up by consumersmax_receive_countnumber of times but haven't
been deleted from the queue.The value fordead_letter_queue_nameshould either be a ARN for an SQS queue, which in that case requires
the queue to have been created in advance, or a alphanumeric queue
name, which in that case will be set up similar to the queue name
you specify in regards to prefixes, etc.Bothdead_letter_queue_nameandmax_receive_countneeds to be
specified together, as they both affect the redrive policy. To
disable the use of DLQ, use aNonevalue for thedead_letter_queue_namekeyword and theRedrivePolicywill be
removed from the queue attribute.To use the already defined values
for a queue, do not supply any values to the keyword arguments in
the decorator.tomodachiwill then not modify the queue attribute
and leave it as is.Message envelopeDepending on the servicemessage_envelope(previously namedmessage_protocol) attribute if used, parts of the enveloped data
would be distributed to different keyword arguments of the decorated
function. It's usually safe to just usedataas an argument. You
can also specify a specificmessage_envelopevalue as a keyword
argument to the decorator for specifying a specific enveloping
method to use instead of the global one set for the service.If you're utilizingfrom tomodachi.envelope import ProtobufBaseand usingProtobufBaseas the specified servicemessage_envelopeyou may also pass a keyword argumentproto_classinto the
decorator, describing the protobuf (Protocol Buffers) generated
Python class to use for decoding incoming messages. Custom
enveloping classes can be built to fit your existing architecture or
for even more control of tracing and shared metadata between
services.Encryption at rest via AWS KMSEncryption at rest for AWS SNS and/or AWS SQS can optionally be
configured by specifying the KMS key alias or KMS key id as
tomodachi service optionsoptions.aws_sns_sqs.sns_kms_master_key_id(to configure encryption
at rest on the SNS topics for which the tomodachi service handles
the SNS -> SQS subscriptions) andoptions.aws_sns_sqs.sqs_kms_master_key_id(to configure encryption
at rest for the SQS queues which the service is consuming).Note that an option value set to an empty string ("") orFalsewill
unset the KMS master key id and thus disable encryption at rest. If
instead an option is completely unset or set toNonevalue no
changes will be done to the KMS related attributes on an existing
topic or queue.It's generally not advised to change the KMS master
key id/alias values for resources currently in use.If it's expected that the services themselves, via their IAM credentials or
assumed role, are responsible for creating queues and topics, these
options could be desirable to use.Do not use these options if you
instead are using IaC tooling to handle the topics, queues and
subscriptions or that they for example are created / updated as a
part of deployments.See further details about AWS KMS for AWS SNS+SQS at:https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-server-side-encryption.htmlhttps://docs.aws.amazon.com/sns/latest/dg/sns-server-side-encryption.html#sse-key-terms.AMQP messaging (RabbitMQ)@tomodachi.amqp@tomodachi.amqp(routing_key,exchange_name="amq.topic",competing=True,queue_name=None,**kwargs,)defhandler(self,data,*args,**kwargs):...Routing key, Exchange and QueueSets up the method to be called whenever aAMQP / RabbitMQ message
is receivedfor the specifiedrouting_key. By default the'amq.topic'topic exchange would be used, it may also be
overridden by setting theoptions.amqp.exchange_namedict value on
the service class.Thecompetingvalue is used when the same queue name should be
used for several services of the same type and thus "compete" for
who should consume the message. Sincetomodachiversion 0.19.x
this value has a changed default value and will now default toTrueas this is the most likely use-case for pub/sub in
distributed architectures.Unlessqueue_nameis specified an auto generated queue name will
be used. Additional prefixes to bothrouting_keyandqueue_namecan be assigned by setting theoptions.amqp.routing_key_prefixandoptions.amqp.queue_name_prefixdict values.Message envelopeDepending on the servicemessage_envelope(previously namedmessage_protocol) attribute if used, parts of the enveloped data
would be distributed to different keyword arguments of the decorated
function. It's usually safe to just usedataas an argument. You
can also specify a specificmessage_envelopevalue as a keyword
argument to the decorator for specifying a specific enveloping
method to use instead of the global one set for the service.If you're utilizingfrom tomodachi.envelope import ProtobufBaseand usingProtobufBaseas the specified servicemessage_envelopeyou may also pass a keyword argumentproto_classinto the
decorator, describing the protobuf (Protocol Buffers) generated
Python class to use for decoding incoming messages. Custom
enveloping classes can be built to fit your existing architecture or
for even more control of tracing and shared metadata between
services.Scheduled functions / cron / triggered on time interval@tomodachi.schedule@tomodachi.schedule(interval=None,timestamp=None,timezone=None,immediately=False,)defhandler(self,*args,**kwargs):...Ascheduled functioninvoked on either a specifiedinterval(you may use the popular cron notation as a str for fine-grained
interval or specify an integer value of seconds) or a specifictimestamp. Thetimezonewill default to your local time unless
explicitly stated.When using an integerintervalyou may also specify wether the
function should be calledimmediatelyon service start or wait the
fullintervalseconds before its first invokation.@tomodachi.heartbeat@tomodachi.heartbeatdefhandler(self,*args,**kwargs):...A function which will beinvoked every second.@tomodachi.minutely/@tomodachi.hourly@tomodachi.minutely@tomodachi.hourly@tomodachi.daily@tomodachi.monthlydefhandler(self,*args,**kwargs):...A scheduled function which will be invoked onceevery minute /
hour / day / month.Scheduled tasks in distributed contextsWhat is your use-case for scheduling function triggers or functions that trigger on
an interval. These types of scheduling may not be optimal in clusters
with many pods in the same replication set, as all the services running
the same code will very likely execute at the same timestamp / interval
(which in same cases may correlated with exactly when they were last
deployed). As such these functions are quite naive and should only be
used with some care, so that it triggering the functions several times
doesn't incur unnecessary costs or come as a bad surprise if the
functions aren't completely idempotent.To perform a task on a specific
timestamp or on an interval where only one of the available services of
the same type in a cluster should trigger is a common thing to solve and
there are several solutions to pick from., some kind of distributed
consensus needs to be reached. Tooling exists, but what you need may
differ depending on your use-case. There's algorithms for distributed
consensus and leader election, Paxos or Raft, that luckily have already
been implemented to solutions like the strongly consistent and
distributed key-value storesetcdandTiKV.Even primitive solutions
such asRedisSETNXcommands would work, but could be costly or hard
to manage access levels around. If you're on k8s there's even a simple
"leader election" API available that just creates a 15 seconds lease.
Solutions are many and if you are in need, go hunting and find one that
suits your use-case, there's probably tooling and libraries available
to call it from your service functions.Implementing proper consensus mechanisms and in turn leader election can
be complicated. In distributed environments the architecture around
these solutions needs to account for leases, decision making when
consensus was not reached, how to handle crashed executors, quick
recovery on master node(s) disruptions, etc.To extend the functionality by building your own trigger decorators for
your endpoints, studying the built-in invoker classes should the first
step of action. All invoker classes should extend the class for a common
developer experience:tomodachi.invoker.Invoker.Function signatures - keywords with transport centric values 🪄Function handlers, middlewares and envelopes can specify additional
keyword arguments in their signatures and receive transport centric
values.The following keywords can be used across all kind of handler functions,
envelopes and envelopes parsing messages. These can be used to structure
apps, logging, tracing, authentication, building more advanced messaging
logic, etc.AWS SNS+SQS related values - function signature keyword argumentsUse the following keywords arguments in function signatures (for handlers, middlewares and envelopes used for AWS SNS+SQS messages).message_attributesValues specified as message attributes that accompanies the message body and that are among other things used for SNS queue subscription filter policies and for distributed tracing.queue_urlCan be used to modify visibility of messages, provide exponential backoffs, move to DLQs, etc.receipt_handleCan be used to modify visibility of messages, provide exponential backoffs, move to DLQs, etc.approximate_receive_countA value that specifies approximately how many times this message has been received from consumers onSQS.ReceiveMessagecalls. Handlers that received a message, but that doesn't delete it from the queue (for example in order to make it visible for other consumers or in case of errors), will add to this count for each time they received it.topicSimply the name of the SNS topic. For messages sent directly to the queue (for example viaSQS.SendMessageAPI calls), instead of via SNS topic subscriptions (SNS.Publish), the value oftopicwill be an empty string.sns_message_idThe message identifier for the SNS message (which is usually embedded in the body of a SQS message). Ths SNS message identifier is the same that is returned in the response when publishing a message withSNS.Publish. Thesns_message_idis read from within the"Body"of SQS messages, if the message body contains a message that comes from an SNS topic subscription. If the SQS message doesn't originate from SNS (if the message isn't type"Notification", and holds a"TopicArn"value), thensns_message_idwill result in an empty string.sqs_message_idThe SQS message identifier, which naturally will differ from the SNS message identifier as one SNS message can be propagated to several SQS queues. Thesqs_message_idis read from the"MessageId"value in the top of the SQS message.message_typeReturns the"Type"value from the message body. For messages consumed from a queue that was sent there from an SNS topic, themessage_typewill be"Notification".raw_message_bodyReturns the full contents (as a string) from"Body", which can be used to implement custom listeners, tailored for more advanced workflows, where more flexibility is needed.message_timestampA timestamp of when the original SNS message was published.message_deduplication_idThe deduplication id for messages in FIFO queues (orNoneon messages in non-FIFO queues).message_group_idThe group id for messages in FIFO queues (orNoneon messages in non-FIFO queues).HTTP related values - function signature keyword argumentsUse the following keywords arguments in function signatures (for handlers and middlewares used for HTTP requests).requestTheaiohttprequest object which holds functionality for all things HTTP requests.status_codeSpecified when predefined error handlers are run. Using the keyword in handlers and middlewares for requests not invoking error handlers should preferably be specified with a default value to ensure it will work on both error handlers and request router handlers.websocketWill be added to websocket requests if used.Middlewares for HTTP and messaging (AWS SNS+SQS, AMQP, etc.) 🧱Middlewares can be used to add functionality to the service, for example
to add logging, authentication, tracing, build more advanced logic for
messaging, unpack request queries, modify HTTP responses, handle
uncaught errors, add additional context to handlers, etc.Custom middleware functions or objects that can be called are added to
the service by specifying them as a list in thehttp_middlewareandmessage_middlewareattribute of the service class.from.middlewareimportlogger_middlewareclassService(tomodachi.Service):name="middleware-example"http_middleware=[logger_middleware]...Middlewares are invoked as a stack in the order they are specified inhttp_middlewareormessage_middlewarewith the first callable in the
list to be called first (and then also return last).Provided arguments to middleware functionsThe first unbound argument of a middleware function will receive the
coroutine function to call next (which would be either the handlers
function or a function for the next middleware in the chain).
(recommended name:func)(optional) The second unbound argument of a middleware function will
receive the service class object. (recommended name:service)(optional) The third unbound argument of a middleware function will
receive therequestobject for HTTP middlewares, or themessage(as parsed by the envelope) for message middlewares. (recommended
name:requestormessage)Use the recommended names to prevent collisions with passed keywords for
transport centric values that are also sent to the middleware if the
keyword arguments are defined in the function signature.Calling the handler or the next middleware in the chainWhen calling the next function in the chain, the middleware function
should be called as an awaitable function (await func()) and for HTTP
middlewares the result should most commonly be returned.Adding custom arguments passed on to the handlerThe function can be called with any number of custom keyword arguments,
which will then be passed to each following middleware and the handler
itself. This pattern works a bit how contextvars can be set up, but
could be useful for passing values and objects instead of keeping them
in a global context.asyncdeflogger_middleware(func:Callable[...,Awaitable],*,traceid:str="")->Any:ifnottraceid:traceid=uuid.uuid4().hexlogger=Logger(traceid=traceid)# Passes the logger and traceid to following middlewares and to the handlerreturnawaitfunc(logger=logger,traceid=traceid)A middleware can only add new keywords or modify the values or existing
keyword arguments (by passing it through again with the new value). The
exception to this is that passed keywords for transport centric values
will be ignored - their value cannot be modified - they will retain
their original value.While a middleware can modify the values of custom keyword arguments,
there is no way for a middleware to completely remove any keyword that
has been added by previous middlewares.Example of a middleware specified as a function that adds tracing to
AWS SQS handlers:This example portrays a middleware function which adds trace spans
around the function, with the trace context populated from a
"traceparent header" value collected from a SNS message' message
attribute. The topic name and SNS message identifier is also added as
attributes to the trace span.asyncdeftrace_middleware(func:Callable[...,Awaitable],*,queue_url:str,topic:str,message_attributes:dict,sns_message_id:str,sqs_message_id:str,)->None:ctx=TraceContextTextMapPropagator().extract(carrier=message_attributes)withtracer.start_as_current_span(f"SNSSQS handler '{func.__name__}'",context=ctx)asspan:span.set_attribute("messaging.system","aws_sqs")span.set_attribute("messaging.operation","process")span.set_attribute("messaging.destination.name",queue_url.rsplit("/")[-1])span.set_attribute("messaging.destination_publish.name",topicorqueue_url.rsplit("/")[-1])span.set_attribute("messaging.message.id",sns_message_idorsqs_message_id)try:# Calls the handler function (or next middleware in the chain)awaitfunc()exceptBaseExceptionasexc:logging.getLogger("exception").exception(exc)span.record_exception(exc,escaped=True)span.set_status(StatusCode.ERROR,f"{exc.__class__.__name__}:{exc}")raiseexcfrom.middlewareimporttrace_middlewarefrom.envelopeimportEvent,MessageEnvelopeclassService(tomodachi.Service):name="middleware-example"message_envelope:MessageEnvelope(key="event")message_middleware=[trace_middleware]@tomodachi.aws_sns_sqs("example-topic",queue_name="example-queue")asyncdefhandler(self,event:Event)->None:...Example of a middleware specified as a class:A middleware can also be specified as the object of a class, in which
case the__call__method of the object will be invoked as the
middleware function. Note that bound functions such as self has to be
included in the signature as it's called as a normal class function.This class provides a simplistic basic auth implementation validating
credentials in the HTTP Authorization header for HTTP requests to the
service.classBasicAuthMiddleware:def__init__(self,username:str,password:str)->None:self.valid_credentials=base64.b64encode(f"{username}:{password}".encode()).decode()asyncdef__call__(self,func:Callable[...,Awaitable[web.Response]],*,request:web.Request,)->web.Response:try:auth=request.headers.get("Authorization","")encoded_credentials=auth.split()[-1]ifauth.startswith("Basic ")else""ifencoded_credentials==self.valid_credentials:username=base64.b64decode(encoded_credentials).decode().split(":")[0]# Calls the handler function (or next middleware in the chain).# The handler (and following middlewares) can use username in their signature.returnawaitfunc(username=username)elifauth:returnweb.json_response({"status":"bad credentials"},status=401)returnweb.json_response({"status":"auth required"},status=401)exceptBaseExceptionasexc:try:logging.getLogger("exception").exception(exc)raiseexcfinally:returnweb.json_response({"status":"internal server error"},status=500)from.middlewareimporttrace_middlewareclassService(tomodachi.Service):name="middleware-example"http_middleware=[BasicAuthMiddleware(username="example",password="example")]@tomodachi.http("GET",r"/")asyncdefhandler(self,request:web.Request,username:str)->web.Response:...Logging and log formatting using thetomodachi.loggingmodule 📚A context aware logger is available from thetomodachi.loggingmodule
that can be fetched withtomodachi.logging.get_logger()or justtomodachi.get_logger()for short.The logger is a initiated using the popularstructlogpackage
(structlog
documentation),
and can be used in the same way as the standard library logger, with a
few additional features, such as holding a context and logging of
additional values.The logger returned fromtomodachi.get_logger()will hold the context
of the current handler task or request for rich contextual log records.To get a logger with another name than the logger set for the current
context, usetomodachi.get_logger(name="my-logger").fromtypingimportAnyimporttomodachiclassService(tomodachi.Service):name="service"@tomodachi.aws_sns_sqs("test-topic",queue_name="test-queue")asyncdefsqs_handler(self,data:Any,topic:str,sns_message_id:str)->None:tomodachi.get_logger().info("received msg",topic=topic,sns_message_id=sns_message_id)The log record will be enriched with the context of the current handler
task or request and the output should look something like this if thejsonformatter is used (note that the example output below has been
prettified -- the JSON that is actually used outputs the entire log
entry on one single line):{"timestamp":"2023-08-13T17:44:09.176295Z","logger":"tomodachi.awssnssqs.handler","level":"info","message":"received msg","handler":"sqs_handler","type":"tomodachi.awssnssqs","topic":"test-topic","sns_message_id":"a1eba63e-8772-4b36-b7e0-b2f524f34bff"}Interactions with Python's built-inloggingmoduleNote that the log entries are propagated to the standard library logger
(as long as it wasn't filtered), in order to allow third party handler
hooks to pick up records or act on them. This will make sure that
integrations such a Sentry's exception tracing will work out of the
box.Similarly thetomodachilogger will also by default receive records
from the standard library logger as adds alogging.roothandler, so
that thetomodachilogger can be used as a drop-in replacement for the
standard library logger. Because of this third party modules using
Python's defaultloggingmodule will use the same formatter astomodachi. Note that iflogging.basicConfig()is called before thetomodachilogger is initialized,tomodachimay not be able to add
itslogging.roothandler.Note that when using the standard library logger directly the contextual
logger won't be selected by default.importloggingfromaiohttp.webimportRequest,ResponseimporttomodachiclassService(tomodachi.Service):name="service"@tomodachi.http("GET",r"/example")asyncdefhttp_handler(self,request:Request)->Response:# contextual loggertomodachi.get_logger().info("http request")# these two rows result in similar log recordslogging.getLogger("service.logger").info("with logging module")tomodachi.get_logger("service.logger").info("with tomodachi.logging module")# extra fields from built in logger ends up as "extra" in log recordslogging.getLogger("service.logger").info("adding extra",extra={"http_request_path":request.path})returnResponse(body="hello world")A GET request to/exampleof this service would result in five log
records being emitted (as shown formatted with thejsonformatter).
The four from the example above and the last one from thetomodachi.transport.httpmodule.{"timestamp":"2023-08-13T19:25:15.923627Z","logger":"tomodachi.http.handler","level":"info","message":"http request","handler":"http_handler","type":"tomodachi.http"}{"timestamp":"2023-08-13T19:25:15.923894Z","logger":"service.logger","level":"info","message":"with logging module"}{"timestamp":"2023-08-13T19:25:15.924043Z","logger":"service.logger","level":"info","message":"with tomodachi.logging module"}{"timestamp":"2023-08-13T19:25:15.924172Z","logger":"service.logger","level":"info","message":"adding extra","extra":{"http_request_path":"/example"}}{"timestamp":"2023-08-13T19:25:15.924507Z","logger":"tomodachi.http.response","level":"info","message":"","status_code":200,"remote_ip":"127.0.0.1","request_method":"GET","request_path":"/example","http_version":"HTTP/1.1","response_content_length":11,"user_agent":"curl/7.88.1","handler_elapsed_time":"0.00135s","request_time":"0.00143s"}Configuring the loggerStart the service using the--logger jsonarguments (or settingTOMODACHI_LOGGER=jsonenvironment value) to change the log formatter
to use thejsonlog formatter. The default log formatterconsoleis
mostly suited for local development environments as it provides a
structured and colorized view of log records.It's also possible to use your own logger implementation by specifying--custom-logger ...(or settingTOMODACHI_CUSTOM_LOGGER=...environment value).Read more about how to start the service with another formatter or
implementation in theusage sectionUsing OpenTelemetry instrumentationInstalltomodachiusing theopentelemetryextras to enable
instrumentation for OpenTelemetry. In addition, install with theopentelemetry-exporter-prometheusextras to use Prometheus exporter
metrics.local~$pipinstalltomodachi[opentelemetry]local~$pipinstalltomodachi[opentelemetry,opentelemetry-exporter-prometheus]When added as a Poetry dependency theopentelemetryextras can be
enabled by addingtomodachi = {extras = ["opentelemetry"]}to thepyproject.tomlfile, and when added to arequiements.txtfile theopentelemetryextras can be enabled by addingtomodachi[opentelemetry]to the file.Auto instrumentation:tomodachi --opentelemetry-instrumentPassing the--opentelemetry-instrumentargument totomodachi runwill automatically instrument the service with the appropriate exporters
and configuration according to the setOTEL_*environment variables.Iftomodachiis installed in the environment, usingtomodachi --opentelemetry-instrument service.pyis mostly equivalent
to runningopentelemetry-instrument tomodachi run service.pyand will
load distros, configurators and instrumentors automatically in the same
way as theopentelemetry-instrumentCLI would do.local~$OTEL_LOGS_EXPORTER=console\OTEL_TRACES_EXPORTER=console\OTEL_METRICS_EXPORTER=console\OTEL_SERVICE_NAME=example-service\tomodachi--opentelemetry-instrumentrunservice/app.pyThe environment variableTOMODACHI_OPENTELEMETRY_INSTRUMENTif set
will also enable auto instrumentation in the same way.local~$OTEL_LOGS_EXPORTER=console\OTEL_TRACES_EXPORTER=console\OTEL_METRICS_EXPORTER=console\OTEL_SERVICE_NAME=example-service\TOMODACHI_OPENTELEMETRY_INSTRUMENT=1\tomodachirunservice/app.pyAuto instrumentation using theopentelemetry-instrumentCLIAuto instrumentation using theopentelemetry-instrumentCLI can be
achieved by starting services usingopentelemetry-instrument [otel-options] tomodachi run [options] <service.py ...>.# either define the OTEL_* environment variables to specify instrumentation specificationlocal~$OTEL_LOGS_EXPORTER=console\OTEL_TRACES_EXPORTER=console\OTEL_METRICS_EXPORTER=console\OTEL_SERVICE_NAME=example-service\opentelemetry-instrumenttomodachirunservice/app.py# or use the arguments passed to the opentelemetry-instrument commandlocal~$opentelemetry-instrument\--logs_exporterconsole\--traces_exporterconsole\--metrics_exporterconsole\--service_nameexample-service\tomodachirunservice/app.pyManual instrumentationAuto instrumentation using eithertomodachi --opentelemetry-instrument, setting theTOMODACHI_OPENTELEMETRY_INSTRUMENT=1env value or using theopentelemetry-instrumentCLI are the recommended ways of instrumenting
services, as they will automatically instrument the service (and libs
with instrumentors installed) with the appropriate exporters and
configuration.However, instrumentation can also be enabled by importing theTomodachiInstrumentorinstrumentation class and calling its'instrumentfunction.importtomodachifromtomodachi.opentelemetryimportTomodachiInstrumentorTomodachiInstrumentor().instrument()classService(tomodachi.Service):name="example-service"@tomodachi.http(GET,r"/example")asyncdefexample(self,request):return200,"hello world"Starting such a service with the appropriateOTEL_*environment
variables would properly instrument traces, logs and metrics for the
service without the need to use theopentelemetry-instrumentCLI.local~$OTEL_LOGS_EXPORTER=console\OTEL_TRACES_EXPORTER=console\OTEL_METRICS_EXPORTER=console\OTEL_SERVICE_NAME=example-service\tomodachirunservice/app.pyService name dynamically set if missingOTEL_SERVICE_NAMEvalueIf theOTEL_SERVICE_NAMEenvironment variable value (or--service_nameargument toopentelemetry-instrument) is not set, the
resource'service.namewill instead be set to thenameattribute of
the service class. In case the service class uses the default generic
names (serviceorapp), the resource'service.namewill instead
be set to the default as specified inhttps://github.com/open-telemetry/semantic-conventions/tree/main/docs/resource#service.In the rare case where there's multipletomodachiservices started
within the same Python process, it should be noted that OTEL traces,
metrics and logging will primarily use theOTEL_SERVICE_NAME, and if
it's missing then use the name from thefirstinstrumented service
class. The same goes for theservice.instance.idresource attribute,
which will be set to the first instrumented service class'uuidvalue
(which in most cases is automatically assigned on service start).
Multi-service execution won't accurately distinguish the service name
of tracers, meters and loggers. The recommended solution if this is an
issue, is to split the services into separate processes instead.Exclude lists to exclude certain URLs from traces and metricsTo exclude certain URLs from traces and metrics, set the environment
variableOTEL_PYTHON_TOMODACHI_EXCLUDED_URLS(orOTEL_PYTHON_EXCLUDED_URLSto cover all instrumentations) to a string
of comma delimited regexes that match the URLs.Regexes from theOTEL_PYTHON_AIOHTTP_EXCLUDED_URLSenvironment
variable will also be excluded.For example,exportOTEL_PYTHON_TOMODACHI_EXCLUDED_URLS="client/.*/info,healthcheck"will exclude requests such ashttps://site/client/123/infoandhttps://site/xyz/healthcheck.You can also pass comma delimited regexes directly to theinstrumentmethod:TomodachiInstrumentor().instrument(excluded_urls="client/.*/info,healthcheck")Prometheus meter provider (experimental)Thetomodachi.opentelemetrymodule also provides a Prometheus meter
provider that can be used to export metrics to Prometheus. Runopentelemetry-instrumentwith the--meter_provider tomodachi_prometheusargument (or setOTEL_PYTHON_METER_PROVIDER=tomodachi_prometheusenvironment value) to
enable the Prometheus meter provider.Environment variables to configure Prometheus meter providerOTEL_PYTHON_TOMODACHI_PROMETHEUS_ADDRESSspecifies the host address the Prometheus export server should listen on. (default:"localhost")OTEL_PYTHON_TOMODACHI_PROMETHEUS_PORTspecifies the port the Prometheus export server should listen on. (default:9464)OTEL_PYTHON_TOMODACHI_PROMETHEUS_INCLUDE_SCOPE_INFOspecifies whether to include scope information asotel_scope_infovalue. (default:true)OTEL_PYTHON_TOMODACHI_PROMETHEUS_INCLUDE_TARGET_INFOspecifies whether to include resource attributes astarget_infovalue. (default:true)OTEL_PYTHON_TOMODACHI_PROMETHEUS_EXEMPLARS_ENABLEDspecifies whether exemplars (experimental) should be collected and used in Prometheus export. (default:false)OTEL_PYTHON_TOMODACHI_PROMETHEUS_NAMESPACE_PREFIXspecifies the namespace prefix for Prometheus metrics. A final underscore is automatically added if prefix is used. (default:"")Dependency requirement for Prometheus meter providerThetomodachi_prometheusmeter provider requires that theopentelemetry-exporter-prometheusandprometheus_clientpackages
package are installed.Usetomodachiextrasopentelemetry-exporter-prometheusto
automatically include a compatible version of the exporter.OpenMetrics output from Prometheus with exemplars enabledWith exemplars enabled, make sure to call the Prometheus client with the
accept headerapplication/openmetrics-textto ensure exemplars are
included in the response.curlhttp://localhost:9464/metrics-H"Accept: application/openmetrics-text"💡 Note that if the accept headerapplication/openmetrics-textis
missing from the request, exemplars will be excluded from the response.Example: starting a service with instrumentationThis example will start and instrument a service with OTLP exported
traces sent to the endpointotelcol:4317and metrics that can be
scraped by Prometheus from port9464. All metrics except fortarget_infoandotel_scope_infowill be prefixed with"tomodachi_". Additionallyexemplarswill be added to the Prometheus
collected metrics that includes sample exemplars with trace_id and
span_id labels.local~$TOMODACHI_OPENTELEMETRY_INSTRUMENT=1\OTEL_TRACES_EXPORTER=otlp\OTEL_EXPORTER_OTLP_ENDPOINT=otelcol:4317\OTEL_PYTHON_METER_PROVIDER=tomodachi_prometheus\OTEL_PYTHON_TOMODACHI_PROMETHEUS_EXEMPLARS_ENABLED=true\OTEL_PYTHON_TOMODACHI_PROMETHEUS_ADDRESS=0.0.0.0\OTEL_PYTHON_TOMODACHI_PROMETHEUS_PORT=9464\OTEL_PYTHON_TOMODACHI_PROMETHEUS_NAMESPACE_PREFIX=tomodachi\tomodachirunservice/app.pyAdditional configuration options 🤩In the service class an attribute namedoptions(as atomodachi.Optionsobject) can be set for additional configuration.importjsonimporttomodachiclassService(tomodachi.Service):name="http-example"options=tomodachi.Options(http=tomodachi.Options.HTTP(port=80,content_type="application/json; charset=utf-8",real_ip_from=["127.0.0.1/32","10.0.0.0/8","172.16.0.0/12","192.168.0.0/16",],keepalive_timeout=5,max_keepalive_requests=20,),watcher=tomodachi.Options.Watcher(ignored_dirs=["node_modules"],),)@tomodachi.http("GET",r"/health")asyncdefhealth_check(self,request):return200,json.dumps({"status":"healthy"})# Specify custom 404 catch-all response@tomodachi.http_error(status_code=404)asyncdeferror_404(self,request):returnjson.dumps({"error":"not-found"})Options are read or written via the service'optionsattributeA service option can be accessed via the configuration key in numerous ways.options.http.sub_key(example:options.http.port)options[f"http.{sub_key}"](example:options["http.port"])options["http"][sub_key](example:options["http"]["port"])The serviceoptionsattribute is an object oftomodachi.Optionstype.HTTP server parametersConfiguration keyDescriptionDefaulthttp.portTCP port (integer value) to listen for incoming connections.9700http.hostNetwork interface to bind TCP server to."0.0.0.0"will bind to all IPv4 interfaces.Noneor""will assume all network interfaces."0.0.0.0"http.reuse_portIf set toTrue(which is also the default value on Linux) the HTTP server will bind to the port using the socket optionSO_REUSEPORT. This will allow several processes to bind to the same port, which could be useful when running services via a process manager such assupervisordor when it's desired to run several processes of a service to utilize additional CPU cores, etc. Note that thereuse_portoption cannot be used on non-Linux platforms.Trueon Linux, otherwiseFalsehttp.keepalive_timeoutEnables connections to use keep-alive if set to an integer value over0. Number of seconds to keep idle incoming connections open.0http.max_keepalive_requestsAn optional number (int) of requests which is allowed for a keep-alive connection. After the specified number of requests has been done, the connection will be closed. An option value of0orNone(default) will allow any number of requests over an open keep-alive connection.Nonehttp.max_keepalive_timeAn optional maximum time in seconds (int) for which keep-alive connections are kept open. If a keep-alive connection has been kept open for more thanhttp.max_keepalive_timeseconds, the following request will be closed upon returning a response. The feature is not used by default and won't be used if the value is0orNone. A keep-alive connection may otherwise be open unless inactive for more than the keep-alive timeout.Nonehttp.client_max_sizeThe client’s maximum size in a request, as an integer, in bytes.(1024 ** 2) * 100http.termination_grace_period_secondsThe number of seconds to wait for functions called via HTTP to gracefully finish execution before terminating the service, for example if service received aSIGINTorSIGTERMsignal while requests were still awaiting response results.30http.real_ip_headerHeader to read the value of the client's real IP address from if service operates behind a reverse proxy. Only used ifhttp.real_ip_fromis set and the proxy's IP correlates with the value fromhttp.real_ip_from."X-Forwarded-For"http.real_ip_fromIP address(es) or IP subnet(s) / CIDR. Allows thehttp.real_ip_headerheader value to be used as client's IP address if connecting reverse proxy's IP equals a value in the list or is within a specified subnet. For example["127.0.0.1/32", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]would permit header to be used if closest reverse proxy is"127.0.0.1"or within the three common private network IP address ranges.[]http.content_typeDefault content-type header to use if not specified in the response."text/plain; charset=utf-8"http.access_logIf set to the default value (boolean)Truethe HTTP access log will be output to stdout (loggertomodachi.http). If set to astrvalue, the access log will additionally also be stored to file using value as filename.Truehttp.server_header"Server"header value in responses."tomodachi"AWS SNS+SQS credentials and prefixesConfiguration keyDescriptionDefaultaws_sns_sqs.region_nameThe AWS region to use for SNS+SQS pub/sub API requests.Noneaws_sns_sqs.aws_access_key_idThe AWS access key to use for SNS+SQS pub/sub API requests.Noneaws_sns_sqs.aws_secret_access_keyThe AWS secret to use for SNS+SQS pub/sub API requests.Noneaws_sns_sqs.topic_prefixA prefix to any SNS topics used. Could be good to differentiate between different dev environments.""aws_sns_sqs.queue_name_prefixA prefix to any SQS queue names used. Could be good to differentiate between different dev environments.""aws_sns_sqs.sns_kms_master_key_idIf set, will set the KMS key (alias or id) to use for encryption at rest on the SNS topics created by the service or subscribed to by the service. Note that an option value set to an empty string ("") orFalsewill unset the KMS master key id and thus disable encryption at rest. If instead an option is completely unset or set toNonevalue no changes will be done to the KMS related attributes on an existing topic.None(no changes to KMS settings)aws_sns_sqs.sqs_kms_master_key_idIf set, will set the KMS key (alias or id) to use for encryption at rest on the SQS queues created by the service or for which the service consumes messages on. Note that an option value set to an empty string ("") orFalsewill unset the KMS master key id and thus disable encryption at rest. If instead an option is completely unset or set toNonevalue no changes will be done to the KMS related attributes on an existing queue.None(no changes to KMS settings)aws_sns_sqs.sqs_kms_data_key_reuse_periodIf set, will set the KMS data key reuse period value on the SQS queues created by the service or for which the service consumes messages on. If the option is completely unset or set toNonevalue no change will be done to the KMSDataKeyReusePeriod attribute of an existing queue, which can be desired if it's specified during deployment, manually or as part of infra provisioning. Unless changed, SQS queues using KMS use the default value300(seconds).NoneCustom AWS endpoints (for example during development)Configuration keyDescriptionDefaultaws_endpoint_urls.snsConfigurable endpoint URL for AWS SNS – primarily used for integration testing during development using fake services / fake endpoints.Noneaws_endpoint_urls.sqsConfigurable endpoint URL for AWS SQS – primarily used for integration testing during development using fake services / fake endpoints.NoneAMQP / RabbitMQ pub/sub settingsConfiguration keyDescriptionDefaultamqp.hostHost address / hostname for RabbitMQ server."127.0.0.1"amqp.portHost post for RabbitMQ server.5672amqp.loginLogin credentials."guest"amqp.passwordLogin credentials."guest"amqp.exchange_nameThe AMQP exchange name to use in the service."amq_topic"amqp.routing_key_prefixA prefix to add to any AMQP routing keys provided in the service.""amqp.queue_name_prefixA prefix to add to any AMQP queue names provided in the service.""amqp.virtualhostAMQP virtualhost settings."/"amqp.sslTLS can be enabled for supported host connections.Falseamqp.heartbeatThe heartbeat timeout value defines after what period of time the peer TCP connection should be considered unreachable (down) by RabbitMQ and client libraries.60amqp.queue_ttlTTL set on newly created queues.86400Code auto reload on file changes (for use in development)Configuration keyDescriptionDefaultwatcher.ignored_dirsDirectories / folders that the automatic code change watcher should ignore. Could be used during development to save on CPU resources if any project folders contains a large number of file objects that doesn't need to be watched for code changes. Already ignored directories are"__pycache__",".git",".svn","__ignored__","__temporary__"and"__tmp__".[]watcher.watched_file_endingsAdditions to the list of file endings that the watcher should monitor for file changes. Already followed file endings are".py",".pyi",".json",".yml",".html"and".phtml".[]Default optionsIf no options are specified or if an emptytomodachi.Optionsobject is instantiated, the default set of options will be applied.>>>importtomodachi>>>tomodachi.Options()∴ http <class: "Options.HTTP" -- prefix: "http">:| port = 9700| host = "0.0.0.0"| reuse_port = False| content_type = "text/plain; charset=utf-8"| charset = "utf-8"| client_max_size = 104857600| termination_grace_period_seconds = 30| access_log = True| real_ip_from = []| real_ip_header = "X-Forwarded-For"| keepalive_timeout = 0| keepalive_expiry = 0| max_keepalive_time = None| max_keepalive_requests = None| server_header = "tomodachi"∴ aws_sns_sqs <class: "Options.AWSSNSSQS" -- prefix: "aws_sns_sqs">:| region_name = None| aws_access_key_id = None| aws_secret_access_key = None| topic_prefix = ""| queue_name_prefix = ""| sns_kms_master_key_id = None| sqs_kms_master_key_id = None| sqs_kms_data_key_reuse_period = None| queue_policy = None| wildcard_queue_policy = None∴ aws_endpoint_urls <class: "Options.AWSEndpointURLs" -- prefix: "aws_endpoint_urls">:| sns = None| sqs = None∴ amqp <class: "Options.AMQP" -- prefix: "amqp">:| host = "127.0.0.1"| port = 5672| login = "guest"| password = "guest"| exchange_name = "amq.topic"| routing_key_prefix = ""| queue_name_prefix = ""| virtualhost = "/"| ssl = False| heartbeat = 60| queue_ttl = 86400· qos <class: "Options.AMQP.QOS" -- prefix: "amqp.qos">:| queue_prefetch_count = 100| global_prefetch_count = 400∴ watcher <class: "Options.Watcher" -- prefix: "watcher">:| ignored_dirs = []| watched_file_endings = []Decorated functions using@tomodachi.decorator🎄Invoker functions can of course be decorated using custom functionality.
For ease of use you can then in turn decorate your decorator with the
the built-in@tomodachi.decoratorto ease development. If the
decorator would return anything else thanTrueorNone(or not
specifying any return statement) the invoked function willnotbe
called and instead the returned value will be used, for example as an
HTTP response.importtomodachi@tomodachi.decoratorasyncdefrequire_csrf(instance,request):token=request.headers.get("X-CSRF-Token")ifnottokenortoken!=request.cookies.get("csrftoken"):return{"body":"Invalid CSRF token","status":403}classService(tomodachi.Service):name="example"@tomodachi.http("POST",r"/create")@require_csrfasyncdefcreate_data(self,request):# Do magic here!return"OK"Good practices for running services in production 🤞When running atomodachiservice in a production environment, it's
important to ensure that the service is set up correctly to handle the
demands and constraints of a live system. Here's some recommendations
of options and operating practices to make running the services a
breeze.Go for a Docker 🐳 environment if possible -- preferably orchestrated with for example Kubernetes to handle automated scaling events to meet demand of incoming requests and/or event queues.Make sure that aSIGTERMsignal is passed to thepythonprocess when a pod is scheduled for termination to give it time to gracefully stop listeners, consumers and finish active handler tasks.This should work automatically for services in Docker if theCMDstatement in yourDockerfileis starting thetomodachiservice directly.In case shell scripts are used inCMDyou might need to trap signals and forward them to the service process.To give services the time to gracefully complete active handler executions and shut down, make sure that the orchestration engine waits at least 30 seconds from sending theSIGTERMto remove the pod.For extra compatibility in k8s and to get around most kind of edge-cases of intermittent timeouts and problems with ingress connections, set the pod specterminationGracePeriodSecondsto90seconds and use apreStoplifecycle hook of 20 seconds.spec:terminationGracePeriodSeconds:90containers:lifecycle:preStop:exec:command:["/bin/sh","-c","sleep20"]If your service inbound network access to HTTP handlers from users or API clients, then it's usually preferred to put some kind of ingress (nginx, haproxy or other type of load balancer) to proxy connections to the service pods.Let the ingress handle public TLS, http2 / http3, client facing keep-alives and WebSocket protocol upgrades and let the service handler just take care of the business logic.Use HTTP options such as the ones in this service to have the service rotate keep-alive connections so that ingress connections doesn't stick to the old pods after a scaling event.If keep-alive connections from ingresses to services stick for too long, the new replicas added when scaling out won't get their balanced share of the requests and the old pods will continue to receive most of the requests.importtomodachiclassService(tomodachi.Service):name="service"options=tomodachi.Options(http=tomodachi.Options.HTTP(port=80,content_type="application/json; charset=utf-8",real_ip_from=["127.0.0.1/32","10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"],keepalive_timeout=10,max_keepalive_time=30,))Use a JSON log formatter such as the one enabled via--logger json(or env variableTOMODACHI_LOGGER=json) so that the log entries can be picked up by a log collector.Always start the service with the--productionCLI argument (or set the env variableTOMODACHI_PRODUCTION=1) to disable the file watcher that restarts the service on file changes, and to hide the start banner so it doesn't end up in log buffers.Not related totomodachidirectly, but always remember to collect the log output and monitor your instances or clusters.Arguments totomodachi runwhen running in production envtomodachirunservice/app.py--loopuvloop--production--log-levelwarning--loggerjsonHere's a breakdown of the arguments and why they would be good for
these kinds of environments.--loop uvloop: This argument sets the event loop implementation touvloop, which is known to be faster than the defaultasyncioloop. This can help improve the performance of your service. However, you should ensure thatuvloopis installed in your environment before using this option.--production: This argument disables the file watcher that restarts the service on file changes and hides the startup info banner. This is important in a production environment where you don't want your service to restart every time a file changes. It also helps to reduce unnecessary output in your logs.--log-level warning: This argument sets the minimum log level towarning. In a production environment, you typically don't want to log every single detail of your service's operation. By setting the log level towarning, you ensure that only important messages are logged.If your infrastructure supports rapid collection of log entries and you see a clear benefit of including logs of log levelinfo, it would make sense to use--log-level infoinstead of filtering on at leastwarning.--logger json: This argument sets the log formatter to output logs in JSON format. This is useful in a production environment where you might have a log management system that can parse and index JSON logs for easier searching and analysis.You can also set these options using environment variables. This can be
useful if you're deploying your service in a containerized environment
like Docker or Kubernetes, where you can set environment variables in
your service's configuration. Here's how you would set the same
options using environment variables:exportTOMODACHI_LOOP=uvloopexportTOMODACHI_PRODUCTION=1exportTOMODACHI_LOG_LEVEL=warningexportTOMODACHI_LOGGER=json

tomodachirunservice/app.pyBy using environment variables, you can easily change the configuration
of your service without having to modify your code or your command line
arguments. This can be especially useful in a CI/CD pipeline where you
might want to adjust your service's configuration based on the
environment it's being deployed to.Requirements 👍Python(3.8+,3.9+,3.10+,3.11+,3.12+)aiohttp(aiohttpis the currently supported HTTP server implementation fortomodachi)aiobotocoreandbotocore(used for AWS SNS+SQS pub/sub messaging)aioamqp(used for RabbitMQ / AMQP pub/sub messaging)structlog(used for logging)uvloop(optional: alternative event loop implementation)Pull requests and bug reportsThis library is open source software. Please add a pull request with the
feature that you deem are missing from the lib or for bug fixes that you
encounter.Make sure that the tests and linters are passing. A limited number of
tests can be run locally without external services. Use GitHub Actions
to run the full test suite and to verify linting and regressions.Read
more in the contribution
guide.GitHub repositoryThe latest developer version oftomodachiis always available at
GitHub.Clone repo:git clone git@github.com:kalaspuff/tomodachi.gitGitHub:https://github.com/kalaspuff/tomodachiAcknowledgements + contributors🙇 Thank you everyone that has come with ideas, reported issues, built
and operated services, helped debug and made contributions to the
library code directly or via libraries that build on the base
functionality.🙏 Many thanks to the amazing contributors that have helped to maketomodachibetter.Changelog of releasesChanges are recorded in the repo as well as together with the GitHub
releases.In repository:https://github.com/kalaspuff/tomodachi/blob/master/CHANGELOG.mdRelease tags:https://github.com/kalaspuff/tomodachi/releasesLICENSEtomodachiis offered under theMIT license.Additional questions and informationWhat is the best way to run atomodachiservice?Docker containers are great and can be scaled out in Kubernetes,
Nomad or other orchestration engines. Some may instead run several
services on the same environment, on the same machine if their
workloads are smaller or more consistent. Remember to gather your
output and monitor your instances or clusters.See the section ongood practices for running services in
production
environmentsfor more insights.Are there any more example services?There are a few examples in theexamplesfolder, including usingtomodachiin anexample Docker
environmentwith or without docker-compose. There are examples to publish events
/ messages to an AWS SNS topic and subscribe to an AWS SQS queue.
There's also a similar code available of how to work with pub/sub
for RabbitMQ via the AMQP transport protocol.What's the recommended setup to run integration tests towards my service?When unit tests are not enough, you can run integration tests towards your services using the third party
librarytomodachi-testcontainers. This library provides a way to run your service in a Docker container.https://github.com/filipsnastins/tomodachi-testcontainershttps://pypi.org/project/tomodachi-testcontainers/Why should I usetomodachi?tomodachiis an easy way to start when experimenting with your
architecture or trying out a concept for a new service – specially if you're
working on services that publish and consume messages (pub-sub messaging), such as events or commands
from AWS SQS or AMQP message brokers.tomodachiprocesses message flows through topics and queues, with enveloping and receiving execution handling.tomodachimay not have all the features you desire out of the box and it may never do, but I believe
it's great for bootstrapping microservices in async Python.Whiletomodachiprovides HTTP handlers, the library may not be the best choice today if you are solely building
services that exposes REST or GraphQL API. In such case, you may be better off to use,
for examplefastapiorlitestar, perhaps in combination withstrawberryas your preferred interface.Note that the HTTP layer on top oftomodachiis usingaiohttp, which provides a more raw interface than libraries such asfastapiorstarlette.I have some great additionsSweet! Please open a pull request with your additions. Make sure
that the tests and linters are passing. A limited number of tests
can be run locally without external services. Use GitHub Actions to
run the full test suite and to verify linting and regressions. Get
started at the shortcontribution
guide.Beta software in production?There are some projects and organizations that already are running
services based ontomodachiin production. The library is provided
as is with an unregular release schedule, and as with most software,
there will be unfortunate bugs or crashes. Consider this currently
as beta software (with an ambition to be stable enough for
production). Would be great to hear about other use-cases in the
wild!Another good idea is to drop in Sentry or other exception debugging
solutions. These are great to catch errors if something wouldn't
work as expected in the internal routing or if your service code
raises unhandled exceptions.Who built this and why?My name isCarl Oscar Aaro[@kalaspuff] and I'm a coder
from Sweden. When I started writing the first few lines of this
library back in 2016, my intention was to experiment with
Python'sasyncio, the event loop, event sourcing and pub-sub message
queues.A lot has happened since -- now running services in both
production and development clusters, while also using microservices
for quick proof of concepts and experimentation. 🎉