def test_export(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" span_datas = [ Span( name="span_name", context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, ) ] cloud_trace_spans = { "name": "projects/{}/traces/{}/spans/{}".format(self.project_id, trace_id, span_id), "span_id": span_id, "parent_span_id": None, "display_name": TruncatableString(value="span_name", truncated_byte_count=0), "attributes": ProtoSpan.Attributes( attribute_map={ "g.co/agent": _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}" .format( pkg_resources.get_distribution( "opentelemetry-sdk").version, cloud_trace_version, )) }), "links": None, "status": None, "time_events": None, "start_time": None, "end_time": None, } client = mock.Mock() exporter = CloudTraceSpanExporter(self.project_id, client=client) exporter.export(span_datas) client.create_span.assert_called_with(**cloud_trace_spans) self.assertTrue(client.create_span.called)
def initialize_tracer(project_id): trace.set_tracer_provider(TracerProvider()) cloud_trace_exporter = CloudTraceSpanExporter(project_id) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(cloud_trace_exporter)) opentelemetry_tracer = trace.get_tracer(__name__) return opentelemetry_tracer
def initialize_tracer(project_id): trace.set_tracer_provider(TracerProvider()) cloud_trace_exporter = CloudTraceSpanExporter(project_id) trace.get_tracer_provider().add_span_processor( SimpleSpanProcessor(cloud_trace_exporter)) propagate.set_global_textmap(CloudTraceFormatPropagator()) opentelemetry_tracer = trace.get_tracer(__name__) return opentelemetry_tracer
def test_export(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" # Create span and associated data. resource_info = Resource( { "cloud.account.id": 123, "host.id": "host", "cloud.zone": "US", "cloud.provider": "gcp", "gcp.resource_type": "gce_instance", } ) span = Span( name="span_name", context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, resource=resource_info, attributes={"attr_key": "attr_value"}, ) # pylint: disable=protected-access span._start_time = int(time_ns() - (60 * 1e9)) span._end_time = time_ns() span_data = [span] # Setup the trace exporter. channel = grpc.insecure_channel(self.address) transport = trace_service_grpc_transport.TraceServiceGrpcTransport( channel=channel ) client = TraceServiceClient(transport=transport) trace_exporter = CloudTraceSpanExporter(self.project_id, client=client) # Export the spans and verify the results. result = trace_exporter.export(span_data) self.assertEqual(result, SpanExportResult.SUCCESS)
def _tracer_setup() -> Iterator[Tracer]: """\ Context manager with common setup for tracing endpoints Yields a tracer (from a fresh SDK with new exporter) then finally flushes spans created during the test after. """ tracer_provider = TracerProvider( sampler=ALWAYS_ON, active_span_processor=BatchSpanProcessor( CloudTraceSpanExporter(project_id=os.environ.get("PROJECT_ID")) ), ) tracer = tracer_provider.get_tracer(INSTRUMENTING_MODULE_NAME) try: yield tracer finally: tracer_provider.shutdown()
def common_setup() -> Iterator[tuple[str, Tracer]]: """\ Context manager with common setup for test endpoints It extracts the test-id header, creates a tracer, and finally flushes spans created during the test """ if TEST_ID not in request.headers: raise Exception(f"{TEST_ID} header is required") test_id = request.headers[TEST_ID] tracer_provider = TracerProvider( sampler=ALWAYS_ON, active_span_processor=BatchSpanProcessor( CloudTraceSpanExporter(project_id=os.environ.get("PROJECT_ID"))), ) tracer = tracer_provider.get_tracer(INSTRUMENTING_MODULE_NAME) try: yield test_id, tracer finally: tracer_provider.shutdown()
def set_up(service_name: str): """Instantiate and configure the span exporter. The exporter is select and configured through environment variables. Parameters ---------- service_name : str The name under which the data is exported. """ if tracing_settings.TRACING_EXPORTER.lower() == "jaeger": from opentelemetry.sdk.trace.export import BatchExportSpanProcessor jaeger_exporter = jaeger.JaegerSpanExporter( service_name=service_name, agent_host_name=tracing_settings.JAEGER_AGENT_HOST_NAME, agent_port=tracing_settings.JAEGER_AGENT_PORT, ) trace.get_tracer_provider().add_span_processor( BatchExportSpanProcessor(jaeger_exporter)) elif tracing_settings.TRACING_EXPORTER.lower() == "console": from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor, ConsoleSpanExporter trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter())) elif tracing_settings.TRACING_EXPORTER == "gcp": from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter from opentelemetry.tools.cloud_trace_propagator import CloudTraceFormatPropagator from opentelemetry.sdk.trace.export import BatchExportSpanProcessor from opentelemetry.propagators import set_global_textmap cloud_trace_exporter = CloudTraceSpanExporter() trace.get_tracer_provider().add_span_processor( BatchExportSpanProcessor(cloud_trace_exporter)) set_global_textmap(CloudTraceFormatPropagator())
def create_app(): """Flask application factory to create instances of the Userservice 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 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.') # Set up tracing and export spans to Cloud Trace. if os.environ['ENABLE_TRACING'] == "true": app.logger.info("✅ Tracing enabled.") # 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( BatchExportSpanProcessor(cloud_trace_exporter)) set_global_textmap(CloudTraceFormatPropagator()) FlaskInstrumentor().instrument_app(app) else: app.logger.info("🚫 Tracing disabled.") 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
return health_pb2.HealthCheckResponse( status=health_pb2.HealthCheckResponse.UNIMPLEMENTED) if __name__ == "__main__": logger.info("initializing recommendationservice") # OpenTelemetry Tracing # TracerProvider provides global state and access to tracers. trace.set_tracer_provider(TracerProvider()) # Export traces to Google Cloud Trace # When running on GCP, the exporter handles authentication # using automatically default application credentials. # When running locally, credentials may need to be set explicitly. cloud_trace_exporter = CloudTraceSpanExporter( project_id=os.environ.get('PROJECT_ID'), ) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(cloud_trace_exporter) ) # TODO: remove OpenCensus after conversion to OpenTelemetry try: sampler = samplers.AlwaysOnSampler() exporter = stackdriver_exporter.StackdriverExporter() oc_interceptor = oc_server_interceptor.OpenCensusServerInterceptor(sampler, exporter) except: oc_interceptor = oc_server_interceptor.OpenCensusServerInterceptor() try: googleclouddebugger.enable(
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("/") 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', 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: raise UserWarning(resp.text) 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: raise UserWarning(resp.text) @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'), 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'), 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') # 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 test_export(self): resource_info = Resource({ "cloud.account.id": 123, "host.id": "host", "cloud.zone": "US", "cloud.provider": "gcp", "gcp.resource_type": "gce_instance", }) span_datas = [ Span( name="span_name", context=SpanContext( trace_id=int(self.example_trace_id, 16), span_id=int(self.example_span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, resource=resource_info, attributes={"attr_key": "attr_value"}, ) ] cloud_trace_spans = { "name": "projects/{}/traces/{}/spans/{}".format(self.project_id, self.example_trace_id, self.example_span_id), "span_id": self.example_span_id, "parent_span_id": None, "display_name": TruncatableString(value="span_name", truncated_byte_count=0), "attributes": ProtoSpan.Attributes( attribute_map={ "g.co/r/gce_instance/zone": _format_attribute_value("US"), "g.co/r/gce_instance/instance_id": _format_attribute_value("host"), "g.co/r/gce_instance/project_id": _format_attribute_value("123"), "g.co/agent": self.agent_code, "attr_key": _format_attribute_value("attr_value"), }), "links": None, "status": Status(code=StatusCode.UNSET.value), "time_events": None, "start_time": None, "end_time": None, # pylint: disable=no-member "span_kind": ProtoSpan.SpanKind.INTERNAL, } client = mock.Mock() exporter = CloudTraceSpanExporter(self.project_id, client=client) exporter.export(span_datas) self.assertTrue(client.batch_write_spans.called) client.batch_write_spans.assert_called_with( "projects/{}".format(self.project_id), [cloud_trace_spans])
CloudTraceFormatPropagator, ) from flask import Flask # Instrumenting requests opentelemetry.ext.requests.RequestsInstrumentor().instrument() # Instrumenting flask app = Flask(__name__) FlaskInstrumentor().instrument_app(app) # 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_textmap(CloudTraceFormatPropagator()) @app.route("/") def hello_world(): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("server_span"): return "Hello World!" if __name__ == "__main__": port = 5000
import requests from opentelemetry import trace from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.cloud_trace_propagator import ( CloudTraceFormatPropagator, ) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor set_global_textmap(CloudTraceFormatPropagator()) tracer_provider = TracerProvider() cloud_trace_exporter = CloudTraceSpanExporter() tracer_provider.add_span_processor( # BatchSpanProcessor buffers spans and sends them in batches in a # background thread. The default parameters are sensible, but can be # tweaked to optimize your performance BatchSpanProcessor(cloud_trace_exporter) ) trace.set_tracer_provider(tracer_provider) tracer = trace.get_tracer(__name__) RequestsInstrumentor().instrument() res = requests.get("http://localhost:6000") print(res.text)
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
def test_constructor_explicit(self): client = mock.Mock() exporter = CloudTraceSpanExporter(self.project_id, client=client) self.assertIs(exporter.client, client) self.assertEqual(exporter.project_id, self.project_id)
def create_app(): """Flask application factory to create instances of the Contact Service 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 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 log formatting date_format = "%Y-%m-%d %H:%M:%S" message_format = '%(asctime)s | [%(levelname)s] | %(funcName)s | %(message)s' logging.basicConfig(format= message_format, datefmt= date_format, stream=sys.stdout) # set log level log_levels = { "DEBUG": logging.DEBUG, "WARNING": logging.WARNING, "INFO": logging.INFO, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL } level = logging.INFO #default user_log_level = os.environ.get("LOG_LEVEL") if user_log_level is not None and user_log_level.upper() in log_levels: level = log_levels.get(user_log_level.upper()) app.logger.setLevel(level) app.logger.info("Starting contacts service.") # Set up tracing and export spans to Cloud Trace. if os.environ['ENABLE_TRACING'] == "true": app.logger.info("✅ Tracing enabled.") # 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( BatchExportSpanProcessor(cloud_trace_exporter) ) set_global_textmap(CloudTraceFormatPropagator()) FlaskInstrumentor().instrument_app(app) else: app.logger.info("🚫 Tracing disabled.") # 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
import jwt 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():
from opentelemetry.tools.cloud_trace_propagator import CloudTraceFormatPropagator # [END trace_demo_imports] app = flask.Flask(__name__) FlaskInstrumentor().instrument_app(app) def configure_exporter(exporter): trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(exporter)) propagators.set_global_textmap(CloudTraceFormatPropagator()) configure_exporter(CloudTraceSpanExporter()) tracer = trace.get_tracer(__name__) @app.route("/") def template_test(): # Sleep for a random time to imitate a random processing time time.sleep(random.uniform(0, 0.5)) with tracer.start_as_current_span("span1"): with tracer.start_as_current_span("span2"): with tracer.start_as_current_span("span3"): print("Hello world from Cloud Trace Exporter!") return "Hello World"
def test_constructor_default(self): exporter = CloudTraceSpanExporter(self.project_id) self.assertEqual(exporter.project_id, self.project_id)
from opentelemetry import trace from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, SimpleExportSpanProcessor, ) # Set up OpenTelemetry tracing trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter())) trace.get_tracer_provider().add_span_processor( BatchExportSpanProcessor(CloudTraceSpanExporter(), schedule_delay_millis=5000)) # Trace postgres queries as well Psycopg2Instrumentor().instrument() import psycopg2 from google.cloud.sqlcommenter.psycopg2.extension import CommenterCursorFactory tracer = trace.get_tracer(__name__) def main(): cursor_factory = CommenterCursorFactory( with_db_driver=True, with_dbapi_level=True,
def test_export(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" resource_info = Resource( { "cloud.account.id": 123, "host.id": "host", "cloud.zone": "US", "cloud.provider": "gcp", "gcp.resource_type": "gce_instance", } ) span_datas = [ Span( name="span_name", context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, resource=resource_info, attributes={"attr_key": "attr_value"}, ) ] cloud_trace_spans = { "name": "projects/{}/traces/{}/spans/{}".format( self.project_id, trace_id, span_id ), "span_id": span_id, "parent_span_id": None, "display_name": TruncatableString( value="span_name", truncated_byte_count=0 ), "attributes": ProtoSpan.Attributes( attribute_map={ "g.co/r/gce_instance/zone": _format_attribute_value("US"), "g.co/r/gce_instance/instance_id": _format_attribute_value( "host" ), "g.co/r/gce_instance/project_id": _format_attribute_value( "123" ), "g.co/agent": _format_attribute_value( "opentelemetry-python {}; google-cloud-trace-exporter {}".format( _strip_characters( pkg_resources.get_distribution( "opentelemetry-sdk" ).version ), _strip_characters(cloud_trace_version), ) ), "attr_key": _format_attribute_value("attr_value"), } ), "links": None, "status": None, "time_events": None, "start_time": None, "end_time": None, } client = mock.Mock() exporter = CloudTraceSpanExporter(self.project_id, client=client) exporter.export(span_datas) self.assertTrue(client.batch_write_spans.called) client.batch_write_spans.assert_called_with( "projects/{}".format(self.project_id), [cloud_trace_spans] )
import time from opentelemetry import trace from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import (BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor) # Set up OpenTelemetry tracing trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( SimpleSpanProcessor(ConsoleSpanExporter())) trace.get_tracer_provider().add_span_processor( BatchSpanProcessor(CloudTraceSpanExporter(), schedule_delay_millis=5000)) # Trace postgres queries as well Psycopg2Instrumentor().instrument() import psycopg2 from google.cloud.sqlcommenter.psycopg2.extension import CommenterCursorFactory tracer = trace.get_tracer(__name__) def main(): cursor_factory = CommenterCursorFactory( with_db_driver=True, with_dbapi_level=True, with_dbapi_threadsafety=True,