Welcome to Tamarco’s documentation!¶
Tamarco¶
Microservices framework designed for asyncio and Python.
Features¶
Lifecycle management.
Standardized settings via etcd.
Automatic logging configuration. Support for sending logs to an ELK stack.
Application metrics via Prometheus.
Designed for asyncio.
Messaging patterns. The framework comes with support for AMQP and Kafka via external resources. The AMQP resource has implemented publish/subscribe, request/response and push/pull patterns.
Custom encoders and decoders.
Pluging oriented architecture. Anyone can create a new resource to add new functionality. External resources are integrated into the framework transparently for the user.
Graceful shutdown.
Resources¶
- The framework allows to write external resources and integrate them in the lifecycle of a microservice easily. List with
the available resources:
Metrics
Registry
Status
Profiler
Memory analizer
HTTP
Postgres (not released yet)
Influxdb (not released yet)
Redis (not released yet)
Websocket (not released yet)
Let us know if you have written a resource.
Examples¶
There are several examples in the examples
folder.
To run them, install tamarco, launch the docker-compose (not necessary for all the examples) and run it.
pip install tamarco
docker-compose up -d
python examples/http_resource/microservice.py
Requirements¶
Support for Python >= 3.6.
Tutorials¶
Quick Start¶
Install Tamarco
To install Tamarco, simply run this command in your terminal of choice. Allowed Python versions are Python >= 3.6. Recommended version is Python 3.7:
$ pip3 install tamarco
Start a project¶
Use Tamarco to start a new project. Use the following command and fill the data requested:
$ tamarco start_project
Write your microservice¶
Start writing the microservice code inside the project folder, in microservice.py.
Write your first microservice¶
In this section, we will create a simple microservice that inserts data to a Postgres table.
Installation¶
For this example, we need the Tamarco framework and the Postgres resource plugin. Optionally, you can create a virtual environment before installing the packages:
$ virtualenv virtualenv -p python3.6
$ . virtualenv/bin/activate
$ pip3 install tamarco tamarco-postgres
Using Tamarco code generation¶
Tamarco provides the generation of a microservice skeleton using cookiecutter. templates. To use this feature, go to the path where you want to create the microservice and type:
$ tamarco start_project
This command will ask you a few questions to get a minimum service configuration and will generate the code in a new folder named with the chosen project_name. The main script file is called microservice.py and for simplification, we will code all our example in this file.
More information about the microservice code generation: Microservice cookicutter template.
Our microservice step by step¶
The code generated in microservice.py is very simple:
from tamarco.core.microservice import Microservice
class MyMicroservice(Microservice):
name = "my_awesome_project_name"
def main():
ms = MyMicroservice()
ms.run()
In the previous code, we can see that our service inherits from the Tamarco base class Microservice. This class will be the base of all the microservices and it is responsible for starting all the resources and at the same time stop all the resources properly when the microservice exits. It has several execution stages in its lifecycle. For more information see: Microservice base class.
The next step is to declare the Postgres resource we want to use:
from tamarco.core.microservice import Microservice
from tamarco-postgres import PostgresClientResource
class MyMicroservice(Microservice):
name = "my_awesome_project_name"
postgres = PostgresClientResource()
In a production environment, we normally get the service settings/configuration from a storage service like etcd, but to simplify, now we set the required configuration using an internal function. More info about the Tamarco settings in: A walk around the settings.
from tamarco.core.microservice import Microservice
from tamarco-postgres import PostgresClientResource
class MyMicroservice(Microservice):
name = "my_awesome_project_name"
postgres = PostgresClientResource()
def __init__(self):
super().__init__()
self.settings.update_internal({
"system": {
"deploy_name": "my_first_microservice",
"logging": {
"profile": "DEVELOP",
},
"resources": {
"postgres": {
"host": "127.0.0.1",
"port": 5432,
"user": "postgres"
}
}
}
})
Our service already knows where to connect to the database, so, we have to create the table and make the queries. Tamarco provides a decorator (@task) to convert a method in an asyncio task. The task is started and stopped when the microservice starts and stops respectively:
from tamarco.core.microservice import Microservice, task
from tamarco-postgres import PostgresClientResource
class MyMicroservice(Microservice):
name = "my_awesome_project_name"
postgres = PostgresClientResource()
def __init__(self):
super().__init__()
self.settings.update_internal({
"system": {
"deploy_name": "my_first_microservice",
"logging": {
"profile": "DEVELOP",
},
"resources": {
"postgres": {
"host": "127.0.0.1",
"port": 5432,
"user": "postgres"
}
}
}
})
@task
async def postgres_query(self):
create_query = '''
CREATE TABLE my_table (
id INT PRIMARY KEY NOT NULL,
name TEXT NOT NULL
);
'''
insert_query = "INSERT INTO my_table (id, name) VALUES (1, 'John Doe');"
select_query = "SELECT * FROM my_table"
try:
await self.postgres.execute(create_query)
await self.postgres.execute(insert_query)
response = await self.postgres.fetch(select_query)
except Exception:
self.logger.exception("Error executing query")
else:
self.logger.info(f"Data: {response}")
NOTICE that we imported task from tamarco.core.microservice!!
Running our microservice¶
Firstly, we need a running Postgres, so we can launch a docker container:
$ docker run -d -p 5432:5432 postgres
In the root of our project, there is the service entry point: app.py. You can execute this file and check the result (don’t forget to activate the virtualenv if you have one):
$ python app.py
How-To Guides¶
How to install Tamarco¶
Tamarco is compatible with Python >= 3.6. Recommended version is Python 3.7.
To install Tamarco, simply run this command in your terminal of choice:
$ pip3 install tamarco
How to setup the logging¶
The profile¶
Two different profiles are allowed:
DEVELOP. The logging level is set to debug.
PRODUCTION. The logging level is set to info.
The profile setting needs to be in capital letters.
system:
logging:
profile: <DEVELOP or PRODUCTION>
Stdout¶
The logging by stdout can be enabled or disabled:
It comes with the
system:
logging:
stdout: true
File handler¶
Write all logs in files with a RotatingFileHandler. It is enabled when the system/logging/file_path exits, saving the logs in the specified location.
system:
logging:
file_path: <file_path>
Logstash¶
Logstash is the log collector used by Tamarco, it collects, processes, enriches and unifies all the logs sent by different components of an infrastructure. Logstash supports multiple choices for the log ingestion, we support three of them simply by activating the corresponding settings:
Logstash UDP handler¶
Send logs to Logstash using a raw UDP socket.
system:
logging:
logstash:
enabled: true
host: 127.0.0.1
port: 5044
fqdn: false
version: 1
Logstash Redis handler¶
Send logs to Logstash using the Redis pubsub pattern.
system:
logging:
redis:
enabled: true
host: 127.0.0.1
port: 6379
password: my_password
ssl: false
Logstash HTTP handler¶
Send logs to Logstash using HTTP requests.
system:
logging:
http:
enabled: true
url: http://127.0.0.1
user:
password:
max_time_seconds: 15
max_records: 100
- The logs are sent in bulk, the max_time_seconds is the maximum time without sending the logs, the max_records configures
the maximum number of logs in a single HTTP request (The first condition triggers the request).
How to setup a metric backend¶
The Microservice class comes by default with the metrics resource, this means that the microservice is going to read the configuration without any explicit code in your microservice.
Prometheus¶
Prometheus, unlike other metric backends, follows a pull-based (over HTTP) architecture at the metric collection. It means that the microservices just have the responsibility of exposing the metrics via an HTTP server and Prometheus collects the metrics requesting them to the microservices.
It is the supported metric backend with a more active development right now.
The metrics resource uses other resource named tamarco_http_report_server, that it is an HTTP server, to expose the application metrics. The metrics always are exposed to the /metrics endpoint. To expose the Prometheus metrics the microservices should be configured as follows:
system:
resources:
metrics:
collect_frequency: 10
handlers:
prometheus:
enabled: true
tamarco_http_report_server:
host: 127.0.0.1
port: 5747
With this configuration, a microservice is going to expose the Prometheus metrics at http://127.0.0.1:5747/metrics.
The collect frequency defines the update period in seconds of the metrics in the HTTP server.
The microservice name is automatically added as metric suffix to the name of the metrics. Example: A summary named http_response_time in a microservice named billing_api is going to be named billing_api_http_response_time in the exposed metrics.
Carbon¶
Only the plaintext protocol sent directly via a TCP socket is supported.
To configure a carbon handler:
system:
resources:
metrics:
handlers:
carbon:
enabled: true
host: 127.0.0.1
port: 2003
collect_frequency: 15
The collect frequency defines the period in seconds where the metrics are collected and sent to carbon.
File¶
It is an extension of the carbon handler, instead of sending the metrics to carbon it just appends the metrics to a file. The format is the following: <metric path> <metric value> <metric timestamp>.
To configure the file handler:
system:
resources:
metrics:
handlers:
file:
enabled: true
path: /tmp/tamarco_metrics
collect_frequency: 15
The collect frequency defines the period in seconds where the metrics are collected and written to a file.
Stdout¶
It is an extension of the carbon handler, instead of sending the metrics to carbon it just writes the metrics in the stdout. The format is the following: <metric path> <metric value> <metric timestamp>.
To configure the file handler:
system:
resources:
metrics:
handlers:
stdout:
enabled: true
collect_frequency: 15
The collect frequency defines the period in seconds where the metrics are collected and written to a file.
How to setup a setting backend¶
There are some ways to set up the settings, etcd is the recommended backend for a centralized configuration. The YML and file and dictionary are useful for development.
etcd¶
etcd is the recommended backend for a centralized configuration. All the configuration of the system can be in etcd, but before being able to read it, we should specify to the microservices how to access an etcd.
The following environment variables need to be properly configured to use etcd:
TAMARCO_ETCD_HOST: Needed to setup the etcd as setting backend.
TAMARCO_ETCD_PORT: Optional variable, by default is 2379.
ETCD_CHECK_KEY: Optional variable, if set the microservice waits until the specified etcd key exits to initialize.
Avoids race conditions between the etcd and microservices initialization. Useful in orchestrators such docker-swarm where dependencies between components cannot be easily specified.
YML file¶
For enable the feature, the following environment variable must be set:
TAMARCO_YML_FILE: Example: ‘settings.yml’. Example of a YML file with the system configuration:
system:
deploy_name: test_tamarco
logging:
profile: DEVELOP
file: false
stdout: true
redis:
enabled: false
host: "127.0.0.1"
port: 7006
password: ''
ssl: false
microservices:
test:
logging:
profile: DEVELOP
file: false
stdout: true
resources:
metrics:
collect_frequency: 15
status:
host: 127.0.0.1
port: 5747
debug: False
amqp:
host: 127.0.0.1
port: 5672
vhost: /
user: microservice
password: 1234
connection_timeout: 10
queues_prefix: "prefix"
Dictionary¶
It is possible to load the configuration from a dictionary:
import asyncio
from sanic.response import text
from tamarco.core.microservice import Microservice, MicroserviceContext, thread
from tamarco.resources.io.http.resource import HTTPClientResource, HTTPServerResource
class HTTPMicroservice(Microservice):
name = 'settings_from_dictionary'
http_server = HTTPServerResource()
def __init__(self):
super().__init__()
self.settings.update_internal({
'system': {
'deploy_name': 'settings_documentation',
'logging': {
'profile': 'PRODUCTION',
},
'resources': {
'http_server': {
'host': '127.0.0.1',
'port': 8080,
'debug': True
}
}
}
})
ms = HTTPMicroservice()
@ms.http_server.app.route('/')
async def index(request):
print('Requested /')
return text('Hello world!')
def main():
ms.run()
if __name__ == '__main__':
main()
How to setup settings for a specific microservice¶
The settings under system.microservice.<microservice_name>.<setting_paths_to_override> overrides the general settings of system.<setting_paths_to_override> in the microservice named <microservice_name>.
In the following example, the microservice dog is going to read the logging profile “DEVELOP” and the other microservices are going to stay in the logging profile “PRODUCTION”:
system:
deploy_name: tamarco_doc
logging:
profile: PRODUCTION
file: false
stdout: true
microservices:
dog:
logging:
profile: DEVELOP
The microservice name is declared when the microservice class is defined:
class MicroserviceExample(Microservice):
name = 'my_microservice_name'
How to setup settings for a resource¶
The resources are designed to automatically load their configuration using the setting resource.
The resources should be defined as an attribute of the microservice class:
class MyMicroservice(Microservice):
name = 'settings_from_dictionary'
recommendation_http_api = HTTPServerResource()
billing_http_api = HTTPServerResource()
def __init__(self):
super().__init__()
self.settings.update_internal({
'system': {
'deploy_name': 'settings_documentation',
'logging': {
'profile': 'PRODUCTION',
},
'resources': {
'recommendation_http_api': {
'host': '127.0.0.1',
'port': 8080,
'debug': True
},
'billing_http_api': {
'host': '127.0.0.1',
'port': 9090,
'debug': False
}
}
}
})
The resources load their configuration based on the name of the attribute used to bind the resource to the microservice. In the example, we have two HTTPServerResource in the same microservice and each one uses a different configuration.
The HTTPServerResource recommendations_api variable is going to find its configuration in the path ‘system.resources.recommendation_api’.
You must be cautious about choosing the name when the instances are created. If several microservices use the same database, the name of the resource instance in the microservice must be the same in all microservices to load the same configuration.
How to use the logging resource¶
Tamarco uses the standard logging library, it only interferes doing an automatic configuration based in the settings.
The microservice comes with a logger ready to use:
import asyncio
from tamarco.core.microservice import Microservice, task
class MyMicroservice(Microservice):
name = 'my_microservice_name'
extra_loggers_names.append("my_extra_logger")
@task
async def periodic_log(self):
logging.getlogger("my_extra_logger").info("Initializing periodic log")
while True:
await asyncio.sleep(1)
self.logger.info("Sleeping 1 second")
if __name__ == "__main__":
ms = MyMicroservice()
ms.run()
Also can configured more loggers adding their names to my_extra_logger list of the Microservice class.
The logger bound to the microservice is the one named as the microservice, so you can get and use the logger whatever you want:
import logging
async def http_handler():
logger = logging.getlogger('my_microservice_name')
logger.info('Handling a HTTP request')
Logging exceptions¶
A very common pattern programming microservices is log exceptions. Tamarco automatically sends the exception tracing to Logstash and print the content by stdout when the exc_info flag is active. Only works with logging lines inside an except statement:
import asyncio
from tamarco.core.microservice import Microservice, task
class MyMicroservice(Microservice):
name = 'my_microservice_name'
@task
async def periodic_exception_log(self):
while True:
try:
raise KeyError
except:
self.logger.warning("Unexpected exception.", exc_info=True)
if __name__ == "__main__":
ms = MyMicroservice()
ms.run()
Adding extra fields and tags¶
The fields extend the logging providing more extra information and the tags allow to filter the logs by this key.
A common pattern is to enrich the logs with some information about the context. For example: with a request identifier the trace can be followed by various microservices.
This fields and tags are automatically sent to Logstash when it is configured.
logger.info("logger line", extra={'tags': {'tag': 'tag_value'}, 'extra_field': 'extra_field_value'})
Default logger fields¶
Automatically some extra fields are added to the logging.
deploy_name: deploy name configured in system/deploy_name, it allows to distinguish logs of different deploys,
for example between staging, develop and production environments. * levelname: log level configured currently in the Microservice. * logger: logger name used when the logger is declared. * service_name: service name declared in the Microservice.
How to use metrics resource¶
All Tamarco meters implement the Flyweight pattern, this means that no matter where you instantiate the meter if two or more meters have the same characteristics they are going to be the same object. You don’t need to be careful about using the same object in multiple places.
Counter¶
A counter is a cumulative metric that represents a single numerical value that only goes up. The counter is reseated when the server restart. A counter can be used to count requests served, events, tasks completed, errors occurred, etc.
cats_counter = Counter('cats', 'animals')
meows_counter = Counter('meows', 'sounds')
jumps_counter = Counter('jumps', 'actions')
class Cat:
def __init__(self):
cats_counter.inc()
# It can work as a decorator, every time a function is called, the counter is increased in one.
@meows_counter
def meow(self):
print('meow')
# Similarly it can be used as a decorator of coroutines.
@jumps_counter
async def jump(self):
print("jump")
Gauge¶
A gauge is a metric that represents a single numerical value. Unlike the counter, it can go down. Gauges are typically used for measured values like temperatures, current memory usage, coroutines, CPU usage, etc. You need to take into account that this kind of data only save the last value when it is reported.
It is used similarly to the counter, a simple example:
ws_connections_metric = Gauge("websocket_connections", "connections")
class WebSocketServer:
@ws_connections_metric
def on_open(self):
...
def on_close(self):
ws_connections_metric.dec()
...
Summary¶
A summary samples observations over sliding windows of time and provides instantaneous insight into their distributions, frequencies, and sums). They are typically used to get feedback about quantities where the distribution of the data is important, as the processing times.
The default quantiles are: [0.5, 0.75, 0.9, 0.95, 0.99].
Timer¶
Gauge and Summary can be used as timers. The timer admits to be used as a context manager and as a decorator:
request_processing_time = Summary("http_requests_processing_time", "time")
@request_processing_time.timeit()
def http_handler(request):
...
import time
my_task_processing_time_gauge = Gauge("my_task_processing_time", "time")
with my_task_processing_time_gauge.timeit()
my_task()
Labels¶
The metrics admit labels to attach additional information in a counter. For example, the status code of an HTTP response can be used as a label to monitoring the amount of failed requests.
A meter with labels:
http_requests_ok = Counter('http_requests', 'requests', labels={'status_code': 200})
def http_request_ping(request):
http_requests_ok.inc()
...
To add a label to an already existent meter:
Explanations¶
A walk around the settings¶
Tamarco is an automation framework for managing the lifecycle and resources of the microservices. The configuration has a critical role in the framework, all the other resources and components of the framework strongly depend on the settings.
When you have thousands of microservices running in production the way to provide the configuration of the system becomes critical. Some desirable characteristics of a microservice settings framework are:
- The configuration should be centralized. A microservice compiled in a container should be able to run in differentenvironments without any change in the code. For example, the network location of a database or its credentialsaren’t going to be the same in a production environment or a staging environment.
- The configuration should be able to change in runtime without restarting the microservices. For example, you shouldbe able to update the configuration of your WebSocket server without close the existing connections.
- The configuration should have redundancy. One of the advantages of a microservice architecture is the facility toobtain redundancy in your services, you should be able to run the microservices in several machines if someonefails, the others should be able to work correctly. Nothing of this has a sense if your services aren’t able toread the configuration, so to take the benefits of this architectural advantage, all critical services of thesystem must be redundant as well.
The external backend supported by this framework right now is etcd v2, we strongly recommend its use in production with Tamarco.
Other settings backends are available to develop:
Dictionary
File based (YML or JSON)
Settings structure¶
The settings can be configured from a simple YAML in etcd [Link of how to configure an etcd from a file]. A generic setting could be the following:
etcd_ready: true
system:
deploy_name: tamarco_tutorial
logging:
profile: PRODUCTION
file: false
stdout: true
resources:
amqp:
host: 172.31.0.102
port: 5672
vhost: /
user: guest
password: guest
connection_timeout: 10
queues_prefix: ""
kafka:
bootstrap_servers: 172.31.0.1:9092,172.31.0.2:9092
microservices:
http_server:
application_cache_seconds: 10
The etcd_ready setting is written by the etcd configuration script when it finishes configuring all the other settings. This prevents the microservices from reading the settings before the environment is properly configured.
All the other tamarco settings are inside a root_path named “system”. The settings under the root path are:
- Deploy_name. Name that identifies a deploy, used by default by logging and metrics resources with the purpose ofdistinct logs and metrics from different deploys. Possible use cases: allow to filter logs of deploys in differentregions or by develop, staging and production with the same monitoring system.
- Logging: Configuration of the logging of the system, it is out of resources because this configuration can’t beavoided since it is a core component, all the microservices and all resources emit logs. More information about thepossible configuration in [TODO link to logging section].
- Resources: configurations of the resources of the system, it can be used by one or more microservices. See:
- Microservice: configuration of the business logic of each microservice. This section also has a special property,all the other settings can be configured by in this section for a specific microservice. See:
Microservice lifecycle¶
Start¶
When the microservice is initialized, the following steps are performed, automatically:
- Start provisional logging with default parameters. Needed in case of some error before being able to read the finallogging configuration from the settings.
- Initialize the settings. All the other resources of the framework depend on being able to read the centralizedconfiguration.
- Initialize the logging with the proper settings. With the settings available, the next step is to be sure that allthe resources can send proper log messages in case of failure before starting them.
- Call the pre_start of the microservice, that triggers the pre_start of the microservices. Operations that need tobe performed before starting the microservice. For example, a HTTP server could need to render some templates beforestart the server. It is not advisable to perform I/O operations in the pre_start statement.
- Call the start of the microservice, they are going to start all the resources. In the start statement the resourcesare expected to perform the initial I/O operations, start a server, connect to a database, etc.
- Call the post_start of the microservice, it is going to call the post_start of all the resources. In this step allthe resources should be working normally because they should be started in the previous step.
Tamarco builds a dependency graph of the order in that the resources should be initialized.
Status of a resource¶
All the resources should report their state, it can be one of the followings:
NOT_STARTED
CONNECTING
STARTED
STOPPING
STOPPED
FAILED
The status of all the resources are exposed via an HTTP API and used by the default restart policies to detect when a resource is failing.
Resource restart policies¶
The status resources come by default with the microservice and their responsibility is to apply the restart policies of the microservice and report the state of the resources via an HTTP API.
There are two settings to control automatically that a resource should do when it has a FAILED status:
system:
resources:
status:
restart_policy:
resources:
restart_microservice_on_failure: ['redis']
restart_resource_on_failure: ['kafka']
Where the microservice is identified by the name of the resource instance in the microservice class.
Keep in mind that the most recommended way is not to use these restart policies and implement a circuit breaker in each resource. But sometimes you could want a simpler solution and in some cases, the default restart policies can be an acceptable way to go.
Stop¶
The shut down of a microservice can be triggered by a restart policy (restart_microservice_on_failure), by a system signal, by a resource (not recommended, a resource shouldn’t have the responsibility of stopping a service) or by business code.
A service only should be stopped calling the method stop_gracefully of the microservice instance.
The shut down is performed doing the following steps:
Call stop() method of the microservice, it is going to call the stop() of all the resources.
Call post_stop() method of the microservice, it is going to call the post_stop() method of all the resources.
- The exit is going to be forced after 30 seconds if the microservice didn’t finish the shut down in this time orsome resource raises an exception stopping the service.
Overwrite lifecycle methods¶
The lifecycle methods are designed to be overwritten by the user, allowing to execute code at a certain point of the lifecycle. Just take into account that these methods are asynchronous and that the super() method should be called.
The available methods are:
pre_start
start
post_start
stop
post_stop
from tamarco import Microservice
class LifecycleMicroservice(Microservice):
async def pre_start(self):
print("Before pre_start of the service")
await super().pre_start()
print("After pre_start of the service")
async def start(self):
print("Before start of the service")
await super().start()
print("After start of the service")
async def post_start(self):
print("Before post_start of the service")
await super().start()
print("After post_start of the service")
async def stop(self):
print("Before stop of the service")
await super().stop()
print("After stop of the service")
async def post_stop(self):
print("Before post_stop of the service")
await super().stop()
print("After post_stop of the service")
def main():
microservice = LifecycleMicroservice(Microservice)
microservice.run()
def __name__ == '__main__':
main()
Microservice base class¶
All the microservices must inherit from the Tamarco Microservice class. Let’s take a deeper look into this class.
To launch the microservice, we use the run function:
.. code-block:: python
from tamarco.core.microservice import Microservice
- class MyMicroservice(Microservice):
[…]
ms = MyMicroservice() ms.run()
When we run the microservice, there is a certain order in the setup of the service and then the event loop is running until an unrecoverable error occurs, or it is stopped.
Setup steps:
1. Configure and load the microservice settings (and of its resources if used). 1. Configure and start the logging service. 1. Pre-start stage: run all the Tamarco resources pre_start methods (only the resources actually used by the microservice). This method can be overriden if we want to do some coding in this step. But don’t forget to call to the Tamarco function too!
1. Start stage: run all the Tamarco resources start methods (only the resources actually used by the microservice). Also collects all the task declared in the microservice (using the @task decorator in a method) and launch them. Generally in this stage is when the database connections, or other services used by the resources are started. This start method can be overriden if we want to do some coding in this step. But don’t forget to call to the Tamarco function too! 1. Post-start stage: run all the Tamarco resources post_start methods (only the resources actually used by the microservice). This method can be overriden if we want to do some coding in this step. But don’t forget to call to the Tamarco function too! 1. Stop stage: run all the Tamarco resources stop methods (only the resources actually used by the microservice). In this stage all resources and tasks are stopped. This method can be overriden if we want to do some coding in this step. But don’t forget to call to the Tamarco function too! 2. Post-stop stage: run all the Tamarco resources post_stop methods (only the resources actually used by the microservice). This step is useful if you want to make some instructions when the microservice stops. This post_stop method can be overriden if we want to do some coding in this step. But don’t forget to call to the Tamarco function too!
Microservice cookicutter template¶
When you install the tamarco python package is available a _tamarco_ command. Calling this command you can create a new microservice skeleton answering before a few questions:
$ tamarco start_project
1. Project name: project name. In the same directory when you execute the tamarco command the script will create a folder with this name and all the initial files insite it. Used also in the docs and README files. 1. Project slug: project short name. Inside of the project name folder, a folder with this name is created and all the microservice logic code should be here. Used also in the docs files. 1. Full name: author’s full name. Used in the docs files. 1. Email: author’s email. Used in the docs files. 1. Version: initial project version. It will be copied to the setup.cfg file. 1. Project short description: this text will be in the initial README file created.
The project skeleton will be:
<project_name>
|
|- docs (folder with the files to generate Sphinx documentation)
|
|- tests (here will be store the microservice tests)
|
|- <project_slug>
| |
| |- logic (microservice business logic code)
| |
| |- resources (code related with the microservice resources: databases, ...)
| |
| |- meters.py (application meters: prometheus, ...)
| |
| |- microservice.py (microservice class inherited from Tamarco Microservice class)
|
|- .coveragerc (coverage configuration file)
|
|- .gitignore
|
|- app.py (entrypoint file for the microservice)
|
|- Dockerfile
|
|- HISTORY.md
|
|- Makefile (run the tests, generate docs, create virtual environments, install requirements, ...)
|
|- README.md
|
|- requirements.txt
|
|- setup.cfg (several python packages configurations: bumpversion, flake8, pytest, ...)
|
Reference¶
Core¶
-
class
tamarco.core.microservice.
Microservice
[source]¶ Main class of a microservice. This class is responsible for controlling the lifecycle of the microservice, also builds and provides the necessary elements that a resource needs to work.
The resources of a microservice should be declared in this class. The microservice automatically takes the ownership of all the declared resources.
-
async
post_start
()[source]¶ Post start stage of lifecycle. This method can be overwritten by the user to add some logic in the start.
-
async
post_stop
()[source]¶ Post stop stage of the lifecycle. This method can be overwritten by the user to add some logic to the shut down.
-
async
pre_start
()[source]¶ Pre start stage of lifecycle. This method can be overwritten by the user to add some logic in the start.
-
run
()[source]¶ Run a microservice. It initializes the main event loop of asyncio, so this function only are going to end when the microservice ends its live cycle.
-
async
start
()[source]¶ Start stage of lifecycle. This method can be overwritten by the user to add some logic in the start.
-
async
-
class
tamarco.core.microservice.
MicroserviceContext
[source]¶ “This class is used to use tamarco resources without using a full microservice, for example a script.
-
tamarco.core.microservice.
task
(name_or_fn)[source]¶ Decorator to convert a method of a microservice in a asyncio task. The task is started and stopped when the microservice starts and stops respectively.
- Parameters
name_or_fn – Name of the task or function. If function the task name is the declared name of the function.
-
tamarco.core.microservice.
task_timer
(interval=1000, one_shot=False, autostart=False) → Union[collections.abc.Callable, Coroutine][source]¶ Decorator to declare a task that should repeated in time intervals.
Examples
>>> @task_timer() >>> async def execute(*arg,**kwargs) >>> print('tick')
>>> @task_timer(interval=1000, oneshot=True, autostart=True) >>> async def execute(*args,**kwargs) >>> print('tick')
- Parameters
interval (int) – Interval in milliseconds when the task is repeated.
one_shot (bool) – Only runs the task once.
autostart (bool) – Task is automatically initialized with the microservice.
-
tamarco.core.microservice.
thread
(name_or_fn)[source]¶ Decorator to convert a method of a microservice in a thread. The thread is started and stopped when the microservice starts and stops respectively.
- Parameters
name_or_fn – Name of the thread or function. If function the thread name is the declared name of the function.
Logging¶
-
class
tamarco.core.logging.logging.
Logging
[source]¶ Class that handles the configuration of the standard logging of python using the microservice settings.
-
configure_settings
(settings)[source]¶ Sets the settings object (a SettingsView(f”{ROOT_SETTINGS}.logging”)).
- Parameters
settings (SettingsInterface) – Settings object that have the logging settings.
-
static
describe_dynamic_settings
()[source]¶ Describe all the class dynamic settings.
- Returns
Settings and their description.
- Return type
dict
-
static
describe_static_settings
()[source]¶ Describe all the settings as a dictionary keys and their values are a setting short description. These settings are the static settings needed by the class.
- Returns
Settings and their description.
- Return type
dict
-
async
start
(loggers, microservice_name, deploy_name, loop)[source]¶ Configure the standard python logging, adding handlers and loggers that uses that handlers.
- Parameters
loggers (list) – Names of the loggers you want to configure.
microservice_name (str) – Name of the microservice that will use the logging.
deploy_name (str) – Deploy name.
loop – asyncio event loop.
-
Patterns¶
-
class
tamarco.core.patterns.
Singleton
[source]¶ Singleton pattern implementation.
This pattern restricts the instantiation of a class to one object.
-
class
tamarco.core.patterns.
Proxy
(obj)[source]¶ Proxy pattern to be used as a pointer. When the value of _obj changes, the reference to the proxy remains.
-
class
tamarco.core.patterns.
Flyweight
(name, bases, dct)[source]¶ Metaclass that implements the Flyweight pattern.
It is like a Singleton but only for the instances with the same key. The key is first parameter that you pass to the class when you create the object.
This class is conceived for the internal use of the Tamarco metrics library.
Example:
>>> class Metric(metaclass=Flyweight): >>> def __init__(self, metric_id): >>> self.metric_id = metric_id >>> >>> http_requests_1 = Metric('http_requests') >>> http_requests_2 = Metric('http_requests') >>> >>> http_requests_1 == http_requests_2 True
-
class
tamarco.core.patterns.
FlyweightWithLabels
(name, bases, dct)[source]¶ Metaclass that extends the pattern of the Flyweight pattern with labels.
This class is conceived for the internal use of the Tamarco metrics library.
Example:
>>> class Metric(metaclass=FlyweightWithLabels): >>> def __init__(self, metric_id, labels=None): >>> self.metric_id = metric_id >>> self.labels = labels if labels else {} >>> >>> requests_http_get_1 = Metric('request', labels={'protocol': 'http', 'method': 'get'}) >>> requests_http_post_1 = Metric('request', labels={'protocol': 'http', 'method': 'post'}) >>> >>> requests_http_get_2 = Metric('request', labels={'protocol': 'http', 'method': 'get'}) >>> requests_http_post_2 = Metric('request', labels={'protocol': 'http', 'method': 'post'}) >>> >>> requests_http_get_1 == requests_http_get_2 True >>> requests_http_post_1 == requests_http_post_2 True
Settings¶
-
class
tamarco.core.settings.settings.
Settings
[source]¶ Core settings class, here is the unique True of settings all the settings values are cached by this class in his internal_backend, all of the other settings are views of the data that this class holds.
The external backend is where the settings should be originally loaded, the internal backend acts as cache to avoid making many requests to the external backend.
-
async
bind
(loop)[source]¶ Binds the settings to one event loop.
- Parameters
loop – Main asyncio event loop.
-
async
cancel_watch_tasks
()[source]¶ Cancel all the pending watcher tasks of the settings in the etcd backend.
-
async
get
(key, default=<class 'tamarco.core.settings.backends.interface._EmptyArg'>)[source]¶ Get a setting value for a key.
- Parameters
key (str) – Path to the setting.
default – Default value in the case that it doesn’t exists.
- Raises
SettingNotFound – The setting can’t be resolved and it hasn’t default value.
- Returns
Setting value.
-
async
get_external
(key, default=<class 'tamarco.core.settings.backends.interface._EmptyArg'>)[source]¶ Get the setting from the external backend updating the internal one with the value of the external.
- Parameters
key (str) – Path to the setting.
default – Default value in case that the setting doesn’t exists in the external backend.
- Returns
Setting value.
-
register_promised_setting
(key, promised_setting)[source]¶ Register a SettingProxy to be resolved when the settings are loaded.
- Parameters
key (str) – setting key to register.
promised_setting – setting proxy to register.
-
async
set
(key, value)[source]¶ Set a setting value.
- Parameters
key (str) – Path to the setting.
value – Value to be set in the setting key.
-
async
start
()[source]¶ Start the settings. First loads the settings from the external settings backend (etcd or yaml file) once the internal and external settings backends are ready, the promised settings (when_loaded_settings) are resolved and the proxies start to holds the settings values.
-
update_internal
(dict_settings)[source]¶ Update the internal cache with new settings.
- Parameters
dict_settings (dict) – Settings to add to the internal backend.
-
async
update_internal_settings
(key, value)[source]¶ Update an specific internal setting.
- Parameters
key (str) – Path to the setting.
value – Setting value.
-
async
watch
(key, callback)[source]¶ Schedule a callback for when a setting is changed in the etcd backend.
- Parameters
key (str) – Path to the setting.
callback – function or coroutine to be called when the setting changes, it should have with two input arguments, one for the setting path and other for the setting value.
-
async
-
class
tamarco.core.settings.settings.
SettingsView
(settings, prefix, microservice_name=None)[source]¶ View/chroot/jail/box of main settings class. Used in the resources to provide them with their subset of settings.
-
async
cancel_watch_tasks
()[source]¶ Cancel all the pending watcher tasks of the settings in the etcd backend.
-
async
delete
(key, raw=False)[source]¶ Delete a setting.
- Parameters
key (str) – Path to the setting.
raw – If True no prefix is used so is not a view.
-
async
get
(key, default=<class 'tamarco.core.settings.backends.interface._EmptyArg'>, raw=False)[source]¶ Get setting.
- Parameters
key (str) – Path to the setting.
default – Default value in case that the setting doesn’t exists in the external backend.
raw – if True no prefix is used so is not a view.
-
async
set
(key, value, raw=False)[source]¶ Set a setting value.
- Parameters
key (str) – Path to the setting.
default – Default value in the case that it doesn’t exists.
raw – If True no prefix is used so is not a view.
- Returns
Setting value.
-
async
Resources¶
-
class
tamarco.resources.bases.
BaseResource
[source]¶ Define the basic interface of a resource. All the tamarco resources should inherit from this class.
- Resource start call chain:
bind
configure_settings
pre_start
start
post_start
- Resource stop call chain:
stop
post_stop
-
async
bind
(microservice, name)[source]¶ Build method, the microservice binds all its resources. Microservice starts and stops the resources.
- Parameters
microservice (Microservice) – Microservice instance managing the resource.
name (str) – Name of the resource instance in the microservice class.
-
async
configure_settings
(settings)[source]¶ Build method, the microservice provides the settings class of each resource. The resource should read the settings via this object.
- Parameters
settings (SettingsView) – Settings view of the resource.
-
class
tamarco.resources.bases.
DatabaseResource
(*args, **kwargs)[source]¶
Contribution guide¶
Welcome to the project!! First of all we want to thank you, we would like to have new collaborators and contributions.
This project is governed by the Tamarco Code of Conduct and we expect that all our members follow them.
Your first contribution¶
There are so many ways to help, improve the documentation, write tutorials or examples, improve the docstrings, make tests, report bugs, etc.
You can take a look at the tickets with the tag good first issue
.
Running tests and linters¶
All the contributions must have at least unit tests.
Make sure that the test are in the correct place. We have separated the tests in two categories, the unit tests
(test/unit
) and the functional tests (test/functional
). Inside each folder the test should follow the same structure
than the main package. For example, a unit test of tamarco/core/microservice.py
should be placed in
tests/unit/core/test_microservice.py
.
Functional tests are considered those that do some kind of I/O, such as those that need third party services (AMQP, Kafka, Postgres, …), open servers (http and websocket resource), manage files or wait for an event. The goal is maintain unit tests that can be passed quickly during development.
Most of the functional tests need docker and docker-compose installed in the system to use some third party services.
Before summit a pull request, please check that all the tests and linters are passing.
make test
make linters
Code review process¶
The project maintainers will leave the feedback.
You need at least two approvals from core developers.
The tests and linters should pass in the CI.
The code must have at least the 80% of coverage.
Contributor Covenant Code of Conduct¶
Our Pledge¶
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
Our Standards¶
Examples of behavior that contributes to creating a positive environment include:
Using welcoming and inclusive language
Being respectful of differing viewpoints and experiences
Gracefully accepting constructive criticism
Focusing on what is best for the community
Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
The use of sexualized language or imagery and unwelcome sexual attention or advances
Trolling, insulting/derogatory comments, and personal or political attacks
Public or private harassment
Publishing others’ private information, such as a physical or electronic address, without explicit permission
Other conduct which could reasonably be considered inappropriate in a professional setting
Our Responsibilities¶
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
Scope¶
This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
Enforcement¶
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@system73.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership.
Attribution¶
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq