Apache Airflow dags w/ backend configuration bundle.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

181 lines
6.7 KiB

#
# Licensed to the Apache Software Foundation (ASF) under one
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# 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.
import time
from typing import Any, Dict, List, Optional, Union
from airflow.exceptions import AirflowException
from airflow.hooks.base import BaseHook
from airflow.utils.log.logging_mixin import LoggingMixin
from datadog import api, initialize
class DatadogHook(BaseHook, LoggingMixin):
"""
Uses datadog API to send metrics of practically anything measurable,
so it's possible to track # of db records inserted/deleted, records read
from file and many other useful metrics.
Depends on the datadog API, which has to be deployed on the same server where
Airflow runs.
:param datadog_conn_id: The connection to datadog, containing metadata for api keys.
:param datadog_conn_id: str
"""
def __init__(self, datadog_conn_id: str = "datadog_default") -> None:
super().__init__()
conn = self.get_connection(datadog_conn_id)
self.api_key = conn.extra_dejson.get("api_key", None)
self.app_key = conn.extra_dejson.get("app_key", None)
self.source_type_name = conn.extra_dejson.get("source_type_name", None)
# If the host is populated, it will use that hostname instead.
# for all metric submissions.
self.host = conn.host
if self.api_key is None:
raise AirflowException(
"api_key must be specified in the Datadog connection details"
)
self.log.info("Setting up api keys for Datadog")
initialize(api_key=self.api_key, app_key=self.app_key)
def validate_response(self, response: Dict[str, Any]) -> None:
"""Validate Datadog response"""
if response["status"] != "ok":
self.log.error("Datadog returned: %s", response)
raise AirflowException("Error status received from Datadog")
def send_metric(
self,
metric_name: str,
datapoint: Union[float, int],
tags: Optional[List[str]] = None,
type_: Optional[str] = None,
interval: Optional[int] = None,
) -> Dict[str, Any]:
"""
Sends a single datapoint metric to DataDog
:param metric_name: The name of the metric
:type metric_name: str
:param datapoint: A single integer or float related to the metric
:type datapoint: int or float
:param tags: A list of tags associated with the metric
:type tags: list
:param type_: Type of your metric: gauge, rate, or count
:type type_: str
:param interval: If the type of the metric is rate or count, define the corresponding interval
:type interval: int
"""
response = api.Metric.send(
metric=metric_name,
points=datapoint,
host=self.host,
tags=tags,
type=type_,
interval=interval,
)
self.validate_response(response)
return response
def query_metric(
self, query: str, from_seconds_ago: int, to_seconds_ago: int
) -> Dict[str, Any]:
"""
Queries datadog for a specific metric, potentially with some
function applied to it and returns the results.
:param query: The datadog query to execute (see datadog docs)
:type query: str
:param from_seconds_ago: How many seconds ago to start querying for.
:type from_seconds_ago: int
:param to_seconds_ago: Up to how many seconds ago to query for.
:type to_seconds_ago: int
"""
now = int(time.time())
response = api.Metric.query(
start=now - from_seconds_ago, end=now - to_seconds_ago, query=query
)
self.validate_response(response)
return response
# pylint: disable=too-many-arguments
def post_event(
self,
title: str,
text: str,
aggregation_key: Optional[str] = None,
alert_type: Optional[str] = None,
date_happened: Optional[int] = None,
handle: Optional[str] = None,
priority: Optional[str] = None,
related_event_id: Optional[int] = None,
tags: Optional[List[str]] = None,
device_name: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
Posts an event to datadog (processing finished, potentially alerts, other issues)
Think about this as a means to maintain persistence of alerts, rather than
alerting itself.
:param title: The title of the event
:type title: str
:param text: The body of the event (more information)
:type text: str
:param aggregation_key: Key that can be used to aggregate this event in a stream
:type aggregation_key: str
:param alert_type: The alert type for the event, one of
["error", "warning", "info", "success"]
:type alert_type: str
:param date_happened: POSIX timestamp of the event; defaults to now
:type date_happened: int
:handle: User to post the event as; defaults to owner of the application key used
to submit.
:param handle: str
:param priority: Priority to post the event as. ("normal" or "low", defaults to "normal")
:type priority: str
:param related_event_id: Post event as a child of the given event
:type related_event_id: id
:param tags: List of tags to apply to the event
:type tags: list[str]
:param device_name: device_name to post the event with
:type device_name: list
"""
response = api.Event.create(
title=title,
text=text,
aggregation_key=aggregation_key,
alert_type=alert_type,
date_happened=date_happened,
handle=handle,
priority=priority,
related_event_id=related_event_id,
tags=tags,
host=self.host,
device_name=device_name,
source_type_name=self.source_type_name,
)
self.validate_response(response)
return response