# instrumentation_key="uuid of the instrumentation key (see your Azure Monitor account)" # ) # Regular open telemetry usage from here, see https://github.com/open-telemetry/opentelemetry-python # for details from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleSpanProcessor # Simple console exporter exporter = ConsoleSpanExporter() trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(exporter)) # Example with Eventgrid SDKs import os from azure.core.messaging import CloudEvent from azure.eventgrid import EventGridPublisherClient from azure.core.credentials import AzureKeyCredential hostname = os.environ['CLOUD_TOPIC_HOSTNAME'] key = AzureKeyCredential(os.environ['CLOUD_ACCESS_KEY']) cloud_event = CloudEvent(source='demo', type='sdk.demo', data={'test': 'hello'}, extensions={'test': 'maybe'}) with tracer.start_as_current_span(name="MyApplication"): client = EventGridPublisherClient(hostname, key)
from flask import Flask, request import mysql.connector from opentelemetry.instrumentation.mysql import MySQLInstrumentor trace.set_tracer_provider(TracerProvider(sampler=sampling.ALWAYS_ON)) #trace.get_tracer_provider().add_span_processor( # SimpleExportSpanProcessor(ConsoleSpanExporter()) #) jaeger_exporter = jaeger.JaegerSpanExporter( service_name="service2", agent_host_name="jaeger", agent_port=6831, ) trace.get_tracer_provider().add_span_processor( BatchExportSpanProcessor(jaeger_exporter)) tracer = trace.get_tracer(__name__) app = Flask(__name__) FlaskInstrumentor().instrument_app(app) MySQLInstrumentor().instrument() @app.route('/') def index(): with tracer.start_as_current_span("service2-db"): # TODO - Move this to app initialization rather than per request cnx = mysql.connector.connect(user='******', password='******',
metric_exporter = CollectorMetricsExporter( # optional: # endpoint="myCollectorUrl:55678", # service_name="test_service", # host_name="machine/container name", ) # Meter is responsible for creating and recording metrics metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) # controller collects metrics created from meter and exports it via the # exporter every interval controller = PushController(meter, metric_exporter, 5) # Configure the tracer to use the collector exporter tracer = trace.get_tracer_provider().get_tracer(__name__) with tracer.start_as_current_span("foo"): print("Hello world!") requests_counter = meter.create_metric( name="requests", description="number of requests", unit="1", value_type=int, metric_type=Counter, label_keys=("environment", ), ) # Labels are used to identify key-values that are associated with a specific # metric that you want to record. These are useful for pre-aggregation and can # be used to store custom dimensions pertaining to a metric
from opentelemetry import trace from opentelemetry.exporter.datadog import ( DatadogExportSpanProcessor, DatadogSpanExporter, ) from opentelemetry.exporter.datadog.propagator import DatadogFormat from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.sdk.trace import TracerProvider app = Flask(__name__) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( DatadogExportSpanProcessor( DatadogSpanExporter(agent_url="http://*****:*****@app.route("/server_request")
# 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.propagate import set_global_textmap from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.tools.cloud_trace_propagator import ( CloudTraceFormatPropagator, ) # Instrumenting requests opentelemetry.ext.requests.RequestsInstrumentor().instrument() # Tracer boilerplate trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( SimpleSpanProcessor(CloudTraceSpanExporter()) ) # Using the X-Cloud-Trace-Context header set_global_textmap(CloudTraceFormatPropagator()) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("client_span"): response = requests.get("http://localhost:5000/")
def create_trace_config( url_filter: typing.Optional[typing.Callable[[str], str]] = None, span_name: typing.Optional[typing.Union[typing.Callable[ [aiohttp.TraceRequestStartParams], str], str]] = None, ) -> aiohttp.TraceConfig: """Create an aiohttp-compatible trace configuration. One span is created for the entire HTTP request, including initial TCP/TLS setup if the connection doesn't exist. By default the span name is set to the HTTP request method. Example usage: .. code:: python import aiohttp from opentelemetry.ext.aiohttp_client import create_trace_config async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session: async with session.get(url) as response: await response.text() :param url_filter: A callback to process the requested URL prior to adding it as a span attribute. This can be useful to remove sensitive data such as API keys or user personal information. :param str span_name: Override the default span name. :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. :rtype: :py:class:`aiohttp.TraceConfig` """ # `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams` # which doesn't exist in the aiottp intersphinx inventory. # Explicitly specify the type for the `span_name` param and rtype to work # around this issue. tracer = trace.get_tracer_provider().get_tracer(__name__, __version__) def _end_trace(trace_config_ctx: types.SimpleNamespace): context_api.detach(trace_config_ctx.token) trace_config_ctx.span.end() async def on_request_start( unused_session: aiohttp.ClientSession, trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestStartParams, ): http_method = params.method.upper() if trace_config_ctx.span_name is None: request_span_name = http_method elif callable(trace_config_ctx.span_name): request_span_name = str(trace_config_ctx.span_name(params)) else: request_span_name = str(trace_config_ctx.span_name) trace_config_ctx.span = trace_config_ctx.tracer.start_span( request_span_name, kind=SpanKind.CLIENT, attributes={ "component": "http", "http.method": http_method, "http.url": trace_config_ctx.url_filter(params.url) if callable(trace_config_ctx.url_filter) else str(params.url), }, ) trace_config_ctx.token = context_api.attach( trace.set_span_in_context(trace_config_ctx.span)) propagators.inject(type(params.headers).__setitem__, params.headers) async def on_request_end( unused_session: aiohttp.ClientSession, trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestEndParams, ): trace_config_ctx.span.set_status( Status(http_status_to_canonical_code(int(params.response.status)))) trace_config_ctx.span.set_attribute("http.status_code", params.response.status) trace_config_ctx.span.set_attribute("http.status_text", params.response.reason) _end_trace(trace_config_ctx) async def on_request_exception( unused_session: aiohttp.ClientSession, trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestExceptionParams, ): if isinstance( params.exception, (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), ): status = StatusCanonicalCode.DEADLINE_EXCEEDED # Assume any getaddrinfo error is a DNS failure. elif isinstance(params.exception, aiohttp.ClientConnectorError) and isinstance( params.exception.os_error, socket.gaierror): # DNS resolution failed status = StatusCanonicalCode.UNKNOWN else: status = StatusCanonicalCode.UNAVAILABLE trace_config_ctx.span.set_status(Status(status)) _end_trace(trace_config_ctx) def _trace_config_ctx_factory(**kwargs): kwargs.setdefault("trace_request_ctx", {}) return types.SimpleNamespace(span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs) trace_config = aiohttp.TraceConfig( trace_config_ctx_factory=_trace_config_ctx_factory) trace_config.on_request_start.append(on_request_start) trace_config.on_request_end.append(on_request_end) trace_config.on_request_exception.append(on_request_exception) return trace_config
import requests from opentelemetry import trace from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter # Set global TracerProvider before instrumenting trace.set_tracer_provider( TracerProvider(resource=Resource.create({SERVICE_NAME: "http-sample-A"}))) # Enable tracing for requests library RequestsInstrumentor().instrument() trace_exporter = AzureMonitorTraceExporter() trace.get_tracer_provider().add_span_processor( BatchSpanProcessor(trace_exporter)) tracer = trace.get_tracer(__name__) # Call django app response = requests.get("http://127.0.0.1:8000/projects", timeout=5)
from opentelemetry import trace from opentelemetry.exporter.jaeger import JaegerSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchExportSpanProcessor # SpanExporter receives the spans and send them to the target location. exporter = JaegerSpanExporter( service_name="auto-instrument-example", agent_host_name="jaeger", agent_port=6831, ) span_processor = BatchExportSpanProcessor(exporter) trace.get_tracer_provider().add_span_processor(span_processor) print('Tracing Initialized') import recommendation_server as r r.main()
def setUp(self): """Create an OpenTelemetry tracer and a shim before every test case.""" trace.set_tracer_provider(TracerProvider()) self.shim = opentracingshim.create_tracer(trace.get_tracer_provider())
# pip install opentelemetry-api # pip install opentelemetry-sdk # pip install opentelemetry-instrumentation-flask # pip install opentelemetry-instrumentation-requests from opentelemetry import trace from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( BatchSpanProcessor(ConsoleSpanExporter())) app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() tracer = trace.get_tracer(__name__) @app.route("/") def hello(): with tracer.start_as_current_span("example-request"): requests.get("https://www.google.com") return "hello"
#!/usr/bin/env python3 # client.py import requests from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import BatchExportSpanProcessor exporter = ConsoleSpanExporter() trace.set_tracer_provider(TracerProvider()) span_processor = BatchExportSpanProcessor(exporter) trace.get_tracer_provider().add_span_processor(span_processor) http_requests.enable(trace.get_tracer_provider()) response = requests.get(url="http://127.0.0.1:5000/")
def _get_tracer(engine, tracer_provider=None): if tracer_provider is None: tracer_provider = trace.get_tracer_provider() return tracer_provider.get_tracer(_normalize_vendor(engine.name), __version__)
from flask import Flask, abort, jsonify, make_response, redirect, \ render_template, request, url_for from opentelemetry import trace from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter from opentelemetry.exporter.cloud_trace.cloud_trace_propagator import CloudTraceFormatPropagator from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.ext.jinja2 import Jinja2Instrumentor from opentelemetry.ext.requests import RequestsInstrumentor from opentelemetry.propagators import set_global_httptextformat from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor # 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()) APP = Flask(__name__) # Add tracing auto-instrumentation for Flask, jinja and requests FlaskInstrumentor().instrument_app(APP) RequestsInstrumentor().instrument() Jinja2Instrumentor().instrument() @APP.route('/version', methods=['GET']) def version(): """ Service version endpoint
# Copyright 2021 Google # # Licensed 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. from opentelemetry import trace from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor trace.set_tracer_provider(TracerProvider()) cloud_trace_exporter = CloudTraceSpanExporter() trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(cloud_trace_exporter)) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("foo"): print("Hello world!")
trace.set_tracer_provider( TracerProvider(resource=Resource.create({ "service.name": "payment", "service.instance.id": str(id(app)), "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.language": "python", "telemetry.sdk.version": pkg_resources.get_distribution("opentelemetry-sdk").version, "host.hostname": socket.gethostname(), }))) tracerProvider = trace.get_tracer_provider() tracer = tracerProvider.get_tracer(__name__) tracerProvider.add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter())) otlp_exporter = OTLPSpanExporter(endpoint="{}:55680".format(OTLP)) tracerProvider.add_span_processor(SimpleExportSpanProcessor(otlp_exporter)) FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument(tracer_provider=tracerProvider) retry_strategy = Retry(total=2, status_forcelist=[401, 401.1, 429, 503], method_whitelist=[ "HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"
) try: # Relative imports should work in the context of the package, e.g.: # `python -m opentelemetry_example_app.grpc.route_guide_client`. from .gen import route_guide_pb2, route_guide_pb2_grpc from . import route_guide_resources except ImportError: # This will fail when running the file as a script, e.g.: # `./route_guide_client.py` # fall back to importing from the same directory in this case. from gen import route_guide_pb2, route_guide_pb2_grpc import route_guide_resources trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter())) def make_route_note(message, latitude, longitude): return route_guide_pb2.RouteNote( message=message, location=route_guide_pb2.Point(latitude=latitude, longitude=longitude), ) def guide_get_one_feature(stub, point): feature = stub.GetFeature(point) if not feature.location: print("Server returned incomplete feature") return
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
def create_app(): """Flask application factory to create instances of the Frontend Flask App """ app = Flask(__name__) # 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 os.environ.get('VERSION'), 200 @app.route('/ready', methods=['GET']) def readiness(): """ Readiness probe """ return 'ok', 200 @app.route('/whereami', methods=['GET']) def whereami(): """ Returns the cluster name + zone name where this Pod is running. """ return "Cluster: " + cluster_name + ", Pod: " + pod_name + ", Zone: " + pod_zone, 200 @app.route("/") def root(): """ Renders home page or login page, depending on authentication status. """ token = request.cookies.get(app.config['TOKEN_NAME']) if not verify_token(token): return login_page() return home() @app.route("/home") def home(): """ Renders home page. Redirects to /login if token is not valid """ token = request.cookies.get(app.config['TOKEN_NAME']) if not verify_token(token): # user isn't authenticated app.logger.debug( 'User isn\'t authenticated. Redirecting to login page.') return redirect( url_for('login_page', _external=True, _scheme=app.config['SCHEME'])) token_data = jwt.decode(token, verify=False) display_name = token_data['name'] username = token_data['user'] account_id = token_data['acct'] hed = {'Authorization': 'Bearer ' + token} # get balance balance = None try: url = '{}/{}'.format(app.config["BALANCES_URI"], account_id) app.logger.debug('Getting account balance.') response = requests.get(url=url, headers=hed, timeout=app.config['BACKEND_TIMEOUT']) if response: balance = response.json() except (requests.exceptions.RequestException, ValueError) as err: app.logger.error('Error getting account balance: %s', str(err)) # get history transaction_list = None try: url = '{}/{}'.format(app.config["HISTORY_URI"], account_id) app.logger.debug('Getting transaction history.') response = requests.get(url=url, headers=hed, timeout=app.config['BACKEND_TIMEOUT']) if response: transaction_list = response.json() except (requests.exceptions.RequestException, ValueError) as err: app.logger.error('Error getting transaction history: %s', str(err)) # get contacts contacts = [] try: url = '{}/{}'.format(app.config["CONTACTS_URI"], username) app.logger.debug('Getting contacts.') response = requests.get(url=url, headers=hed, timeout=app.config['BACKEND_TIMEOUT']) if response: contacts = response.json() except (requests.exceptions.RequestException, ValueError) as err: app.logger.error('Error getting contacts: %s', str(err)) _populate_contact_labels(account_id, transaction_list, contacts) return render_template('index.html', cluster_name=cluster_name, pod_name=pod_name, pod_zone=pod_zone, cymbal_logo=os.getenv('CYMBAL_LOGO', 'false'), history=transaction_list, balance=balance, name=display_name, account_id=account_id, contacts=contacts, message=request.args.get('msg', None), bank_name=os.getenv('BANK_NAME', 'Bank of Anthos')) def _populate_contact_labels(account_id, transactions, contacts): """ Populate contact labels for the passed transactions. Side effect: Take each transaction and set the 'accountLabel' field with the label of the contact each transaction was associated with. If there was no associated contact, set 'accountLabel' to None. If any parameter is None, nothing happens. Params: account_id - the account id for the user owning the transaction list transactions - a list of transactions as key/value dicts [{transaction1}, {transaction2}, ...] contacts - a list of contacts as key/value dicts [{contact1}, {contact2}, ...] """ app.logger.debug('Populating contact labels.') if account_id is None or transactions is None or contacts is None: return # Map contact accounts to their labels. If no label found, default to None. contact_map = {c['account_num']: c.get('label') for c in contacts} # Populate the 'accountLabel' field. If no match found, default to None. for trans in transactions: if trans['toAccountNum'] == account_id: trans['accountLabel'] = contact_map.get( trans['fromAccountNum']) elif trans['fromAccountNum'] == account_id: trans['accountLabel'] = contact_map.get(trans['toAccountNum']) @app.route('/payment', methods=['POST']) def payment(): """ Submits payment request to ledgerwriter service Fails if: - token is not valid - basic validation checks fail - response code from ledgerwriter is not 201 """ token = request.cookies.get(app.config['TOKEN_NAME']) if not verify_token(token): # user isn't authenticated app.logger.error( 'Error submitting payment: user is not authenticated.') return abort(401) try: account_id = jwt.decode(token, verify=False)['acct'] recipient = request.form['account_num'] if recipient == 'add': recipient = request.form['contact_account_num'] label = request.form.get('contact_label', None) if label: # new contact. Add to contacts list _add_contact(label, recipient, app.config['LOCAL_ROUTING'], False) transaction_data = { "fromAccountNum": account_id, "fromRoutingNum": app.config['LOCAL_ROUTING'], "toAccountNum": recipient, "toRoutingNum": app.config['LOCAL_ROUTING'], "amount": int(Decimal(request.form['amount']) * 100), "uuid": request.form['uuid'] } _submit_transaction(transaction_data) app.logger.info('Payment initiated successfully.') return redirect( url_for('home', msg='Payment successful', _external=True, _scheme=app.config['SCHEME'])) except requests.exceptions.RequestException as err: app.logger.error('Error submitting payment: %s', str(err)) except UserWarning as warn: app.logger.error('Error submitting payment: %s', str(warn)) msg = 'Payment failed: {}'.format(str(warn)) return redirect( url_for('home', msg=msg, _external=True, _scheme=app.config['SCHEME'])) return redirect( url_for('home', msg='Payment failed', _external=True, _scheme=app.config['SCHEME'])) @app.route('/deposit', methods=['POST']) def deposit(): """ Submits deposit request to ledgerwriter service Fails if: - token is not valid - routing number == local routing number - response code from ledgerwriter is not 201 """ token = request.cookies.get(app.config['TOKEN_NAME']) if not verify_token(token): # user isn't authenticated app.logger.error( 'Error submitting deposit: user is not authenticated.') return abort(401) try: # get account id from token account_id = jwt.decode(token, verify=False)['acct'] if request.form['account'] == 'add': external_account_num = request.form['external_account_num'] external_routing_num = request.form['external_routing_num'] if external_routing_num == app.config['LOCAL_ROUTING']: raise UserWarning("invalid routing number") external_label = request.form.get('external_label', None) if external_label: # new contact. Add to contacts list _add_contact(external_label, external_account_num, external_routing_num, True) else: account_details = json.loads(request.form['account']) external_account_num = account_details['account_num'] external_routing_num = account_details['routing_num'] transaction_data = { "fromAccountNum": external_account_num, "fromRoutingNum": external_routing_num, "toAccountNum": account_id, "toRoutingNum": app.config['LOCAL_ROUTING'], "amount": int(Decimal(request.form['amount']) * 100), "uuid": request.form['uuid'] } _submit_transaction(transaction_data) app.logger.info('Deposit submitted successfully.') return redirect( url_for('home', msg='Deposit successful', _external=True, _scheme=app.config['SCHEME'])) except requests.exceptions.RequestException as err: app.logger.error('Error submitting deposit: %s', str(err)) except UserWarning as warn: app.logger.error('Error submitting deposit: %s', str(warn)) msg = 'Deposit failed: {}'.format(str(warn)) return redirect( url_for('home', msg=msg, _external=True, _scheme=app.config['SCHEME'])) return redirect( url_for('home', msg='Deposit failed', _external=True, _scheme=app.config['SCHEME'])) def _submit_transaction(transaction_data): app.logger.debug('Submitting transaction.') token = request.cookies.get(app.config['TOKEN_NAME']) hed = { 'Authorization': 'Bearer ' + token, 'content-type': 'application/json' } resp = requests.post(url=app.config["TRANSACTIONS_URI"], data=jsonify(transaction_data).data, headers=hed, timeout=app.config['BACKEND_TIMEOUT']) try: resp.raise_for_status() # Raise on HTTP Status code 4XX or 5XX except requests.exceptions.HTTPError as http_request_err: raise UserWarning(resp.text) from http_request_err def _add_contact(label, acct_num, routing_num, is_external_acct=False): """ Submits a new contact to the contact service. Raise: UserWarning if the response status is 4xx or 5xx. """ app.logger.debug('Adding new contact.') token = request.cookies.get(app.config['TOKEN_NAME']) hed = { 'Authorization': 'Bearer ' + token, 'content-type': 'application/json' } contact_data = { 'label': label, 'account_num': acct_num, 'routing_num': routing_num, 'is_external': is_external_acct } token_data = jwt.decode(token, verify=False) url = '{}/{}'.format(app.config["CONTACTS_URI"], token_data['user']) resp = requests.post(url=url, data=jsonify(contact_data).data, headers=hed, timeout=app.config['BACKEND_TIMEOUT']) try: resp.raise_for_status() # Raise on HTTP Status code 4XX or 5XX except requests.exceptions.HTTPError as http_request_err: raise UserWarning(resp.text) from http_request_err @app.route("/login", methods=['GET']) def login_page(): """ Renders login page. Redirects to /home if user already has a valid token """ token = request.cookies.get(app.config['TOKEN_NAME']) if verify_token(token): # already authenticated app.logger.debug( 'User already authenticated. Redirecting to /home') return redirect( url_for('home', _external=True, _scheme=app.config['SCHEME'])) return render_template('login.html', cymbal_logo=os.getenv('CYMBAL_LOGO', 'false'), cluster_name=cluster_name, pod_name=pod_name, pod_zone=pod_zone, message=request.args.get('msg', None), default_user=os.getenv('DEFAULT_USERNAME', ''), default_password=os.getenv( 'DEFAULT_PASSWORD', ''), bank_name=os.getenv('BANK_NAME', 'Bank of Anthos')) @app.route('/login', methods=['POST']) def login(): """ Submits login request to userservice and saves resulting token Fails if userservice does not accept input username and password """ return _login_helper(request.form['username'], request.form['password']) def _login_helper(username, password): try: app.logger.debug('Logging in.') req = requests.get(url=app.config["LOGIN_URI"], params={ 'username': username, 'password': password }) req.raise_for_status() # Raise on HTTP Status code 4XX or 5XX # login success token = req.json()['token'].encode('utf-8') claims = jwt.decode(token, verify=False) max_age = claims['exp'] - claims['iat'] resp = make_response( redirect( url_for('home', _external=True, _scheme=app.config['SCHEME']))) resp.set_cookie(app.config['TOKEN_NAME'], token, max_age=max_age) app.logger.info('Successfully logged in.') return resp except (RequestException, HTTPError) as err: app.logger.error('Error logging in: %s', str(err)) return redirect( url_for('login', msg='Login Failed', _external=True, _scheme=app.config['SCHEME'])) @app.route("/signup", methods=['GET']) def signup_page(): """ Renders signup page. Redirects to /login if token is not valid """ token = request.cookies.get(app.config['TOKEN_NAME']) if verify_token(token): # already authenticated app.logger.debug( 'User already authenticated. Redirecting to /home') return redirect( url_for('home', _external=True, _scheme=app.config['SCHEME'])) return render_template('signup.html', cymbal_logo=os.getenv('CYMBAL_LOGO', 'false'), cluster_name=cluster_name, pod_name=pod_name, pod_zone=pod_zone, bank_name=os.getenv('BANK_NAME', 'Bank of Anthos')) @app.route("/signup", methods=['POST']) def signup(): """ Submits signup request to userservice Fails if userservice does not accept input form data """ try: # create user app.logger.debug('Creating new user.') resp = requests.post(url=app.config["USERSERVICE_URI"], data=request.form, timeout=app.config['BACKEND_TIMEOUT']) if resp.status_code == 201: # user created. Attempt login app.logger.info('New user created.') return _login_helper(request.form['username'], request.form['password']) except requests.exceptions.RequestException as err: app.logger.error('Error creating new user: %s', str(err)) return redirect( url_for('login', msg='Error: Account creation failed', _external=True, _scheme=app.config['SCHEME'])) @app.route('/logout', methods=['POST']) def logout(): """ Logs out user by deleting token cookie and redirecting to login page """ app.logger.info('Logging out.') resp = make_response( redirect( url_for('login_page', _external=True, _scheme=app.config['SCHEME']))) resp.delete_cookie(app.config['TOKEN_NAME']) return resp def verify_token(token): """ Validates token using userservice public key """ app.logger.debug('Verifying token.') if token is None: return False try: jwt.decode(token, key=app.config['PUBLIC_KEY'], algorithms='RS256', verify=True) app.logger.debug('Token verified.') return True except jwt.exceptions.InvalidTokenError as err: app.logger.error('Error validating token: %s', str(err)) return False # register html template formatters def format_timestamp_day(timestamp): """ Format the input timestamp day in a human readable way """ # TODO: time zones? date = datetime.datetime.strptime(timestamp, app.config['TIMESTAMP_FORMAT']) return date.strftime('%d') def format_timestamp_month(timestamp): """ Format the input timestamp month in a human readable way """ # TODO: time zones? date = datetime.datetime.strptime(timestamp, app.config['TIMESTAMP_FORMAT']) return date.strftime('%b') def format_currency(int_amount): """ Format the input currency in a human readable way """ if int_amount is None: return '$---' amount_str = '${:0,.2f}'.format(abs(Decimal(int_amount) / 100)) if int_amount < 0: amount_str = '-' + amount_str return amount_str # set up global variables app.config["TRANSACTIONS_URI"] = 'http://{}/transactions'.format( os.environ.get('TRANSACTIONS_API_ADDR')) app.config["USERSERVICE_URI"] = 'http://{}/users'.format( os.environ.get('USERSERVICE_API_ADDR')) app.config["BALANCES_URI"] = 'http://{}/balances'.format( os.environ.get('BALANCES_API_ADDR')) app.config["HISTORY_URI"] = 'http://{}/transactions'.format( os.environ.get('HISTORY_API_ADDR')) app.config["LOGIN_URI"] = 'http://{}/login'.format( os.environ.get('USERSERVICE_API_ADDR')) app.config["CONTACTS_URI"] = 'http://{}/contacts'.format( os.environ.get('CONTACTS_API_ADDR')) app.config['PUBLIC_KEY'] = open(os.environ.get('PUB_KEY_PATH'), 'r').read() app.config['LOCAL_ROUTING'] = os.getenv('LOCAL_ROUTING_NUM') app.config[ 'BACKEND_TIMEOUT'] = 4 # timeout in seconds for calls to the backend app.config['TOKEN_NAME'] = 'token' app.config['TIMESTAMP_FORMAT'] = '%Y-%m-%dT%H:%M:%S.%f%z' app.config['SCHEME'] = os.environ.get('SCHEME', 'http') # where am I? metadata_url = 'http://metadata.google.internal/computeMetadata/v1/' metadata_headers = {'Metadata-Flavor': 'Google'} # get GKE cluster name cluster_name = "unknown" try: req = requests.get(metadata_url + 'instance/attributes/cluster-name', headers=metadata_headers) if req.ok: cluster_name = str(req.text) except (RequestException, HTTPError) as err: app.logger.warning("Unable to capture GKE cluster name.") # get GKE pod name pod_name = "unknown" pod_name = socket.gethostname() # get GKE node zone pod_zone = "unknown" try: req = requests.get(metadata_url + 'instance/zone', headers=metadata_headers) if req.ok: pod_zone = str(req.text.split("/")[3]) except (RequestException, HTTPError) as err: app.logger.warning("Unable to capture GKE node zone.") # register formater functions app.jinja_env.globals.update(format_currency=format_currency) app.jinja_env.globals.update(format_timestamp_month=format_timestamp_month) app.jinja_env.globals.update(format_timestamp_day=format_timestamp_day) # Set up logging app.logger.handlers = logging.getLogger('gunicorn.error').handlers app.logger.setLevel(logging.getLogger('gunicorn.error').level) app.logger.info('Starting frontend service.') # Set up tracing and export spans to Cloud Trace. if os.environ['ENABLE_TRACING'] == "true": app.logger.info("✅ Tracing enabled.") trace.set_tracer_provider(TracerProvider()) cloud_trace_exporter = CloudTraceSpanExporter() trace.get_tracer_provider().add_span_processor( BatchExportSpanProcessor(cloud_trace_exporter)) set_global_textmap(CloudTraceFormatPropagator()) # Add tracing auto-instrumentation for Flask, jinja and requests FlaskInstrumentor().instrument_app(app) RequestsInstrumentor().instrument() Jinja2Instrumentor().instrument() else: app.logger.info("🚫 Tracing disabled.") return app
def extract( self, carrier: CarrierT, context: typing.Optional[Context] = None, getter: Getter = default_getter, ) -> Context: trace_id = format_trace_id(trace.INVALID_TRACE_ID) span_id = format_span_id(trace.INVALID_SPAN_ID) sampled = "0" flags = None single_header = _extract_first_element( getter.get(carrier, self.SINGLE_HEADER_KEY)) if single_header: # The b3 spec calls for the sampling state to be # "deferred", which is unspecified. This concept does not # translate to SpanContext, so we set it as recorded. sampled = "1" fields = single_header.split("-", 4) if len(fields) == 1: sampled = fields[0] elif len(fields) == 2: trace_id, span_id = fields elif len(fields) == 3: trace_id, span_id, sampled = fields elif len(fields) == 4: trace_id, span_id, sampled, _ = fields else: return trace.set_span_in_context(trace.INVALID_SPAN) else: trace_id = (_extract_first_element( getter.get(carrier, self.TRACE_ID_KEY)) or trace_id) span_id = (_extract_first_element( getter.get(carrier, self.SPAN_ID_KEY)) or span_id) sampled = (_extract_first_element( getter.get(carrier, self.SAMPLED_KEY)) or sampled) flags = (_extract_first_element(getter.get( carrier, self.FLAGS_KEY)) or flags) if (self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None): id_generator = trace.get_tracer_provider().id_generator trace_id = id_generator.generate_trace_id() span_id = id_generator.generate_span_id() sampled = "0" else: trace_id = int(trace_id, 16) span_id = int(span_id, 16) options = 0 # The b3 spec provides no defined behavior for both sample and # flag values set. Since the setting of at least one implies # the desire for some form of sampling, propagate if either # header is set to allow. if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceFlags.SAMPLED return trace.set_span_in_context( trace.NonRecordingSpan( trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=trace_id, span_id=span_id, is_remote=True, trace_flags=trace.TraceFlags(options), trace_state=trace.TraceState(), )))