-
Notifications
You must be signed in to change notification settings - Fork 0
/
productpage.py
executable file
·431 lines (351 loc) · 14.9 KB
/
productpage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
#!/usr/bin/python
#
# Copyright Istio Authors
#
# 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 __future__ import print_function
from flask_bootstrap import Bootstrap
from flask import Flask, request, session, render_template, redirect, url_for
from flask import _request_ctx_stack as stack
from jaeger_client import Tracer, ConstSampler
from jaeger_client.reporter import NullReporter
from jaeger_client.codecs import B3Codec
from opentracing.ext import tags
from opentracing.propagation import Format
from opentracing_instrumentation.request_context import get_current_span, span_in_context
import simplejson as json
import requests
import sys
from json2html import *
import logging
import os
import asyncio
# These two lines enable debugging at httplib level (requests->urllib3->http.client)
# You will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA.
# The only thing missing will be the response.body which is not logged.
try:
import http.client as http_client
except ImportError:
# Python 2
import httplib as http_client
http_client.HTTPConnection.debuglevel = 1
app = Flask(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
app.logger.addHandler(logging.StreamHandler(sys.stdout))
app.logger.setLevel(logging.DEBUG)
# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
Bootstrap(app)
servicesDomain = "" if (os.environ.get("SERVICES_DOMAIN") is None) else "." + os.environ.get("SERVICES_DOMAIN")
detailsHostname = "details" if (os.environ.get("DETAILS_HOSTNAME") is None) else os.environ.get("DETAILS_HOSTNAME")
ratingsHostname = "ratings" if (os.environ.get("RATINGS_HOSTNAME") is None) else os.environ.get("RATINGS_HOSTNAME")
reviewsHostname = "reviews" if (os.environ.get("REVIEWS_HOSTNAME") is None) else os.environ.get("REVIEWS_HOSTNAME")
flood_factor = 0 if (os.environ.get("FLOOD_FACTOR") is None) else int(os.environ.get("FLOOD_FACTOR"))
details = {
"name": "http://{0}{1}:9080".format(detailsHostname, servicesDomain),
"endpoint": "details",
"children": []
}
ratings = {
"name": "http://{0}{1}:9080".format(ratingsHostname, servicesDomain),
"endpoint": "ratings",
"children": []
}
reviews = {
"name": "http://{0}{1}:9080".format(reviewsHostname, servicesDomain),
"endpoint": "reviews",
"children": [ratings]
}
productpage = {
"name": "http://{0}{1}:9080".format(detailsHostname, servicesDomain),
"endpoint": "details",
"children": [details, reviews]
}
service_dict = {
"productpage": productpage,
"details": details,
"reviews": reviews,
}
# A note on distributed tracing:
#
# Although Istio proxies are able to automatically send spans, they need some
# hints to tie together the entire trace. Applications need to propagate the
# appropriate HTTP headers so that when the proxies send span information, the
# spans can be correlated correctly into a single trace.
#
# To do this, an application needs to collect and propagate headers from the
# incoming request to any outgoing requests. The choice of headers to propagate
# is determined by the trace configuration used. See getForwardHeaders for
# the different header options.
#
# This example code uses OpenTracing (http://opentracing.io/) to propagate
# the 'b3' (zipkin) headers. Using OpenTracing for this is not a requirement.
# Using OpenTracing allows you to add application-specific tracing later on,
# but you can just manually forward the headers if you prefer.
#
# The OpenTracing example here is very basic. It only forwards headers. It is
# intended as a reference to help people get started, eg how to create spans,
# extract/inject context, etc.
# A very basic OpenTracing tracer (with null reporter)
tracer = Tracer(
one_span_per_rpc=True,
service_name='productpage',
reporter=NullReporter(),
sampler=ConstSampler(decision=True),
extra_codecs={Format.HTTP_HEADERS: B3Codec()}
)
def trace():
'''
Function decorator that creates opentracing span from incoming b3 headers
'''
def decorator(f):
def wrapper(*args, **kwargs):
request = stack.top.request
try:
# Create a new span context, reading in values (traceid,
# spanid, etc) from the incoming x-b3-*** headers.
span_ctx = tracer.extract(
Format.HTTP_HEADERS,
dict(request.headers)
)
# Note: this tag means that the span will *not* be
# a child span. It will use the incoming traceid and
# spanid. We do this to propagate the headers verbatim.
rpc_tag = {tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER}
span = tracer.start_span(
operation_name='op', child_of=span_ctx, tags=rpc_tag
)
except Exception as e:
# We failed to create a context, possibly due to no
# incoming x-b3-*** headers. Start a fresh span.
# Note: This is a fallback only, and will create fresh headers,
# not propagate headers.
span = tracer.start_span('op')
with span_in_context(span):
r = f(*args, **kwargs)
return r
wrapper.__name__ = f.__name__
return wrapper
return decorator
def getForwardHeaders(request):
headers = {}
# x-b3-*** headers can be populated using the opentracing span
span = get_current_span()
carrier = {}
tracer.inject(
span_context=span.context,
format=Format.HTTP_HEADERS,
carrier=carrier)
headers.update(carrier)
# We handle other (non x-b3-***) headers manually
if 'user' in session:
headers['end-user'] = session['user']
# Keep this in sync with the headers in details and reviews.
incoming_headers = [
# All applications should propagate x-request-id. This header is
# included in access log statements and is used for consistent trace
# sampling and log sampling decisions in Istio.
'x-request-id',
# Lightstep tracing header. Propagate this if you use lightstep tracing
# in Istio (see
# https://istio.io/latest/docs/tasks/observability/distributed-tracing/lightstep/)
# Note: this should probably be changed to use B3 or W3C TRACE_CONTEXT.
# Lightstep recommends using B3 or TRACE_CONTEXT and most application
# libraries from lightstep do not support x-ot-span-context.
'x-ot-span-context',
# Datadog tracing header. Propagate these headers if you use Datadog
# tracing.
'x-datadog-trace-id',
'x-datadog-parent-id',
'x-datadog-sampling-priority',
# W3C Trace Context. Compatible with OpenCensusAgent and Stackdriver Istio
# configurations.
'traceparent',
'tracestate',
# Cloud trace context. Compatible with OpenCensusAgent and Stackdriver Istio
# configurations.
'x-cloud-trace-context',
# Grpc binary trace context. Compatible with OpenCensusAgent nad
# Stackdriver Istio configurations.
'grpc-trace-bin',
# b3 trace headers. Compatible with Zipkin, OpenCensusAgent, and
# Stackdriver Istio configurations. Commented out since they are
# propagated by the OpenTracing tracer above.
# 'x-b3-traceid',
# 'x-b3-spanid',
# 'x-b3-parentspanid',
# 'x-b3-sampled',
# 'x-b3-flags',
# Application-specific headers to forward.
'user-agent',
]
# For Zipkin, always propagate b3 headers.
# For Lightstep, always propagate the x-ot-span-context header.
# For Datadog, propagate the corresponding datadog headers.
# For OpenCensusAgent and Stackdriver configurations, you can choose any
# set of compatible headers to propagate within your application. For
# example, you can propagate b3 headers or W3C trace context headers with
# the same result. This can also allow you to translate between context
# propagation mechanisms between different applications.
for ihdr in incoming_headers:
val = request.headers.get(ihdr)
if val is not None:
headers[ihdr] = val
return headers
# The UI:
@app.route('/')
@app.route('/index.html')
def index():
""" Display productpage with normal user and test user buttons"""
global productpage
table = json2html.convert(json=json.dumps(productpage),
table_attributes="class=\"table table-condensed table-bordered table-hover\"")
return render_template('index.html', serviceTable=table)
@app.route('/health')
def health():
return 'Product page is healthy'
@app.route('/login', methods=['POST'])
def login():
user = request.values.get('username')
response = app.make_response(redirect(request.referrer))
session['user'] = user
return response
@app.route('/logout', methods=['GET'])
def logout():
response = app.make_response(redirect(request.referrer))
session.pop('user', None)
return response
# a helper function for asyncio.gather, does not return a value
async def getProductReviewsIgnoreResponse(product_id, headers):
getProductReviews(product_id, headers)
# flood reviews with unnecessary requests to demonstrate Istio rate limiting, asynchoronously
async def floodReviewsAsynchronously(product_id, headers):
# the response is disregarded
await asyncio.gather(*(getProductReviewsIgnoreResponse(product_id, headers) for _ in range(flood_factor)))
# flood reviews with unnecessary requests to demonstrate Istio rate limiting
def floodReviews(product_id, headers):
loop = asyncio.new_event_loop()
loop.run_until_complete(floodReviewsAsynchronously(product_id, headers))
loop.close()
@app.route('/productpage')
@trace()
def front():
product_id = 0 # TODO: replace default value
headers = getForwardHeaders(request)
user = session.get('user', '')
product = getProduct(product_id)
detailsStatus, details = getProductDetails(product_id, headers)
if flood_factor > 0:
floodReviews(product_id, headers)
reviewsStatus, reviews = getProductReviews(product_id, headers)
return render_template(
'productpage.html',
detailsStatus=detailsStatus,
reviewsStatus=reviewsStatus,
product=product,
details=details,
reviews=reviews,
user=user)
# The API:
@app.route('/api/v1/products')
def productsRoute():
return json.dumps(getProducts()), 200, {'Content-Type': 'application/json'}
@app.route('/api/v1/products/<product_id>')
@trace()
def productRoute(product_id):
headers = getForwardHeaders(request)
status, details = getProductDetails(product_id, headers)
return json.dumps(details), status, {'Content-Type': 'application/json'}
@app.route('/api/v1/products/<product_id>/reviews')
@trace()
def reviewsRoute(product_id):
headers = getForwardHeaders(request)
status, reviews = getProductReviews(product_id, headers)
return json.dumps(reviews), status, {'Content-Type': 'application/json'}
@app.route('/api/v1/products/<product_id>/ratings')
@trace()
def ratingsRoute(product_id):
headers = getForwardHeaders(request)
status, ratings = getProductRatings(product_id, headers)
return json.dumps(ratings), status, {'Content-Type': 'application/json'}
# Data providers:
def getProducts():
return [
{
'id': 0,
'title': 'The Comedy of Errors',
'descriptionHtml': '<a href="https://en.wikipedia.org/wiki/The_Comedy_of_Errors">Wikipedia Summary</a>: The Comedy of Errors is one of <b>William Shakespeare\'s</b> early plays. It is his shortest and one of his most farcical comedies, with a major part of the humour coming from slapstick and mistaken identity, in addition to puns and word play.'
}
]
def getProduct(product_id):
products = getProducts()
if product_id + 1 > len(products):
return None
else:
return products[product_id]
def getProductDetails(product_id, headers):
try:
url = details['name'] + "/" + details['endpoint'] + "/" + str(product_id)
res = requests.get(url, headers=headers, timeout=3.0)
except BaseException:
res = None
if res and res.status_code == 200:
return 200, res.json()
else:
status = res.status_code if res is not None and res.status_code else 500
return status, {'error': 'Sorry, product details are currently unavailable for this book.'}
def getProductReviews(product_id, headers):
# Do not remove. Bug introduced explicitly for illustration in fault injection task
# TODO: Figure out how to achieve the same effect using Envoy retries/timeouts
for _ in range(2):
try:
url = reviews['name'] + "/" + reviews['endpoint'] + "/" + str(product_id)
res = requests.get(url, headers=headers, timeout=3.0)
except BaseException:
res = None
if res and res.status_code == 200:
return 200, res.json()
status = res.status_code if res is not None and res.status_code else 500
return status, {'error': 'Sorry, product reviews are currently unavailable for this book.'}
def getProductRatings(product_id, headers):
try:
url = ratings['name'] + "/" + ratings['endpoint'] + "/" + str(product_id)
res = requests.get(url, headers=headers, timeout=3.0)
except BaseException:
res = None
if res and res.status_code == 200:
return 200, res.json()
else:
status = res.status_code if res is not None and res.status_code else 500
return status, {'error': 'Sorry, product ratings are currently unavailable for this book.'}
class Writer(object):
def __init__(self, filename):
self.file = open(filename, 'w')
def write(self, data):
self.file.write(data)
def flush(self):
self.file.flush()
if __name__ == '__main__':
if len(sys.argv) < 2:
logging.error("usage: %s port" % (sys.argv[0]))
sys.exit(-1)
p = int(sys.argv[1])
logging.info("start at port %s" % (p))
# Make it compatible with IPv6 if Linux
if sys.platform == "linux":
app.run(host='::', port=p, debug=True, threaded=True)
else:
app.run(host='0.0.0.0', port=p, debug=True, threaded=True)