Skip to content

Commit bee2ca8

Browse files
committed
app changed to use a file with url, not single url ; also some unit-tests added but not completely
1 parent c3564fb commit bee2ca8

7 files changed

+242
-28
lines changed

README.md

+47-13
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
## Usage
44

5-
Multiple installation scenarios are provided.
5+
Multiple installation scenarios are provided. To test exporter locally without real GE servers try to run a fake GE server instead:
6+
7+
```shell script
8+
python3 fake_ge_server.py && echo 'http://localhost:8081' > $(pwd)/urls.txt
9+
```
10+
Don't forget to clone repo first!
611

712
### Docker
813

9-
Don't forget to pass valid url link to your Gradle Enterprise server:
14+
1015

1116
```shell script
12-
docker run -it --rm \
13-
-p 8080:8080 \
14-
-e APP_URL_LINK=http://localhost \
15-
-e APP_PORT=8080 \
17+
PORT=8080 ; docker run -it --rm \
18+
-p ${PORT}:${PORT} \
19+
-v "$(pwd)/":"/app/" \
20+
-e APP_FILE_PATH=/app/urls.txt \
21+
-e APP_PORT=${PORT} \
1622
-e APP_CHECK_INTERVAL=60 \
1723
-e LOG_LEVEL=DEBUG \
1824
bissquit/gradle-server-exporter:latest
@@ -28,22 +34,50 @@ docker-compose up -d --build
2834

2935
### k8s
3036

31-
Use [k8s-handle](https://github.com/2gis/k8s-handle) to deploy exporter to k8s environment.
37+
Use [k8s-handle](https://github.com/2gis/k8s-handle) to deploy exporter to k8s environment:
38+
39+
```shell script
40+
k8s-handle apply -s env-name
41+
```
3242

3343
Render templates without deployment:
3444

3545
```shell script
3646
k8s-handle render -s env-name
3747
```
3848

39-
Deploy:
40-
41-
```shell script
42-
k8s-handle apply -s env-name
49+
# Help
50+
51+
Exporter looks for the file contains url(s) to your Gradle Enterprise Servers. Each line is [well-formatted](https://validators.readthedocs.io/en/latest/#module-validators.url) url.
52+
You may pass options both via command line arguments or environment variables:
53+
54+
|Command line argument|Environment variable|Description|
55+
| ----------- | ----------- | ----------- |
56+
|-h, --help|-|show help message|
57+
|-f, --file|`APP_FILE_PATH`|Absolute path to file. Each line is url link (default: Empty string)|
58+
|-p, --port|`APP_PORT`|Port to be listened (default: 8080)|
59+
|-t, --time|`APP_CHECK_INTERVAL`|Default time range in seconds to check metrics (default: 60)|
60+
|-|`LOG_LEVEL`|Log level based on Python [logging](https://docs.python.org/3/library/logging.html) module. expected values: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)|
61+
62+
Metrics example:
63+
64+
```text
65+
gradle_ingest_queue_pending{url="http://localhost:8081"} 0
66+
gradle_ingest_queue_requested{url="http://localhost:8081"} 0
67+
gradle_ingest_queue_ageMins{url="http://localhost:8081"} 0
68+
gradle_ingest_queue_requestWaitTimeSecs{url="http://localhost:8081"} 0
69+
gradle_ingest_queue_incomingRate1m{url="http://localhost:8081"} 0.03221981766544038
70+
gradle_ingest_queue_incomingRate5m{url="http://localhost:8081"} 0.02219163413405735
71+
gradle_ingest_queue_incomingRate15m{url="http://localhost:8081"} 0.021373141599789678
72+
gradle_ingest_queue_processingRate1m{url="http://localhost:8081"} 0.03399783025186821
73+
gradle_ingest_queue_processingRate5m{url="http://localhost:8081"} 0.022374841163558885
74+
gradle_ingest_queue_processingRate15m{url="http://localhost:8081"} 0.021459615070953553
4375
```
4476

45-
## Setup environment
46-
Setup is quite simple
77+
78+
## Dev environment
79+
80+
Setup environment is quite simple:
4781
```shell script
4882
make env
4983
make test

deploy/config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ common:
2828

2929
env-name:
3030
app_name: gradle-server-exporter
31-
url_link: http://localhost
31+
file_path: /app/urls.txt

deploy/templates/deployment.yaml.j2

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ spec:
2626
- name: {{ app_name }}
2727
image: {{ image_path }}:{{ image_version }}
2828
env:
29-
- name: APP_URL_LINK
30-
value: "{{ url_link }}"
29+
- name: APP_FILE_PATH
30+
value: "{{ file_path }}"
3131
- name: APP_PORT
3232
value: "{{ app_port }}"
3333
- name: APP_LOOP_TIME

fake_ge_server.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from aiohttp import web
2+
3+
4+
async def handle(request):
5+
name = request.match_info.get('name', "Anonymous")
6+
text = '''{
7+
"pending" : 0,
8+
"requested" : 0,
9+
"ageMins" : 0,
10+
"requestWaitTimeSecs" : 0,
11+
"incomingRate1m" : 0.03221981766544038,
12+
"incomingRate5m" : 0.02219163413405735,
13+
"incomingRate15m" : 0.021373141599789678,
14+
"processingRate1m" : 0.03399783025186821,
15+
"processingRate5m" : 0.022374841163558885,
16+
"processingRate15m" : 0.021459615070953553
17+
}'''
18+
return web.Response(text=text, headers={'Content-Type': 'application/json'})
19+
20+
app = web.Application()
21+
app.add_routes([web.get('/', handle)])
22+
23+
if __name__ == '__main__':
24+
web.run_app(app, port=8081)

gradle_server_exporter.py

+68-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import os
2+
import sys
23
import json
34
import aiohttp
45
from aiohttp import web
56
import asyncio
67
import logging
78
import argparse
9+
import validators
810

911
logging.basicConfig(level=os.getenv("LOG_LEVEL", logging.INFO), format='%(asctime)s - %(levelname)s - %(message)s')
1012
logger = logging.getLogger(__name__)
@@ -14,6 +16,10 @@ def parse_args():
1416
# You may either use command line argument or env variables
1517
parser = argparse.ArgumentParser(prog='gradle_server_exporter',
1618
description='Prometheus exporter for Gradle Enterprise Server ')
19+
parser.add_argument('-f', '--file',
20+
default=os.getenv("APP_FILE_PATH", ''),
21+
type=str,
22+
help='Absolute path to file. Each line is url link (default: Empty string)')
1723
parser.add_argument('-p', '--port',
1824
default=os.getenv("APP_PORT", 8080),
1925
type=int,
@@ -22,10 +28,6 @@ def parse_args():
2228
default=os.getenv("APP_CHECK_INTERVAL", 60),
2329
type=int,
2430
help='Default time range in seconds to check metrics (default: 60)')
25-
parser.add_argument('-l', '--link',
26-
default=os.getenv("APP_URL_LINK", False),
27-
type=str,
28-
help='Put source ip address into labels set (default: False)')
2931
return parser.parse_args()
3032

3133

@@ -75,10 +77,10 @@ def validate_json(json_data):
7577
return None
7678

7779

78-
def generate_metrics(json_data):
80+
def generate_metrics(json_data, url):
7981
metrics_str = ''
8082
for k, v in json_data.items():
81-
metrics_str += f'gradle_ingest_queue_{k} {v}\n'
83+
metrics_str += f'gradle_ingest_queue_{k}{{url="{url}"}} {v}\n'
8284
return metrics_str
8385

8486

@@ -92,9 +94,16 @@ async def cleanup_background_tasks(app):
9294

9395

9496
async def metrics_checker(app):
97+
if not app['urls_list']:
98+
logging.critical('Empty urls list is accepted. Nothing to check. Exiting...')
99+
sys.exit(1)
100+
95101
while True:
96-
json_data = await get_data(app['args'].link)
97-
app['metrics_str'] = generate_metrics(json_data=json_data)
102+
app['metrics_str'] = ''
103+
for url in app['urls_list']:
104+
json_data = await get_data(url=url)
105+
app['metrics_str'] += generate_metrics(json_data=json_data,
106+
url=url)
98107
await asyncio.sleep(app['args'].time)
99108

100109

@@ -106,12 +115,63 @@ async def return_metrics(request):
106115
return web.Response(text=request.app['metrics_str'])
107116

108117

118+
class HandleFileData:
119+
def __init__(self, path):
120+
self.path = path
121+
122+
def return_urls_list(self):
123+
strs_list = self.read_file(path=self.path)
124+
urls_list = self.normalize_urls_list(strs_list=strs_list)
125+
return urls_list
126+
127+
def normalize_urls_list(self, strs_list):
128+
invalid_urls_counter = 0
129+
urls_list = []
130+
131+
for line in strs_list:
132+
line_striped = line.strip()
133+
if line_striped == '':
134+
pass
135+
# ignore line in the list if it doesn't look like a valid url
136+
elif self.normalize_url(url=line_striped) is False:
137+
invalid_urls_counter += 1
138+
else:
139+
urls_list.append(line_striped)
140+
141+
if invalid_urls_counter > 0:
142+
logger.warning(f'{invalid_urls_counter} url(s) has been removed due to invalid format. Check file {self.path}')
143+
logger.info('Read more about url format at https://validators.readthedocs.io/en/latest/#module-validators.url')
144+
145+
return urls_list
146+
147+
@staticmethod
148+
def read_file(path):
149+
try:
150+
logger.info(f'Reading file {path}')
151+
with open(path) as file:
152+
# Return all lines in the file, as a list where each line is an item
153+
strs_list = file.readlines()
154+
except OSError as error:
155+
logger.critical(f'Could not read file {path}: {error}')
156+
strs_list = []
157+
return strs_list
158+
159+
@staticmethod
160+
def normalize_url(url):
161+
result = True
162+
if not validators.url(url):
163+
logger.warning(f'String {url} is not a valid url. Skipping...')
164+
result = False
165+
return result
166+
167+
109168
def main():
110169
args = parse_args()
111170
app = web.Application()
112171
# For storing global-like variables, feel free to save them in an Application instance
113172
app['metrics_str'] = 'Initialization'
114173
app['args'] = args
174+
app['urls_list'] = HandleFileData(args.file).return_urls_list()
115175
app.add_routes([web.get('/metrics', return_metrics)])
116176
# https://docs.aiohttp.org/en/stable/web_advanced.html#background-tasks
117177
app.on_startup.append(start_background_tasks)

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
aiohttp==3.7.4.post0
2+
validators==0.18.2

tests/test_gradle_server_exporter.py

+99-4
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import aiohttp
33
import json
44
import yarl
5+
import io
56
from multidict import CIMultiDictProxy, CIMultiDict
67
from gradle_server_exporter import \
78
get_data, \
89
validate_json, \
910
parse_args, \
10-
generate_metrics
11+
generate_metrics, \
12+
HandleFileData
1113

1214

1315
class MockResponse:
@@ -55,6 +57,18 @@ async def json(self):
5557
return json.loads(self._text)
5658

5759

60+
# emulates incorrect directory path
61+
class MockOSReadFile:
62+
def __init__(self, path):
63+
self.path = path
64+
65+
def __enter__(self):
66+
pass
67+
68+
def __exit__(self, exc_type, exc_val, exc_tb):
69+
raise OSError
70+
71+
5872
@pytest.mark.asyncio
5973
async def test_get_data(mocker):
6074
json_data = {
@@ -105,13 +119,94 @@ def test_parse_args():
105119

106120

107121
def test_generate_metrics():
122+
fake_url_str = 'http://fake.url'
108123
json_data = {
109124
"pending": 0,
110125
"requested": 0
111126
}
112-
result = generate_metrics(json_data=json_data)
113-
assert result == 'gradle_ingest_queue_pending 0\ngradle_ingest_queue_requested 0\n'
127+
result = generate_metrics(json_data=json_data,
128+
url=fake_url_str)
129+
assert result == f'gradle_ingest_queue_pending{{url="{fake_url_str}"}} 0\ngradle_ingest_queue_requested{{url="{fake_url_str}"}} 0\n'
114130

115131
json_data = json.loads('{}')
116-
result = generate_metrics(json_data=json_data)
132+
result = generate_metrics(json_data=json_data,
133+
url=fake_url_str)
117134
assert result == ''
135+
136+
137+
def test_normalize_url():
138+
invalid_urls_list = [
139+
'http:/google.com',
140+
'httpa://google.com',
141+
'htt:/google.com',
142+
'http//google.com',
143+
'ttp://google.com',
144+
'http://google',
145+
'http://google.com:44a',
146+
'google.com'
147+
]
148+
149+
for url in invalid_urls_list:
150+
result = HandleFileData('').normalize_url(url=url)
151+
assert result is False
152+
153+
valid_urls_list = [
154+
'http://google.com',
155+
'https://google.com',
156+
'http://google.com:443',
157+
'http://google.com/path',
158+
'http://google.com:443/path?'
159+
]
160+
161+
for url in valid_urls_list:
162+
result = HandleFileData('').normalize_url(url=url)
163+
assert result is True
164+
165+
166+
@pytest.mark.asyncio
167+
def test_read_file(mocker):
168+
fake_path_str = '/fake/path'
169+
170+
file_data_str = 'http://google.ru \nhttps://ya.ru\n'
171+
# creates file-like obj in memory with appropriate methods like read() and write()
172+
file = io.StringIO(file_data_str)
173+
mocker.patch("builtins.open", return_value=file)
174+
client = HandleFileData('').read_file(fake_path_str)
175+
assert client == ['http://google.ru \n', 'https://ya.ru\n']
176+
177+
file_data_str = '\n\n'
178+
file = io.StringIO(file_data_str)
179+
mocker.patch("builtins.open", return_value=file)
180+
client = HandleFileData('').read_file(fake_path_str)
181+
assert client == ['\n', '\n']
182+
183+
file_data_str = ''
184+
file = io.StringIO(file_data_str)
185+
mocker.patch("builtins.open", return_value=file)
186+
client = HandleFileData('').read_file(fake_path_str)
187+
assert client == []
188+
189+
resp = MockOSReadFile(fake_path_str)
190+
mocker.patch("builtins.open", return_value=resp)
191+
client = HandleFileData('').read_file(fake_path_str)
192+
assert client == []
193+
194+
195+
def test_normalize_urls_list():
196+
file_data_str = [
197+
'htt://google.ru \n',
198+
'https://ya.ru\n',
199+
'\n',
200+
' \n',
201+
' \n',
202+
'\n ',
203+
'\n ',
204+
'\n\n',
205+
'\n\n ',
206+
'\n\n ',
207+
' ',
208+
'google.ru',
209+
' http://site.tld '
210+
]
211+
result = HandleFileData('').normalize_urls_list(strs_list=file_data_str)
212+
assert result == ['https://ya.ru', 'http://site.tld']

0 commit comments

Comments
 (0)