Skip to content

Commit da1aff2

Browse files
authored
Merge pull request #2 from crane-cloud/ft-project-promethuis
Feat: Enable fetching of project metrics (WIP)
2 parents 802833e + a1a1074 commit da1aff2

12 files changed

Lines changed: 212 additions & 90 deletions

File tree

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ COPY . /app
2020

2121

2222
# make port available to the world outside this container
23-
EXPOSE 50000
23+
EXPOSE 4000
2424

2525
# connect to start script when db is being used
26-
CMD ["flask", "run", "--host=0.0.0.0", "--port=50000"]
26+
CMD ["flask", "run", "--host=0.0.0.0", "--port=4000"]

api_docs.yml

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -84,51 +84,6 @@ paths:
8484
500:
8585
description: "Internal Server Error"
8686

87-
"/projects/{project_id}/apps/{app_id}/metrics/memory":
88-
post:
89-
tags:
90-
- apps
91-
consumes:
92-
- application/json
93-
produces:
94-
- application/json
95-
parameters:
96-
- in: header
97-
name: Authorization
98-
required: true
99-
description: "Bearer [token]"
100-
type: string
101-
- in: path
102-
name: project_id
103-
required: true
104-
type: string
105-
- in: path
106-
name: app_id
107-
required: true
108-
type: string
109-
- in: body
110-
name: app metrics
111-
schema:
112-
properties:
113-
start:
114-
type: number
115-
format: float
116-
end:
117-
type: number
118-
format: float
119-
step:
120-
type: string
121-
122-
responses:
123-
200:
124-
description: "Memory Usage metrics for an Application"
125-
404:
126-
description: "Application metrics not found"
127-
400:
128-
description: "Bad request"
129-
500:
130-
description: "Internal Server Error"
131-
13287
"/projects/{project_id}/metrics/network":
13388
post:
13489
tags:

app/controllers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .project import (ProjectCPUView, ProjectMemoryUsageView,
2-
ProjectNetworkRequestView, ProjectStorageUsageView)
2+
ProjectNetworkRequestView)
33
from .index import (IndexView)

app/controllers/project.py

Lines changed: 101 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,123 @@
1-
import os
2-
import datetime
31
import json
4-
from types import SimpleNamespace
5-
6-
7-
import datetime
82
from prometheus_http_client import Prometheus
93
from flask_restful import Resource, request
10-
from flask import current_app, render_template
4+
from app.helpers.utils import get_project_data
115

126
from app.helpers.authenticate import (
137
jwt_required
148
)
159

16-
# Todo: figure out a way to connect to projects and get projects
17-
1810

1911
class ProjectMemoryUsageView(Resource):
20-
2112
@jwt_required
2213
def post(self, project_id):
23-
return dict(status='success', data=dict()), 200
14+
project = get_project_data(project_id, request)
15+
16+
if project.status_code != 200:
17+
return dict(status='fail', message=project.message), project.status_code
18+
19+
start = project.start
20+
end = project.end
21+
step = project.step
22+
namespace = project.namespace
23+
prometheus = Prometheus()
24+
25+
try:
26+
prom_memory_data = prometheus.query_rang(
27+
start=start,
28+
end=end,
29+
step=step,
30+
metric='sum(rate(container_memory_usage_bytes{container_name!="POD", image!="", namespace="'+namespace+'"}[5m]))')
31+
except Exception as error:
32+
return dict(status='fail', message=str(error)), 500
33+
34+
new_data = json.loads(prom_memory_data)
35+
final_data_list = []
36+
37+
try:
38+
for value in new_data["data"]["result"][0]["values"]:
39+
mem_case = {'timestamp': float(
40+
value[0]), 'value': float(value[1])}
41+
final_data_list.append(mem_case)
42+
except:
43+
return dict(status='fail', message='No values found'), 404
44+
45+
return dict(status='success', data=dict(values=final_data_list)), 200
2446

2547

2648
class ProjectCPUView(Resource):
27-
# @jwt_required
49+
@jwt_required
2850
def post(self, project_id):
29-
return dict(status='success', data=dict()), 200
51+
project = get_project_data(project_id, request)
52+
53+
if project.status_code != 200:
54+
return dict(status='fail', message=project.message), project.status_code
55+
56+
start = project.start
57+
end = project.end
58+
step = project.step
59+
namespace = project.namespace
60+
prometheus = Prometheus()
61+
62+
try:
63+
prom_cpu_data = prometheus.query_rang(
64+
start=start,
65+
end=end,
66+
step=step,
67+
metric='sum(rate(container_cpu_usage_seconds_total{container!="POD", image!="",namespace="' +
68+
namespace+'"}[5m]))'
69+
)
70+
except Exception as error:
71+
return dict(status='fail', message=str(error)), 500
72+
73+
new_data = json.loads(prom_cpu_data)
74+
final_data_list = []
75+
76+
try:
77+
for value in new_data["data"]["result"][0]["values"]:
78+
mem_case = {'timestamp': float(
79+
value[0]), 'value': float(value[1])}
80+
final_data_list.append(mem_case)
81+
except:
82+
return dict(status='fail', message='No values found'), 404
83+
84+
return dict(status='success', data=dict(values=final_data_list)), 200
3085

3186

3287
class ProjectNetworkRequestView(Resource):
33-
# @jwt_required
88+
@ jwt_required
3489
def post(self, project_id):
35-
return dict(status='success', data=dict()), 200
90+
project = get_project_data(project_id, request)
3691

92+
if project.status_code != 200:
93+
return dict(status='fail', message=project.message), project.status_code
3794

38-
class ProjectStorageUsageView(Resource):
39-
# @jwt_required
40-
def post(self, project_id):
41-
return dict(status='success', data=dict()), 200
95+
start = project.start
96+
end = project.end
97+
step = project.step
98+
namespace = project.namespace
99+
prometheus = Prometheus()
100+
101+
try:
102+
prom_ntw_data = prometheus.query_rang(
103+
start=start,
104+
end=end,
105+
step=step,
106+
metric='sum(rate(container_network_receive_bytes_total{namespace="' +
107+
namespace+'"}[5m]))'
108+
)
109+
except Exception as error:
110+
return dict(status='fail', message=str(error)), 500
111+
112+
new_data = json.loads(prom_ntw_data)
113+
final_data_list = []
114+
115+
try:
116+
for value in new_data["data"]["result"][0]["values"]:
117+
mem_case = {'timestamp': float(
118+
value[0]), 'value': float(value[1])}
119+
final_data_list.append(mem_case)
120+
except:
121+
return dict(status='fail', message='No values found'), 404
122+
123+
return dict(status='success', data=dict(values=final_data_list)), 200

app/helpers/authenticate.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
)
55
from functools import wraps
66
import jwt
7-
from flask import request , jsonify
7+
from flask import request, jsonify
88
import os
99

1010

@@ -13,23 +13,23 @@ def jwt_required(fn):
1313
def wrapper(*args, **kwargs):
1414

1515
access_token = request.headers.get('Authorization')
16-
17-
payload : object = {}
1816

19-
if access_token is None :
20-
return dict(message = 'Access token was not supplied'), 401
17+
payload: object = {}
2118

22-
try :
19+
if access_token is None:
20+
return dict(message='Access token was not supplied'), 401
21+
22+
try:
2323
token = access_token.split(' ')[1]
2424
if (access_token.split(' ')[0] != "Bearer"):
25-
return dict(message = "Bad Authorization header. Expected value 'Bearer <JWT>'") , 422
26-
27-
payload = jwt.decode(token, os.getenv('JWT_Token'), algorithms= ['HS256'])
25+
return dict(message="Bad Authorization header. Expected value 'Bearer <JWT>'"), 422
26+
27+
payload = jwt.decode(token, os.getenv(
28+
'JWT_SALT'), algorithms=['HS256'])
2829

29-
3030
except jwt.exceptions.DecodeError:
31-
return dict(message = "Access token is not valid or key") , 401
32-
31+
return dict(message="Access token is not valid or key"), 401
32+
3333
return fn(*args, **kwargs)
3434
return wrapper
3535

app/helpers/utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import requests
2+
import os
3+
import datetime
4+
from app.schemas import MetricsSchema
5+
from types import SimpleNamespace
6+
7+
PRODUCT_BASE_URL = os.getenv('PRODUCT_BASE_URL')
8+
PROJECT_ENDPOINT = f"{PRODUCT_BASE_URL}/projects"
9+
10+
11+
def get_project_data(project_id, request):
12+
project_query_data = request.get_json()
13+
14+
metric_schema = MetricsSchema()
15+
16+
validated_query_data = metric_schema.load(
17+
project_query_data)
18+
19+
# if errors:
20+
# return dict(status='fail', message=errors), 400
21+
22+
current_time = datetime.datetime.now()
23+
yesterday_time = current_time + datetime.timedelta(days=-1)
24+
25+
start = validated_query_data.get('start', yesterday_time.timestamp())
26+
end = validated_query_data.get('end', current_time.timestamp())
27+
step = validated_query_data.get('step', '1h')
28+
29+
project_response = requests.get(
30+
f"{PROJECT_ENDPOINT}/{project_id}", headers={
31+
"accept": "application/json",
32+
"Authorization": request.headers.get('Authorization')
33+
})
34+
35+
if not project_response.ok:
36+
return SimpleNamespace(status='failed', message="Failed to fetch project for current user", status_code=400)
37+
38+
project_response = project_response.json()
39+
40+
project_data = project_response['data']['project']
41+
42+
namespace = project_data['alias']
43+
prometheus_url = project_data['cluster']['prometheus_url']
44+
if not prometheus_url:
45+
return SimpleNamespace(status='fail', message='No prometheus url provided', status_code=404)
46+
47+
os.environ["PROMETHEUS_URL"] = prometheus_url
48+
return SimpleNamespace(
49+
start=start,
50+
end=end,
51+
step=step,
52+
namespace=namespace,
53+
status_code=200
54+
)

app/routes/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from flask_restful import Api
22
from app.controllers import (
33
IndexView, ProjectCPUView, ProjectMemoryUsageView,
4-
ProjectNetworkRequestView, ProjectStorageUsageView, IndexView)
4+
ProjectNetworkRequestView, IndexView)
55

66
api = Api()
77

@@ -14,5 +14,3 @@
1414
'/projects/<string:project_id>/metrics/memory')
1515
api.add_resource(ProjectNetworkRequestView,
1616
'/projects/<string:project_id>/metrics/network')
17-
api.add_resource(ProjectStorageUsageView,
18-
'/projects/<string:project_id>/metrics/storage')

app/schemas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .monitoring_metrics import (
2+
MetricsSchema, UserGraphSchema, AppGraphSchema)

app/schemas/monitoring_metrics.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from marshmallow import Schema, fields, validate
2+
3+
4+
class MetricsSchema(Schema):
5+
6+
start = fields.Float()
7+
end = fields.Float()
8+
step = fields.String(validate=[
9+
validate.Regexp(
10+
regex=r'^(?!\s*$)', error='step value should be a valid string'
11+
)
12+
])
13+
14+
15+
class UserGraphSchema(Schema):
16+
start = fields.Date()
17+
end = fields.Date()
18+
set_by = fields.String(
19+
validate=[
20+
validate.OneOf(["year", "month"],
21+
error='set_by should be year or month'
22+
),
23+
])
24+
25+
26+
class AppGraphSchema(Schema):
27+
start = fields.Date()
28+
end = fields.Date()
29+
set_by = fields.String(
30+
validate=[
31+
validate.OneOf(["year", "month"],
32+
error='set_by should be year or month'
33+
),
34+
])

config/config.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ class Base:
66

77
# main
88
SECRET_KEY = os.getenv("FLASK_APP_SECRET")
9-
VERIFICATION_SALT = os.getenv("FLASK_VERIFY_SALT")
10-
PASSWORD_SALT = os.getenv("FLASK_APP_SALT")
9+
JWT_SALT = os.getenv("JWT_SALT")
1110

1211

1312
class Development(Base):

0 commit comments

Comments
 (0)