Development

This page describes the StackStorm development processes and contains general guidelines and information on how to contribute to the project.

Contributing

We welcome and appreciate contributions of any kind (code, tests, documentation, examples, use cases, ...).

If you need help or get stuck at any point during this process, stop by on our Slack Community and we will do our best to assist you.

For information on contributing an integration pack, please refer to the Create and Contribute a Pack page.

For an overview of core StackStorm code structure, please refer to Code structure.

Setting up a Development Environment

There are multiple ways for you to set up a development environment and get started with StackStorm development.

The best and easiest approach is to use our Vagrant images which contains all the dependencies you need to get started. For more information, see st2vagrant.

Another approach is to install StackStorm and all the dependencies from source on a server or VM of your liking. For more information about this approach, see Installing StackStorm from sources.

General Contribution Guidelines

  • Any non-trivial change must contain corresponding tests. For more information, refer to the Testing page (or the Pack Testing page for pack development).
  • All the functions and methods must contain Sphinx docstrings which are used to generate the API documentation. We follow the Apache Libcloud project docstrings conventions. For more information, refer to the Docstring conventions page.
  • If you are adding a new feature, make sure to add a corresponding documentation and examples.

Code style guide

  • We follow PEP8 Python Style Guide
  • Use 4 spaces for a tab
  • Use 100 characters in a line
  • Make sure edited file doesn’t contain any trailing whitespace
  • Make sure that all the source files contains an Apache 2.0 license header. For example, see one of the existing Python files with source code.
  • You can verify that your modifications don’t break any rules by running the lint script - make flake8

Most StackStorm repositories use shared Flake8 and PyLint configuration files which you can get from the lint-configs repo.

And most importantly, follow the existing style in the file you are editing and be consistent.

General coding guidelines

Logging

Logging is important because it increases the visibility and makes the project easiest to debug and support.

You are encouraged to generously use the log statements across the code base - you should log every event which increases the visibility and / or makes the product easier to debug and support.

Every log statement should also include as much as useful additional context as possible. This context should be included in the dictionary which is passed via extra keyword argument to the logger method as shown bellow.

Default log formatters we use include this additional context as part of the message which makes it easier for user to find the relevant information.

On top of that, we also offer Gelf log formatters which outputs log messages in GELF format (structured JSON). Users can use this formatter to ship structured logs to Graylog2, loggly, logstash or a similar service.

Obtaining a reference to the logger object

To obtain a reference to the logger instance you should use st2common.log.getLogger function as shown bellow. You should use this function and not the one from the stdlib logging module because we declare a custom log level and do a couple of other things which are only available on loggers which are obtained through of version of getLogger.

In most cases, you should do that at the top of the module after the imports and re-use this logger through that module.

from st2common import log as logging

LOG = logging.getLogger(__name__)
LOG.debug('....')

Passing context to the logger

As noted above, you should always include as much context as possible in the log messages. Context is provided by passing a dictionary via the extra keyword argument to the logger method.

This dictionary should contain values which are relevant to the log message in question (e.g. created / modified database object, user who performed the action, etc.).

If you are passing an instance of a custom class as a value, you should implement to_dict method on that class. This method is responsible for returning a dictionary representation of this object which can be serialized as JSON.

Keep in mind that this method is already implemented for all of the StackStorm database objects (ActionDB, RunnerTypeDB, etc.).

action_db = ...
user_db = ...
remote_addr = ...

extra = {'action_db': action_db, 'user_db': user_db, 'remote_addr': remote_addr}
LOG.debug('New action has been created. ActionDB.id=%s' % (action_db.id),
          extra=extra)

Using the AUDIT log level

StackStorm code declares a custom AUDIT log level. This log level is to be when recording CRUD operations on the resources and when performing other actions which should be logged in the audit log.

For example:

LOG.audit('KeyValuePair updated. KeyValuePair.id=%s' % (kvp_db.id), extra=extra)

Dealing with dates and datetime objects

All the datetime objects which are being used in the codebase should be timezone aware and represented in UTC. Same goes for storing dates in the database - timestamps are preferred, but if you can’t use a timestamp, stored dates should be represented in UTC.

If you want to store a timestamp with a microsecond precision you should use st2common.fields.ComplexDateTimeField field class.

If you want to retrieve datetime object for current time, you should use st2common.util.date.get_datetime_utc_now which returns a timezone aware datetime object in UTC. st2common.util.date also contains other date and time related utility functions.

Instantiating model classes

When instantiating mongoengine model classes (e.g. ActionDB, RuleDB, SensorTypeDB, etc.) make sure to pass all the field values as arguments to the model constructor instead of performing a late assignment of variables on the class instance.

Good:

action_db = ActionDB(pack='mypack', name='myaction', enabled=True)

Bad:

action_db = ActionDB()
action_db.pack = 'mypack'
action_db.name = 'myaction'
action_db.enabled = True

Passing all the fields as keyword arguments to the constructor means we can preserve the constructor functionality. On top of that it also makes it more clear and obvious to the developers when the values are available and allows us to perform basic “static” analysis on the code.