Skip to content

Commit

Permalink
Telemetry support (#339)
Browse files Browse the repository at this point in the history
* Add foundation for telemetry (disabled by default)

---------

Co-authored-by: Brandon Squizzato <[email protected]>
  • Loading branch information
mjholder and bsquizz authored Dec 18, 2023
1 parent 18a0dea commit f57f4ce
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 2 deletions.
8 changes: 7 additions & 1 deletion bonfire/bonfire.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from wait_for import TimedOutError

import bonfire.config as conf
from bonfire.elastic_logging import ElasticLogger
from bonfire.local import get_local_apps, get_appsfile_apps
from bonfire.utils import AppOrComponentSelector, RepoFile, SYNTAX_ERR
from bonfire.namespaces import (
Expand Down Expand Up @@ -51,7 +52,9 @@
merge_app_configs,
)


log = logging.getLogger(__name__)
es_telemetry = ElasticLogger()

APP_SRE_SRC = "appsre"
FILE_SRC = "file"
Expand All @@ -66,6 +69,7 @@


def _error(msg):
es_telemetry.send_telemetry(msg, success=False)
click.echo(f"ERROR: {msg}", err=True)
sys.exit(1)

Expand All @@ -86,7 +90,8 @@ def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
result = f(*args, **kwargs)
return result
except KeyboardInterrupt:
_error(f"{command}: aborted by keyboard interrupt")
except TimedOutError as err:
Expand Down Expand Up @@ -1350,6 +1355,7 @@ def _err_handler(err):
_err_handler(err)
else:
log.info("successfully deployed to namespace %s", ns)
es_telemetry.send_telemetry("successful deployment")
url = get_console_url()
if url:
ns_url = f"{url}/k8s/cluster/projects/{ns}"
Expand Down
8 changes: 7 additions & 1 deletion bonfire/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@

DEFAULT_GRAPHQL_URL = "https://app-interface.apps.appsrep05ue1.zqxk.p1.openshiftapps.com/graphql"

DEFAULT_ELASTICSEARCH_HOST = "https://localhost:9200/search-bonfire/_doc"
DEFAULT_ENABLE_TELEMETRY = "false"

ENV_FILE = str(DEFAULT_ENV_PATH.absolute()) if DEFAULT_ENV_PATH.exists() else ""
load_dotenv(ENV_FILE)

Expand All @@ -55,14 +58,17 @@
# can be used to set name of 'requester' on namespace reservations
BONFIRE_NS_REQUESTER = os.getenv("BONFIRE_NS_REQUESTER")
# set to true when bonfire is running via automation using a bot acct (not an end user)
BONFIRE_BOT = os.getenv("BONFIRE_BOT")
BONFIRE_BOT = os.getenv("BONFIRE_BOT", "false").lower() == "true"

BONFIRE_DEFAULT_PREFER = str(os.getenv("BONFIRE_DEFAULT_PREFER", "ENV_NAME=frontends")).split(",")
BONFIRE_DEFAULT_REF_ENV = str(os.getenv("BONFIRE_DEFAULT_REF_ENV", "insights-stage"))
BONFIRE_DEFAULT_FALLBACK_REF_ENV = str(
os.getenv("BONFIRE_DEFAULT_FALLBACK_REF_ENV", "insights-stage")
)

ELASTICSEARCH_HOST = os.getenv("ELASTICSEARCH_HOST", DEFAULT_ELASTICSEARCH_HOST)
ELASTICSEARCH_APIKEY = os.getenv("ELASTICSEARCH_APIKEY")
ENABLE_TELEMETRY = os.getenv("ENABLE_TELEMETRY", DEFAULT_ENABLE_TELEMETRY).lower() == "true"

DEFAULT_FRONTEND_DEPENDENCIES = (
"chrome-service",
Expand Down
85 changes: 85 additions & 0 deletions bonfire/elastic_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from datetime import datetime as dt
import logging
import json
import requests
import sys
import uuid
from concurrent.futures import ThreadPoolExecutor

import bonfire.config as conf


log = logging.getLogger(__name__)


class ElasticLogger:
def __init__(self):
self.es_telemetry = logging.getLogger("elasicsearch")

# prevent duplicate handlers
self.es_handler = next(
(h for h in self.es_telemetry.handlers if type(h) is AsyncElasticsearchHandler), None
)
if not self.es_handler:
self.es_handler = AsyncElasticsearchHandler(conf.ELASTICSEARCH_HOST)
self.es_telemetry.addHandler(self.es_handler)

def send_telemetry(self, log_message, success=True):
self.es_handler.set_success_status(success)

self.es_telemetry.info(log_message)


class AsyncElasticsearchHandler(logging.Handler):
def __init__(self, es_url):
super().__init__()
self.es_url = es_url
self.executor = ThreadPoolExecutor(max_workers=10)
self.start_time = dt.now()
self.metadata = {
"uuid": str(uuid.uuid4()),
"start_time": self.start_time.isoformat(),
"bot": conf.BONFIRE_BOT,
"command": self._mask_parameter_values(sys.argv[1:]),
}

def emit(self, record):
self.metadata["@timestamp"] = dt.now().isoformat()
self.metadata["elapsed_sec"] = (dt.now() - self.start_time).total_seconds()

log_entry = {"log": self.format(record), "metadata": self.metadata}
if conf.ENABLE_TELEMETRY:
self.executor.submit(self.send_to_es, json.dumps(log_entry))

def set_success_status(self, run_status):
self.metadata["succeeded"] = run_status

def send_to_es(self, log_entry):
# Convert log_entry to JSON and send to Elasticsearch
try:
headers = {
"Authorization": conf.ELASTICSEARCH_APIKEY,
"Content-Type": "application/json",
}

response = requests.post(self.es_url, headers=headers, data=log_entry, timeout=0.1)
response.raise_for_status()
except Exception as e:
# Handle exceptions (e.g., network issues, Elasticsearch down)
log.error("Error sending data to elasticsearch: %s", e)

@staticmethod
def _mask_parameter_values(cli_args):
masked_list = []

is_parameter = False
for arg in cli_args:
if is_parameter:
masked_arg = f"{arg.split('=')[0]}=*******"
masked_list.append(masked_arg)
is_parameter = False
else:
masked_list.append(arg)
is_parameter = arg == "-p" or arg == "--set-parameter"

return masked_list
1 change: 1 addition & 0 deletions bonfire/namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@


log = logging.getLogger(__name__)

TIME_FMT = "%Y-%m-%dT%H:%M:%SZ"


Expand Down

0 comments on commit f57f4ce

Please sign in to comment.