2019-08-16 07:09:26 +03:00
.. Licensed to the Apache Software Foundation (ASF) under one
2018-11-13 17:01:44 +03:00
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
2019-08-16 07:09:26 +03:00
.. http://www.apache.org/licenses/LICENSE-2.0
2018-11-13 17:01:44 +03:00
2019-08-16 07:09:26 +03:00
.. Unless required by applicable law or agreed to in writing,
2018-11-13 17:01:44 +03:00
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
2019-08-16 07:09:26 +03:00
2015-06-11 09:09:22 +03:00
Plugins
=======
Airflow has a simple plugin manager built-in that can integrate external
2015-06-29 01:57:13 +03:00
features to its core by simply dropping files in your
2015-06-11 09:09:22 +03:00
`` $AIRFLOW_HOME/plugins `` folder.
2015-06-29 01:57:13 +03:00
The python modules in the `` plugins `` folder get imported,
2018-03-02 11:29:06 +03:00
and **hooks** , **operators** , **sensors** , **macros** , **executors** and web **views**
2015-06-11 09:09:22 +03:00
get integrated to Airflow's main collections and become available for use.
What for?
---------
2015-06-29 01:57:13 +03:00
Airflow offers a generic toolbox for working with data. Different
2015-06-11 09:09:22 +03:00
organizations have different stacks and different needs. Using Airflow
plugins can be a way for companies to customize their Airflow installation
to reflect their ecosystem.
2015-06-29 01:57:13 +03:00
Plugins can be used as an easy way to write, share and activate new sets of
2015-06-11 09:09:22 +03:00
features.
2015-06-29 01:57:13 +03:00
There's also a need for a set of more complex applications to interact with
2015-06-11 09:09:22 +03:00
different flavors of data and metadata.
Examples:
* A set of tools to parse Hive logs and expose Hive metadata (CPU /IO / phases/ skew /...)
* An anomaly detection framework, allowing people to collect metrics, set thresholds and alerts
* An auditing tool, helping understand who accesses what
* A config-driven SLA monitoring tool, allowing you to set monitored tables and at what time
2015-06-29 01:57:13 +03:00
they should land, alert people, and expose visualizations of outages
2015-06-11 09:09:22 +03:00
* ...
2015-06-29 01:57:13 +03:00
Why build on top of Airflow?
----------------------------
2015-06-11 09:09:22 +03:00
Airflow has many components that can be reused when building an application:
* A web server you can use to render your views
* A metadata database to store your models
2015-06-29 01:57:13 +03:00
* Access to your databases, and knowledge of how to connect to them
* An array of workers that your application can push workload to
2018-02-09 12:08:06 +03:00
* Airflow is deployed, you can just piggy back on its deployment logistics
2015-06-11 09:09:22 +03:00
* Basic charting capabilities, underlying libraries and abstractions
2015-06-18 00:31:05 +03:00
Interface
---------
2015-06-29 01:57:13 +03:00
To create a plugin you will need to derive the
2015-06-18 00:31:05 +03:00
`` airflow.plugins_manager.AirflowPlugin `` class and reference the objects
you want to plug into Airflow. Here's what the class you need to derive
2015-06-29 01:57:13 +03:00
looks like:
2015-06-18 00:31:05 +03:00
.. code :: python
2019-05-06 10:31:50 +03:00
class AirflowPlugin:
2015-06-18 00:31:05 +03:00
# The name of your plugin (str)
name = None
# A list of class(es) derived from BaseOperator
operators = []
2018-03-02 11:29:06 +03:00
# A list of class(es) derived from BaseSensorOperator
sensors = []
2015-06-18 00:31:05 +03:00
# A list of class(es) derived from BaseHook
hooks = []
# A list of class(es) derived from BaseExecutor
executors = []
# A list of references to inject into the macros namespace
macros = []
2019-02-25 00:52:49 +03:00
# A list of Blueprint object created from flask.Blueprint. For use with the flask_appbuilder based GUI
2015-06-18 00:31:05 +03:00
flask_blueprints = []
2018-10-23 19:21:30 +03:00
# A list of dictionaries containing FlaskAppBuilder BaseView object and some metadata. See example below
appbuilder_views = []
# A list of dictionaries containing FlaskAppBuilder BaseView object and some metadata. See example below
appbuilder_menu_items = []
2019-03-22 07:13:29 +03:00
# A function that validate the statsd stat name, apply changes to the stat name if necessary and
# return the transformed stat name.
#
# The function should have the following signature:
# def func_name(stat_name: str) -> str:
stat_name_handler = None
2019-02-13 22:58:05 +03:00
# A callback to perform actions when airflow starts and the plugin is loaded.
# NOTE: Ensure your plugin has *args, and * *kwargs in the method definition
# to protect against extra parameters injected into the on_load(...)
# function in future changes
def on_load(*args, * *kwargs):
# ... perform Plugin boot actions
pass
2019-04-25 20:00:00 +03:00
# A list of global operator extra links that can redirect users to
# external systems. These extra links will be available on the
# task page in the form of buttons.
#
# Note: the global operator extra link can be overridden at each
# operator level.
global_operator_extra_links = []
2018-10-23 19:21:30 +03:00
2015-06-18 00:31:05 +03:00
2018-08-05 21:24:20 +03:00
You can derive it by inheritance (please refer to the example below).
Please note `` name `` inside this class must be specified.
After the plugin is imported into Airflow,
you can invoke it using statement like
.. code :: python
2018-11-14 13:56:56 +03:00
from airflow.{type, like "operators", "sensors"}.{name specified inside the plugin class} import *
2018-08-05 21:24:20 +03:00
When you write your own plugins, make sure you understand them well.
There are some essential properties for each type of plugin.
For example,
* For `` Operator `` plugin, an `` execute `` method is compulsory.
* For `` Sensor `` plugin, a `` poke `` method returning a Boolean value is compulsory.
2019-01-02 16:11:15 +03:00
Make sure you restart the webserver and scheduler after making changes to plugins so that they take effect.
2018-08-05 21:24:20 +03:00
2019-04-25 20:00:00 +03:00
.. _plugin-example:
2015-06-11 09:09:22 +03:00
Example
-------
2016-02-02 00:25:56 +03:00
The code below defines a plugin that injects a set of dummy object
2015-06-29 01:57:13 +03:00
definitions in Airflow.
2015-06-11 09:09:22 +03:00
.. code :: python
2015-06-29 01:57:13 +03:00
2015-06-18 00:31:05 +03:00
# This is the class you derive to create a plugin
from airflow.plugins_manager import AirflowPlugin
from flask import Blueprint
2019-02-25 00:52:49 +03:00
from flask_appbuilder import expose, BaseView as AppBuilderBaseView
2015-06-11 09:09:22 +03:00
# Importing base classes that we need to derive
from airflow.hooks.base_hook import BaseHook
2018-03-02 11:29:06 +03:00
from airflow.models import BaseOperator
2019-04-25 20:00:00 +03:00
from airflow.models.baseoperator import BaseOperatorLink
2018-03-02 11:29:06 +03:00
from airflow.sensors.base_sensor_operator import BaseSensorOperator
2015-06-11 09:09:22 +03:00
from airflow.executors.base_executor import BaseExecutor
2016-10-02 09:43:20 +03:00
# Will show up under airflow.hooks.test_plugin.PluginHook
2015-06-11 09:09:22 +03:00
class PluginHook(BaseHook):
pass
2016-10-02 09:43:20 +03:00
# Will show up under airflow.operators.test_plugin.PluginOperator
2015-06-11 09:09:22 +03:00
class PluginOperator(BaseOperator):
pass
2018-03-02 11:29:06 +03:00
# Will show up under airflow.sensors.test_plugin.PluginSensorOperator
class PluginSensorOperator(BaseSensorOperator):
pass
2016-10-02 09:43:20 +03:00
# Will show up under airflow.executors.test_plugin.PluginExecutor
2015-06-11 09:09:22 +03:00
class PluginExecutor(BaseExecutor):
pass
2016-10-02 09:43:20 +03:00
# Will show up under airflow.macros.test_plugin.plugin_macro
2019-03-26 13:19:15 +03:00
# and in templates through {{ macros.test_plugin.plugin_macro }}
2016-10-02 09:43:20 +03:00
def plugin_macro():
pass
2018-06-04 21:15:35 +03:00
# Creating a flask blueprint to integrate the templates and static folder
2015-06-18 00:31:05 +03:00
bp = Blueprint(
"test_plugin", __name__,
2016-10-02 09:43:20 +03:00
template_folder='templates', # registers airflow/plugins/templates as a Jinja template folder
2015-06-18 00:31:05 +03:00
static_folder='static',
static_url_path='/static/test_plugin')
2016-10-02 09:43:20 +03:00
2018-10-23 19:21:30 +03:00
# Creating a flask appbuilder BaseView
class TestAppBuilderBaseView(AppBuilderBaseView):
2019-01-12 10:19:40 +03:00
default_view = "test"
2018-10-23 19:21:30 +03:00
@expose("/")
def test(self):
return self.render("test_plugin/test.html", content="Hello galaxy!")
2019-02-25 00:52:49 +03:00
2018-10-23 19:21:30 +03:00
v_appbuilder_view = TestAppBuilderBaseView()
v_appbuilder_package = {"name": "Test View",
"category": "Test Plugin",
"view": v_appbuilder_view}
# Creating a flask appbuilder Menu Item
appbuilder_mitem = {"name": "Google",
"category": "Search",
"category_icon": "fa-th",
"href": "https://www.google.com"}
2019-03-22 07:13:29 +03:00
# Validate the statsd stat name
def stat_name_dummy_handler(stat_name):
return stat_name
2019-04-25 20:00:00 +03:00
# A global operator extra link that redirect you to
# task logs stored in S3
class S3LogLink(BaseOperatorLink):
name = 'S3'
def get_link(self, operator, dttm):
return 'https://s3.amazonaws.com/airflow-logs/{dag_id}/{task_id}/{execution_date}'.format(
dag_id=operator.dag_id,
task_id=operator.task_id,
execution_date=dttm,
)
2015-06-18 00:31:05 +03:00
# Defining the plugin class
class AirflowTestPlugin(AirflowPlugin):
name = "test_plugin"
operators = [PluginOperator]
2018-03-02 11:29:06 +03:00
sensors = [PluginSensorOperator]
2015-06-18 00:31:05 +03:00
hooks = [PluginHook]
executors = [PluginExecutor]
2016-10-02 09:43:20 +03:00
macros = [plugin_macro]
flask_blueprints = [bp]
2018-10-23 19:21:30 +03:00
appbuilder_views = [v_appbuilder_package]
appbuilder_menu_items = [appbuilder_mitem]
[AIRFLOW-4057] Fix bug in stat name validation (#4974)
* [AIRFLOW-4057] Fix bug in stat name validation
The `@validate_stats` wrapper is wrapping an instance method, so it's first
argument is actually an instance of `SafeStatsdLogger`, not the stat_name.
Here's the backtrace that shows up in the webserver logs without the fix:
File "/Users/andrewstahlman/src/incubator-airflow/airflow/www/views.py", line 86, in <module>
dagbag = models.DagBag(os.devnull, include_examples=False)
File "/Users/andrewstahlman/src/incubator-airflow/airflow/models/__init__.py", line 312, in __init__
safe_mode=safe_mode)
File "/Users/andrewstahlman/src/incubator-airflow/airflow/models/__init__.py", line 594, in collect_dags
'collect_dags', (timezone.utcnow() - start_dttm).total_seconds(), 1)
File "/Users/andrewstahlman/src/incubator-airflow/airflow/stats.py", line 85, in wrapper
log.warning('Invalid stat name: {stat}.'.format(stat=stat), err)
Message: 'Invalid stat name: <airflow.stats.SafeStatsdLogger object at 0x10bf194a8>.'
Arguments: (InvalidStatsNameException('The stat_name has to be a string'),)
I've verified the fix by updating the unit tests to exercise the "public"
methods rather than testing the internal validation logic directly.
* Wrap stat_name_handler in staticmethod
2019-03-26 06:15:10 +03:00
stat_name_handler = staticmethod(stat_name_dummy_handler)
2019-04-25 20:00:00 +03:00
global_operator_extra_links = [S3LogLink(),]
2018-10-23 19:21:30 +03:00
Note on role based views
------------------------
Airflow 1.10 introduced role based views using FlaskAppBuilder. You can configure which UI is used by setting
rbac = True. To support plugin views and links for both versions of the UI and maintain backwards compatibility,
the fields appbuilder_views and appbuilder_menu_items were added to the AirflowTestPlugin class.
2019-01-11 02:03:59 +03:00
Plugins as Python packages
--------------------------
2019-01-27 14:32:10 +03:00
It is possible to load plugins via `setuptools entrypoint <https://packaging.python.org/guides/creating-and-discovering-plugins/#using-package-metadata> `_ mechanism. To do this link
2019-01-11 02:03:59 +03:00
your plugin using an entrypoint in your package. If the package is installed, airflow
will automatically load the registered plugins from the entrypoint list.
2019-02-13 22:58:05 +03:00
.. note ::
2019-02-05 21:30:04 +03:00
Neither the entrypoint name (eg, `my_plugin` ) nor the name of the
plugin class will contribute towards the module and class name of the plugin
itself. The structure is determined by
`airflow.plugins_manager.AirflowPlugin.name` and the class name of the plugin
component with the pattern `airflow.{component}.{name}.{component_class_name}` .
2019-01-11 02:03:59 +03:00
.. code-block :: python
# my_package/my_plugin.py
from airflow.plugins_manager import AirflowPlugin
from airflow.models import BaseOperator
from airflow.hooks.base_hook import BaseHook
class MyOperator(BaseOperator):
pass
class MyHook(BaseHook):
pass
class MyAirflowPlugin(AirflowPlugin):
name = 'my_namespace'
operators = [MyOperator]
hooks = [MyHook]
.. code-block :: python
from setuptools import setup
setup(
name="my-package",
...
entry_points = {
'airflow.plugins': [
'my_plugin = my_package.my_plugin:MyAirflowPlugin'
]
}
)
This will create a hook, and an operator accessible at:
- `airflow.hooks.my_namespace.MyHook`
- `airflow.operators.my_namespace.MyOperator`