def configure_opentelemetry(flask_app: flask.Flask):
    """Configure a flask application to use OpenTelemetry.

    This activates the specific components:

    * sets tracer to the SDK's Tracer
    * enables requests integration on the Tracer
    * uses a WSGI middleware to enable configuration

    TODO:

    * processors?
    * exporters?
    """
    # Start by configuring all objects required to ensure
    # a complete end to end workflow.
    # the preferred implementation of these objects must be set,
    # as the opentelemetry-api defines the interface with a no-op
    # implementation.
    trace.set_preferred_tracer_implementation(lambda _: Tracer())
    # Next, we need to configure how the values that are used by
    # traces and metrics are propagated (such as what specific headers
    # carry this value).

    # TBD: can remove once default TraceContext propagators are installed.
    propagators.set_global_httptextformat(B3Format())

    # Integrations are the glue that binds the OpenTelemetry API
    # and the frameworks and libraries that are used together, automatically
    # creating Spans and propagating context as appropriate.
    opentelemetry.ext.http_requests.enable(trace.tracer())
    instrument_app(flask_app)
Beispiel #2
0
    def test_distributed_context(self):
        previous_propagator = propagators.get_global_httptextformat()
        try:
            propagators.set_global_httptextformat(MockHTTPTextFormat())
            result = requests.get(self.URL)
            self.assertEqual(result.text, "Hello!")

            span_list = self.memory_exporter.get_finished_spans()
            self.assertEqual(len(span_list), 1)
            span = span_list[0]

            headers = dict(httpretty.last_request().headers)
            self.assertIn(MockHTTPTextFormat.TRACE_ID_KEY, headers)
            self.assertEqual(
                str(span.get_context().trace_id),
                headers[MockHTTPTextFormat.TRACE_ID_KEY],
            )
            self.assertIn(MockHTTPTextFormat.SPAN_ID_KEY, headers)
            self.assertEqual(
                str(span.get_context().span_id),
                headers[MockHTTPTextFormat.SPAN_ID_KEY],
            )

        finally:
            propagators.set_global_httptextformat(previous_propagator)
Beispiel #3
0
    def setUpClass(cls):
        """Set preferred tracer implementation only once rather than before
        every test method.
        """

        trace.set_preferred_tracer_implementation(lambda T: Tracer())

        # Save current propagator to be restored on teardown.
        cls._previous_propagator = propagators.get_global_httptextformat()

        # Set mock propagator for testing.
        propagators.set_global_httptextformat(MockHTTPTextFormat)
    def test_extract_empty_context_returns_invalid_context(self):
        """In the case where the propagator cannot extract a
        SpanContext, extract should return and invalid span context.
        """
        _old_propagator = propagators.get_global_httptextformat()
        propagators.set_global_httptextformat(NOOPHTTPTextFormat())
        try:
            carrier = {}

            ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier)
            self.assertEqual(ctx.unwrap(), trace.INVALID_SPAN_CONTEXT)
        finally:
            propagators.set_global_httptextformat(_old_propagator)
    def test_distributed_context(self):
        previous_propagator = propagators.get_global_httptextformat()
        try:
            propagators.set_global_httptextformat(MockHTTPTextFormat())
            result = self.perform_request(self.URL)
            self.assertEqual(result.text, "Hello!")

            span = self.assert_span()

            headers = dict(httpretty.last_request().headers)
            self.assertIn(MockHTTPTextFormat.TRACE_ID_KEY, headers)
            self.assertEqual(
                str(span.get_context().trace_id),
                headers[MockHTTPTextFormat.TRACE_ID_KEY],
            )
            self.assertIn(MockHTTPTextFormat.SPAN_ID_KEY, headers)
            self.assertEqual(
                str(span.get_context().span_id),
                headers[MockHTTPTextFormat.SPAN_ID_KEY],
            )

        finally:
            propagators.set_global_httptextformat(previous_propagator)
Beispiel #6
0
from opentelemetry import trace, propagators
from opentelemetry.sdk.trace import Tracer
from opentelemetry.sdk.context.propagation.b3_format import B3Format
from opentelemetry.ext.http_requests import enable
from opentelemetry.ext.wsgi import OpenTelemetryMiddleware
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor

from kitchen_service import KitchenService
from kitchen_consumer import KitchenConsumer
from donut import Donut
from status import NEW_ORDER

trace.set_preferred_tracer_implementation(lambda T: Tracer())
propagators.set_global_httptextformat(B3Format())
tracer = trace.tracer()
enable(tracer)

tracer.add_span_processor(SimpleExportSpanProcessor(ConsoleSpanExporter()))

app = Flask(__name__)
app.static_folder = 'static'

app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app)

kitchen_service = KitchenService()
kitchen_consumer = KitchenConsumer()


@app.route('/')
 def tearDownClass(cls):
     # Restore previous propagator.
     propagators.set_global_httptextformat(cls._previous_propagator)
    def setUpClass(cls):
        # Save current propagator to be restored on teardown.
        cls._previous_propagator = propagators.get_global_httptextformat()

        # Set mock propagator for testing.
        propagators.set_global_httptextformat(MockHTTPTextFormat())
# 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 opentelemetry.ext.requests
import requests
from opentelemetry import trace
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
from opentelemetry.exporter.cloud_trace.cloud_trace_propagator import (
    CloudTraceFormatPropagator, )
from opentelemetry.propagators import set_global_httptextformat
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor

# Instrumenting requests
opentelemetry.ext.requests.RequestsInstrumentor().instrument()

# Tracer boilerplate
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
    SimpleExportSpanProcessor(CloudTraceSpanExporter()))

# Using the X-Cloud-Trace-Context header
set_global_httptextformat(CloudTraceFormatPropagator())

tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("client_span"):
    response = requests.get("http://localhost:5000/")
Beispiel #10
0
def create_app():
    """Flask application factory to create instances
    of the Userservice Flask App
    """
    app = Flask(__name__)

    # Set up tracing and export spans to Cloud Trace
    trace.set_tracer_provider(TracerProvider())
    cloud_trace_exporter = CloudTraceSpanExporter()
    trace.get_tracer_provider().add_span_processor(
        SimpleExportSpanProcessor(cloud_trace_exporter))

    set_global_httptextformat(CloudTraceFormatPropagator())

    # Add Flask auto-instrumentation for tracing
    FlaskInstrumentor().instrument_app(app)

    # Disabling unused-variable for lines with route decorated functions
    # as pylint thinks they are unused
    # pylint: disable=unused-variable
    @app.route('/version', methods=['GET'])
    def version():
        """
        Service version endpoint
        """
        return app.config['VERSION'], 200

    @app.route('/ready', methods=['GET'])
    def readiness():
        """
        Readiness probe
        """
        return 'ok', 200

    @app.route('/users', methods=['POST'])
    def create_user():
        """Create a user record.

        Fails if that username already exists.

        Generates a unique accountid.

        request fields:
        - username
        - password
        - password-repeat
        - firstname
        - lastname
        - birthday
        - timezone
        - address
        - state
        - zip
        - ssn
        """
        try:
            app.logger.debug('Sanitizing input.')
            req = {k: bleach.clean(v) for k, v in request.form.items()}
            __validate_new_user(req)
            # Check if user already exists
            if users_db.get_user(req['username']) is not None:
                raise NameError('user {} already exists'.format(
                    req['username']))

            # Create password hash with salt
            app.logger.debug("Creating password hash.")
            password = req['password']
            salt = bcrypt.gensalt()
            passhash = bcrypt.hashpw(password.encode('utf-8'), salt)

            accountid = users_db.generate_accountid()

            # Create user data to be added to the database
            user_data = {
                'accountid': accountid,
                'username': req['username'],
                'passhash': passhash,
                'firstname': req['firstname'],
                'lastname': req['lastname'],
                'birthday': req['birthday'],
                'timezone': req['timezone'],
                'address': req['address'],
                'state': req['state'],
                'zip': req['zip'],
                'ssn': req['ssn'],
            }
            # Add user_data to database
            app.logger.debug("Adding user to the database")
            users_db.add_user(user_data)
            app.logger.info("Successfully created user.")

        except UserWarning as warn:
            app.logger.error("Error creating new user: %s", str(warn))
            return str(warn), 400
        except NameError as err:
            app.logger.error("Error creating new user: %s", str(err))
            return str(err), 409
        except SQLAlchemyError as err:
            app.logger.error("Error creating new user: %s", str(err))
            return 'failed to create user', 500

        return jsonify({}), 201

    def __validate_new_user(req):
        app.logger.debug('validating create user request: %s', str(req))
        # Check if required fields are filled
        fields = (
            'username',
            'password',
            'password-repeat',
            'firstname',
            'lastname',
            'birthday',
            'timezone',
            'address',
            'state',
            'zip',
            'ssn',
        )
        if any(f not in req for f in fields):
            raise UserWarning('missing required field(s)')
        if any(not bool(req[f] or req[f].strip()) for f in fields):
            raise UserWarning('missing value for input field(s)')

        # Verify username contains only 2-15 alphanumeric or underscore characters
        if not re.match(r"\A[a-zA-Z0-9_]{2,15}\Z", req['username']):
            raise UserWarning(
                'username must contain 2-15 alphanumeric characters or underscores'
            )
        # Check if passwords match
        if not req['password'] == req['password-repeat']:
            raise UserWarning('passwords do not match')

    @app.route('/login', methods=['GET'])
    def login():
        """Login a user and return a JWT token

        Fails if username doesn't exist or password doesn't match hash

        token expiry time determined by environment variable

        request fields:
        - username
        - password
        """
        app.logger.debug('Sanitizing login input.')
        username = bleach.clean(request.args.get('username'))
        password = bleach.clean(request.args.get('password'))

        # Get user data
        try:
            app.logger.debug('Getting the user data.')
            user = users_db.get_user(username)
            if user is None:
                raise LookupError('user {} does not exist'.format(username))

            # Validate the password
            app.logger.debug('Validating the password.')
            if not bcrypt.checkpw(password.encode('utf-8'), user['passhash']):
                raise PermissionError('invalid login')

            full_name = '{} {}'.format(user['firstname'], user['lastname'])
            exp_time = datetime.utcnow() + timedelta(
                seconds=app.config['EXPIRY_SECONDS'])
            payload = {
                'user': username,
                'acct': user['accountid'],
                'name': full_name,
                'iat': datetime.utcnow(),
                'exp': exp_time,
            }
            app.logger.debug('Creating jwt token.')
            token = jwt.encode(payload,
                               app.config['PRIVATE_KEY'],
                               algorithm='RS256')
            app.logger.info('Login Successful.')
            return jsonify({'token': token.decode("utf-8")}), 200

        except LookupError as err:
            app.logger.error('Error logging in: %s', str(err))
            return str(err), 404
        except PermissionError as err:
            app.logger.error('Error logging in: %s', str(err))
            return str(err), 401
        except SQLAlchemyError as err:
            app.logger.error('Error logging in: %s', str(err))
            return 'failed to retrieve user information', 500

    @atexit.register
    def _shutdown():
        """Executed when web app is terminated."""
        app.logger.info("Stopping userservice.")

    # Set up logger
    app.logger.handlers = logging.getLogger('gunicorn.error').handlers
    app.logger.setLevel(logging.getLogger('gunicorn.error').level)
    app.logger.info('Starting userservice.')

    app.config['VERSION'] = os.environ.get('VERSION')
    app.config['EXPIRY_SECONDS'] = int(os.environ.get('TOKEN_EXPIRY_SECONDS'))
    app.config['PRIVATE_KEY'] = open(os.environ.get('PRIV_KEY_PATH'),
                                     'r').read()
    app.config['PUBLIC_KEY'] = open(os.environ.get('PUB_KEY_PATH'), 'r').read()

    # Configure database connection
    try:
        users_db = UserDb(os.environ.get("ACCOUNTS_DB_URI"), app.logger)
    except OperationalError:
        app.logger.critical("users_db database connection failed")
        sys.exit(1)
    return app
Beispiel #11
0
def configure_opentelemetry(
    access_token: str = _LS_ACCESS_TOKEN,
    span_endpoint: str = _OTEL_EXPORTER_OTLP_SPAN_ENDPOINT,
    metric_endpoint: str = _OTEL_EXPORTER_OTLP_METRIC_ENDPOINT,
    service_name: str = _LS_SERVICE_NAME,
    service_version: str = _LS_SERVICE_VERSION,
    propagator: list = _OTEL_PROPAGATORS,
    resource_labels: str = _OTEL_RESOURCE_LABELS,
    log_level: int = _OTEL_LOG_LEVEL,
    span_exporter_endpoint_insecure: bool = _OTEL_EXPORTER_OTLP_SPAN_INSECURE,
    metric_exporter_endpoint_insecure: bool = (
        _OTEL_EXPORTER_OTLP_METRIC_INSECURE
    ),
):
    """
    Configures OpenTelemetry with Lightstep environment variables

    This function works as a configuration layer that allows the Lightstep end
    user to use current environment variables seamlessly with OpenTelemetry. In
    this way, it is not needed to make any configuration changes to the
    environment before using OpenTelemetry. The configuration can be done via
    environment variables (prefixed with `LS`) or via arguments passed to this
    function. Each argument has a 1:1 correspondence with an environment
    variable, their description follows:

    Arguments:
        access_token (str): LS_ACCESS_TOKEN, the access token used to
            authenticate with the Lightstep satellite. This configuration value
            is mandatory.
        span_endpoint (str): OTEL_EXPORTER_OTLP_SPAN_ENDPOINT, the URL of the Lightstep
            satellite where the spans are to be exported. Defaults to
            `ingest.lightstep.com:443`.
        metric_endpoint (str): OTEL_EXPORTER_OTLP_METRIC_ENDPOINT, the URL of the metrics collector
            where the metrics are to be exported. Defaults to
            `ingest.lightstep.com:443/metrics`.
        service_name (str): LS_SERVICE_NAME, the name of the service that is
            used along with the access token to send spans to the Lighstep
            satellite. This configuration value is mandatory.
        service_version (str): LS_SERVICE_VERSION, the version of the service
            used to sernd spans to the Lightstep satellite. Defaults to
            `"unknown"`.
        propagator (list): OTEL_PROPAGATORS, a list of propagators to be used.
            Defaults to `["b3"]`.
        resource_labels (dict): OTEL_RESOURCE_LABELS, a dictionary of
            key value pairs used to instantiate the resouce of the tracer
            provider. Defaults to
            `{
                "service.name": _LS_SERVICE_NAME,
                "service.version": _LS_SERVICE_VERSION,
                "telemetry.sdk.language": "python",
                "telemetry.sdk.version": "0.9b0",
            }`
        log_level (int): OTEL_LOG_LEVEL, a boolean value that indicates the log
            level. Defaults to `logging.DEBUG`.
            information is to be printed. Defaults to `False`.
        span_exporter_endpoint_insecure (bool):
            OTEL_EXPORTER_OTLP_SPAN_INSECURE, a boolean value that indicates if
            an insecure channel is to be used to send spans to the satellite.
            Defaults to `False`.
        metric_exporter_endpoint_insecure (bool):
            OTEL_EXPORTER_OTLP_METRIC_INSECURE, a boolean value that indicates
            if an insecure channel is to be used to send spans to the
            satellite. Defaults to `False`.
    """

    basicConfig(level=log_level)

    _logger.debug("configuration")

    for key, value in {
        "access_token": access_token,
        "span_endpoint": span_endpoint,
        "metric_endpoint": metric_endpoint,
        "service_name": service_name,
        "service_version": service_version,
        "propagator": propagator,
        "resource_labels": resource_labels,
        "log_level": log_level,
        "span_exporter_endpoint_insecure": span_exporter_endpoint_insecure,
        "metric_exporter_endpoint_insecure": metric_exporter_endpoint_insecure,
    }.items():
        _logger.debug("%s: %s", key, value)

    if not _validate_service_name(service_name):

        message = (
            "Invalid configuration: service name missing. "
            "Set environment variable LS_SERVICE_NAME or call "
            "configure_opentelemetry with service_name defined"
        )
        _logger.error(message)
        raise InvalidConfigurationError(message)

    if access_token is None:
        if span_endpoint == _DEFAULT_OTEL_EXPORTER_OTLP_SPAN_ENDPOINT:
            message = (
                "Invalid configuration: token missing. "
                "Must be set to send data to {}. "
                "Set environment variable LS_ACCESS_TOKEN or call"
                "configure_opentelemetry with access_token defined"
            ).format(_OTEL_EXPORTER_OTLP_SPAN_ENDPOINT)
            _logger.error(message)
            raise InvalidConfigurationError(message)

    if not _validate_token(access_token):
        message = (
            "Invalid configuration: invalid token. "
            "Token must be a 32, 84 or 104 character long string."
        )
        _logger.error(message)
        raise InvalidConfigurationError(message)

    _logger.debug("configuring propagation")

    # FIXME use entry points (instead of a dictionary) to locate propagator
    # classes
    set_global_httptextformat(
        CompositeHTTPPropagator(
            [{"b3": B3Format}[propagator] for propagator in _OTEL_PROPAGATORS]
        )
    )

    _logger.debug("configuring tracing")

    metadata = None

    if _env.str("OPENTELEMETRY_PYTHON_TRACER_PROVIDER", None) is None:
        # FIXME now that new values can be set in the global configuration
        # object, check for this object having a tracer_provider attribute, if
        # not, set it to "sdk_tracer_provider" instead of using
        # set_tracer_provider, this in order to avoid having more than one
        # method of setting configuration.
        set_tracer_provider(TracerProvider())

    if access_token != "":
        metadata = (("lightstep-access-token", access_token),)

    credentials = ssl_channel_credentials()

    if span_exporter_endpoint_insecure:
        credentials = None

    # FIXME Do the same for metrics when the OTLPMetricsExporter is in
    # OpenTelemetry.
    get_tracer_provider().add_span_processor(
        BatchExportSpanProcessor(
            LightstepOTLPSpanExporter(
                endpoint=span_endpoint,
                credentials=credentials,
                metadata=metadata,
            )
        )
    )

    get_tracer_provider().resource = Resource(resource_labels)

    if log_level == DEBUG:
        get_tracer_provider().add_span_processor(
            BatchExportSpanProcessor(ConsoleSpanExporter())
        )
Beispiel #12
0
def create_app():
    """Flask application factory to create instances
    of the Connector Service Flask App
    """
    app = Flask(__name__)

    # Set up tracing and export spans to Cloud Trace
    trace.set_tracer_provider(TracerProvider())
    cloud_trace_exporter = CloudTraceSpanExporter()
    trace.get_tracer_provider().add_span_processor(
        SimpleExportSpanProcessor(cloud_trace_exporter))

    set_global_httptextformat(CloudTraceFormatPropagator())

    # Add Flask auto-instrumentation for tracing
    FlaskInstrumentor().instrument_app(app)

    # Disabling unused-variable for lines with route decorated functions
    # as pylint thinks they are unused
    # pylint: disable=unused-variable
    @app.route("/version", methods=["GET"])
    def version():
        """
        Service version endpoint
        """
        return app.config["VERSION"], 200

    @app.route("/ready", methods=["GET"])
    def ready():
        """Readiness probe."""
        return "ok", 200

    @app.route("/sync", methods=["POST"])
    def sync():
        """Sync PostgreSQL metadata with Google Data Catalog

        """
        auth_header = request.headers.get("Authorization")
        if auth_header:
            token = auth_header.split(" ")[-1]
        else:
            token = ""

        try:
            auth_payload = jwt.decode(token,
                                      key=app.config["PUBLIC_KEY"],
                                      algorithms="RS256")

            if auth_payload is None:
                raise PermissionError

            logging.info("Starting sync logic.")
            datacatalog_cli.PostgreSQL2DatacatalogCli().run(
                _get_connector_run_args())
            logging.info("Sync execution done.")
            return jsonify({}), 200

        except (PermissionError, jwt.exceptions.InvalidTokenError) as err:
            logging.error("Error executing sync: %s", str(err))
            return "authentication denied", 401
        except UserWarning as warn:
            logging.error("Error executing sync: %s", str(warn))
            return str(warn), 400
        except Exception as err:
            logging.error("Error executing sync: %s", str(err))
            return "failed to sync", 500

    @atexit.register
    def _shutdown():
        """Executed when web app is terminated."""
        logging.info("Stopping contacts service.")

    def _get_connector_run_args():
        return [
            '--datacatalog-project-id',
            os.environ.get('DATACATALOG_PROJECT_ID'),
            '--datacatalog-location-id',
            os.environ.get('DATACATALOG_LOCATION_ID'), '--postgresql-host',
            os.environ.get('POSTGRESQL_SERVER'), '--postgresql-user',
            os.environ.get('POSTGRES_USER'), '--postgresql-pas',
            os.environ.get('POSTGRES_PASSWORD'), '--postgresql-database',
            os.environ.get('POSTGRES_DB')
        ]

    logging_client = gcp_logging.Client()
    logging_client.setup_logging(log_level=logging.INFO)
    root_logger = logging.getLogger()
    # use the GCP handler ONLY in order to prevent logs from getting written to STDERR
    root_logger.handlers = [
        handler for handler in root_logger.handlers
        if isinstance(handler, (CloudLoggingHandler, ContainerEngineHandler,
                                AppEngineHandler))
    ]

    logging.info("Service PostgreSQL connector created.")

    # setup global variables
    app.config["VERSION"] = os.environ.get("VERSION")
    app.config["PUBLIC_KEY"] = open(os.environ.get("PUB_KEY_PATH"), "r").read()

    return app
trace.set_tracer_provider(TracerProvider())

trace.get_tracer_provider().add_span_processor(
    DatadogExportSpanProcessor(
        DatadogSpanExporter(agent_url="http://*****:*****@app.route("/server_request")
def server_request():
    param = request.args.get("param")
    with tracer.start_as_current_span("server-inner"):
        if param == "error":
            raise ValueError("forced server error")
        return "served: {}".format(param)
Beispiel #14
0
def create_app():
    """Flask application factory to create instances
    of the Contact Service Flask App
    """
    app = Flask(__name__)

    # Set up tracing and export spans to Cloud Trace
    trace.set_tracer_provider(TracerProvider())
    cloud_trace_exporter = CloudTraceSpanExporter()
    trace.get_tracer_provider().add_span_processor(
        SimpleExportSpanProcessor(cloud_trace_exporter))

    set_global_httptextformat(CloudTraceFormatPropagator())

    # Add Flask auto-instrumentation for tracing
    FlaskInstrumentor().instrument_app(app)

    # Disabling unused-variable for lines with route decorated functions
    # as pylint thinks they are unused
    # pylint: disable=unused-variable
    @app.route("/version", methods=["GET"])
    def version():
        """
        Service version endpoint
        """
        return app.config["VERSION"], 200

    @app.route("/ready", methods=["GET"])
    def ready():
        """Readiness probe."""
        return "ok", 200

    @app.route("/contacts/<username>", methods=["GET"])
    def get_contacts(username):
        """Retrieve the contacts list for the authenticated user.
        This list is used for populating Payment and Deposit fields.

        Return: a list of contacts
        """
        auth_header = request.headers.get("Authorization")
        if auth_header:
            token = auth_header.split(" ")[-1]
        else:
            token = ""
        try:
            auth_payload = jwt.decode(token,
                                      key=app.config["PUBLIC_KEY"],
                                      algorithms="RS256")
            if username != auth_payload["user"]:
                raise PermissionError

            contacts_list = contacts_db.get_contacts(username)
            app.logger.debug("Succesfully retrieved contacts.")
            return jsonify(contacts_list), 200
        except (PermissionError, jwt.exceptions.InvalidTokenError) as err:
            app.logger.error("Error retrieving contacts list: %s", str(err))
            return "authentication denied", 401
        except SQLAlchemyError as err:
            app.logger.error("Error retrieving contacts list: %s", str(err))
            return "failed to retrieve contacts list", 500

    @app.route("/contacts/<username>", methods=["POST"])
    def add_contact(username):
        """Add a new favorite account to user's contacts list

        Fails if account or routing number are invalid
        or if label is not alphanumeric

        request fields:
        - account_num
        - routing_num
        - label
        - is_external
        """
        auth_header = request.headers.get("Authorization")
        if auth_header:
            token = auth_header.split(" ")[-1]
        else:
            token = ""
        try:
            auth_payload = jwt.decode(token,
                                      key=app.config["PUBLIC_KEY"],
                                      algorithms="RS256")
            if username != auth_payload["user"]:
                raise PermissionError
            req = {
                k: (bleach.clean(v) if isinstance(v, str) else v)
                for k, v in request.get_json().items()
            }
            _validate_new_contact(req)

            _check_contact_allowed(username, auth_payload["acct"], req)
            # Create contact data to be added to the database.
            contact_data = {
                "username": username,
                "label": req["label"],
                "account_num": req["account_num"],
                "routing_num": req["routing_num"],
                "is_external": req["is_external"],
            }
            # Add contact_data to database
            app.logger.debug("Adding new contact to the database.")
            contacts_db.add_contact(contact_data)
            app.logger.info("Successfully added new contact.")
            return jsonify({}), 201

        except (PermissionError, jwt.exceptions.InvalidTokenError) as err:
            app.logger.error("Error adding contact: %s", str(err))
            return "authentication denied", 401
        except UserWarning as warn:
            app.logger.error("Error adding contact: %s", str(warn))
            return str(warn), 400
        except ValueError as err:
            app.logger.error("Error adding contact: %s", str(err))
            return str(err), 409
        except SQLAlchemyError as err:
            app.logger.error("Error adding contact: %s", str(err))
            return "failed to add contact", 500

    def _validate_new_contact(req):
        """Check that this new contact request has valid fields"""
        app.logger.debug("validating add contact request: %s", str(req))
        # Check if required fields are filled
        fields = ("label", "account_num", "routing_num", "is_external")
        if any(f not in req for f in fields):
            raise UserWarning("missing required field(s)")

        # Validate account number (must be 10 digits)
        if req["account_num"] is None or not re.match(r"\A[0-9]{10}\Z",
                                                      req["account_num"]):
            raise UserWarning("invalid account number")
        # Validate routing number (must be 9 digits)
        if req["routing_num"] is None or not re.match(r"\A[0-9]{9}\Z",
                                                      req["routing_num"]):
            raise UserWarning("invalid routing number")
        # Only allow external accounts to deposit
        if (req["is_external"]
                and req["routing_num"] == app.config["LOCAL_ROUTING"]):
            raise UserWarning("invalid routing number")
        # Validate label
        # Must be >0 and <=30 chars, alphanumeric and spaces, can't start with space
        if req["label"] is None or not re.match(
                r"^[0-9a-zA-Z][0-9a-zA-Z ]{0,29}$", req["label"]):
            raise UserWarning("invalid account label")

    def _check_contact_allowed(username, accountid, req):
        """Check that this contact is allowed to be created"""
        app.logger.debug(
            "checking that this contact is allowed to be created: %s",
            str(req))
        # Don't allow self reference
        if (req["account_num"] == accountid
                and req["routing_num"] == app.config["LOCAL_ROUTING"]):
            raise ValueError("may not add yourself to contacts")

        # Don't allow identical contacts
        for contact in contacts_db.get_contacts(username):
            if (contact["account_num"] == req["account_num"]
                    and contact["routing_num"] == req["routing_num"]):
                raise ValueError("account already exists as a contact")

            if contact["label"] == req["label"]:
                raise ValueError("contact already exists with that label")

    @atexit.register
    def _shutdown():
        """Executed when web app is terminated."""
        app.logger.info("Stopping contacts service.")

    # set up logger
    app.logger.handlers = logging.getLogger("gunicorn.error").handlers
    app.logger.setLevel(logging.getLogger("gunicorn.error").level)
    app.logger.info("Starting contacts service.")

    # setup global variables
    app.config["VERSION"] = os.environ.get("VERSION")
    app.config["LOCAL_ROUTING"] = os.environ.get("LOCAL_ROUTING_NUM")
    app.config["PUBLIC_KEY"] = open(os.environ.get("PUB_KEY_PATH"), "r").read()

    # Configure database connection
    try:
        contacts_db = ContactsDb(os.environ.get("ACCOUNTS_DB_URI"), app.logger)
    except OperationalError:
        app.logger.critical("database connection failed")
        sys.exit(1)
    return app