Merge ~hloeung/charm-graylog:master into ~graylog-charmers/charm-graylog:master

Proposed by Haw Loeung
Status: Merged
Approved by: Haw Loeung
Approved revision: 62a0100004fdda1a217eab595a727142e4159697
Merged at revision: efc18399977fbf5e6bd8a6fd2b8d745293b24382
Proposed branch: ~hloeung/charm-graylog:master
Merge into: ~graylog-charmers/charm-graylog:master
Diff against target: 532 lines (+236/-45)
11 files modified
.gitignore (+7/-3)
Makefile (+5/-10)
config.yaml (+5/-0)
files/check_graylog_health.py (+1/-1)
lib/logextract.py (+7/-7)
reactive/graylog.py (+69/-4)
requirements.txt (+1/-0)
tox.ini (+27/-0)
unit_tests/requirements.txt (+6/-0)
unit_tests/test_graylog.py (+94/-3)
unit_tests/test_logextract.py (+14/-17)
Reviewer Review Type Date Requested Status
Haw Loeung Needs Resubmitting
Joel Sing (community) +1 Approve
Review via email: mp+361477@code.launchpad.net

Commit message

Add option to tune JVM heap size - LP: #1807565

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Unable to determine commit message from repository - please click "Set commit message" and enter the commit message manually.

Revision history for this message
Joel Sing (jsing) :
review: Needs Fixing
Revision history for this message
Joel Sing (jsing) :
Revision history for this message
Joel Sing (jsing) wrote :

LGTM, couple of minor comments inline.

review: Approve (+1)
Revision history for this message
Haw Loeung (hloeung) wrote :

Minor fixes without having to change existing code.

review: Needs Resubmitting
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Merge proposal is approved, but source revision has changed, setting status to needs review.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision efc18399977fbf5e6bd8a6fd2b8d745293b24382

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index ca44404..874e257 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -1,5 +1,9 @@
6-deps/
7-__pycache__
8-*~
9 *.swp
10+*~
11+.coverage
12+.pytest_cache/
13+.tox/
14+.unit-state.db
15+__pycache__
16 builds/
17+deps/
18diff --git a/Makefile b/Makefile
19index 6483241..dc50e2b 100644
20--- a/Makefile
21+++ b/Makefile
22@@ -1,21 +1,16 @@
23 BUILDDEST:=$(if $(JUJU_REPOSITORY),$(JUJU_REPOSITORY),"../graylog-built")
24 BUILTCHARMDIR:="$(BUILDDEST)/builds/graylog"
25
26-PYTHON_SCRIPTS = \
27- ./actions/actions.py \
28- ./files/check_graylog_health.py \
29- ./lib/graylogapi.py \
30- ./reactive/graylog.py \
31- ./tests/*.py \
32- ./unit_tests/graylog.py
33-
34 all: unittest
35
36 .PHONY: clean
37 clean:
38+ @echo "Cleaning files"
39 @rm -f .unit-state.db
40 @find . -name "*.pyc" -type f -exec rm -f '{}' \;
41 @find . -name "__pycache__" -type d -prune -exec rm -rf '{}' \;
42+ @rm -rf ./.tox
43+ @rm -rf ./.pytest_cache
44
45 .PHONY: sysdeps
46 sysdeps:
47@@ -35,12 +30,12 @@ testdeps:
48 .PHONY: lint
49 lint: sysdeps
50 @echo "Checking Python syntax..."
51- @flake8 --max-complexity=10 --filename=\* --ignore=E501,E402 $(PYTHON_SCRIPTS) && echo OK
52+ @tox -e lint
53
54 .PHONY: unittest
55 unittest: sysdeps lint
56 @echo "Running unit tests..."
57- nosetests3 unit_tests/*
58+ @tox -e unit
59
60 .PHONY: charmbuild
61 charmbuild:
62diff --git a/config.yaml b/config.yaml
63index 451f73f..21e76c6 100644
64--- a/config.yaml
65+++ b/config.yaml
66@@ -88,3 +88,8 @@ options:
67 default: 5044
68 type: int
69 description: TCP port for Beats input when relation is joined
70+ jvm_heap_size:
71+ default: "1G"
72+ type: string
73+ description: |
74+ JVM Heap memory size (default 1G)
75diff --git a/files/check_graylog_health.py b/files/check_graylog_health.py
76index 5842727..a349d4a 100755
77--- a/files/check_graylog_health.py
78+++ b/files/check_graylog_health.py
79@@ -11,7 +11,7 @@ libs_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
80 if libs_dir not in sys.path:
81 sys.path.append(libs_dir)
82
83-from graylogapi import GraylogAPI
84+from graylogapi import GraylogAPI # NOQA: E402
85
86 STATE_OK = 0
87 STATE_WARNING = 1
88diff --git a/lib/logextract.py b/lib/logextract.py
89index 07e4c6f..2447da4 100644
90--- a/lib/logextract.py
91+++ b/lib/logextract.py
92@@ -4,6 +4,7 @@ extraction
93
94 import re
95
96+
97 class GraylogTitleSourceItems:
98
99 def __init__(self, graylogapi=None):
100@@ -22,7 +23,7 @@ class GraylogTitleSourceItems:
101 def post(self, title, source):
102 return self.graylogapi.request(
103 self.url, method='POST',
104- data={'title' : title, 'source': source})
105+ data={'title': title, 'source': source})
106
107 def delete(self, title):
108 id_ = self.get_for_title(title)
109@@ -44,17 +45,16 @@ class GraylogTitleSourceItems:
110 else:
111 self.post(title, source)
112
113-class GraylogPipelines(GraylogTitleSourceItems):
114
115+class GraylogPipelines(GraylogTitleSourceItems):
116 url = '/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/pipeline'
117
118-class GraylogRules(GraylogTitleSourceItems):
119
120+class GraylogRules(GraylogTitleSourceItems):
121 url = '/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/rule'
122
123
124 class GraylogStreams:
125-
126 def __init__(self, graylogapi=None):
127 self.graylogapi = graylogapi
128
129@@ -85,20 +85,21 @@ class GraylogStreams:
130 if pipeline:
131 self.graylogapi.request(url, method="POST",
132 data={"stream_id": streamid,
133- "pipeline_ids" : [pipeline['id']]})
134-
135+ "pipeline_ids": [pipeline['id']]})
136
137
138 class LogExtractPipelineException(Exception):
139 """Error handling log extraction
140 """
141
142+
143 _logextract_pipe_template = '''
144 pipeline "{}"
145 stage 0 match either
146 {}
147 end'''
148
149+
150 class LogExtractPipeline:
151 """Manage a pipeline with rules for log extraction
152 The pipeline comprises a single stage which holds
153@@ -139,4 +140,3 @@ class LogExtractPipeline:
154 pipesrc = _logextract_pipe_template.format(self.title, ''.join(titlerefs))
155 self.pipelines.set(self.title, pipesrc)
156 self.streams.connect_pipeline(self.title, self.all_messages)
157-
158diff --git a/reactive/graylog.py b/reactive/graylog.py
159index 0ce774b..c3895ff 100644
160--- a/reactive/graylog.py
161+++ b/reactive/graylog.py
162@@ -18,11 +18,15 @@ if libs_dir not in sys.path:
163 sys.path.append(libs_dir)
164
165 from graylogapi import GraylogAPI # NOQA: E402
166-import logextract
167+import logextract # NOQA: E402
168
169 API_PORT = '9001' # This is the default set by the snap
170 API_URL = 'http://127.0.0.1:9001/api/' # This is the default set by the snap
171 CONF_FILE = '/var/snap/graylog/common/server.conf'
172+# /snap/graylog is a read-only squashfs so we use the initial settings
173+# and write out an override to SERVER_DEFAULT_CONF_FILE
174+SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE = '/snap/graylog/current/etc/default/graylog-server'
175+SERVER_DEFAULT_CONF_FILE = '/var/snap/graylog/current/default-graylog-server'
176 ELASTICSEARCH_DISCOVERY_PORT = '9300'
177 SERVICE_NAME = 'snap.graylog.graylog'
178
179@@ -97,6 +101,8 @@ def configure_graylog():
180 if rules_config and isinstance(rules_config, list):
181 _configure_logextraction(rules_config)
182
183+ set_jvm_heap_size(conf['jvm_heap_size'])
184+
185 remove_state('graylog_api.configured')
186 set_state('graylog.configured')
187 report_status()
188@@ -530,7 +536,7 @@ def set_conf(key, value, conf_path=CONF_FILE):
189 so setting the configuration this way avoids having to regenerate those values.
190 """
191 conf = []
192- key_re = re.compile('^#*\s*{}'.format(key))
193+ key_re = re.compile('^#*\\s*{}'.format(key))
194
195 changed = False
196 replaced = False
197@@ -563,6 +569,64 @@ def set_conf(key, value, conf_path=CONF_FILE):
198 with open(conf_path, 'wb') as conf_file:
199 conf_file.write('\n'.join(conf).encode())
200 return True
201+
202+ return False
203+
204+
205+def parse_java_opts(opts, size):
206+ foundXms = False
207+ foundXmx = False
208+ new_opts = []
209+ for opt in opts.split():
210+ if opt.startswith('-Xms'):
211+ foundXms = True
212+ if size == '':
213+ continue
214+ new_opts.append('-Xms{}'.format(size))
215+ elif opt.startswith('-Xmx'):
216+ foundXmx = True
217+ if size == '':
218+ continue
219+ new_opts.append('-Xmx{}'.format(size))
220+ else:
221+ new_opts.append(opt)
222+ if not foundXms:
223+ new_opts.append('-Xms{}'.format(size))
224+ if not foundXmx:
225+ new_opts.append('-Xmx{}'.format(size))
226+ return new_opts
227+
228+
229+def set_jvm_heap_size(heap_size='1G', conf_path=SERVER_DEFAULT_CONF_FILE):
230+ size = heap_size.lower()
231+ conf = []
232+ key_re = re.compile('^\\s*GRAYLOG_SERVER_JAVA_OPTS\\s*="(.*)"$')
233+ changed = False
234+ # If specified /etc/default/graylog-server doesn't exist, use the default
235+ # file shipped out by the snap
236+ if not os.path.exists(conf_path):
237+ updated_conf_path = SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE
238+ else:
239+ updated_conf_path = conf_path
240+ with open(updated_conf_path, 'rb') as conf_file:
241+ for line in conf_file:
242+ line = line.decode()
243+ m = key_re.match(line)
244+ if not m:
245+ conf.append(line)
246+ continue
247+ new_opts = parse_java_opts(m.group(1), size)
248+ new_line = 'GRAYLOG_SERVER_JAVA_OPTS="{}"\n'.format(' '.join(new_opts))
249+ if new_line != line:
250+ hookenv.log('Updating GRAYLOG_SERVER_JAVA_OPTS="{}".'.format(''.join(new_opts)))
251+ changed = True
252+ conf.append(new_line)
253+
254+ if changed:
255+ set_state('graylog.needs_restart')
256+ with open(conf_path, 'wb') as conf_file:
257+ conf_file.write(''.join(conf).encode())
258+ return True
259 return False
260
261
262@@ -586,7 +650,7 @@ def configure_nagios(nagios):
263 if conf['web_listen_uri']:
264 url = urlparse(conf['web_listen_uri'])
265 # LP:#1706265: Work around this bug by escaping some characters.
266- check_string = '\<title\>Graylog\ Web\ Interface\</title\>'
267+ check_string = '\\<title\\>Graylog\\ Web\\ Interface\\</title\\>'
268 url_path = url.path
269 # Catch URI without an explicit path
270 if not url_path:
271@@ -597,7 +661,8 @@ def configure_nagios(nagios):
272 nrpe_setup.add_check(
273 'graylog_http',
274 'Graylog Web UI check',
275- '/usr/lib/nagios/plugins/check_http -I {} -p {} -u "{}" -s "{}"'.format(url.hostname, str(url.port), url_path, check_string)
276+ '/usr/lib/nagios/plugins/check_http -I {} -p {} -u "{}" -s "{}"'
277+ .format(url.hostname, str(url.port), url_path, check_string)
278 )
279 check_string = 'cluster_id'
280 nrpe_setup.add_check(
281diff --git a/requirements.txt b/requirements.txt
282new file mode 100644
283index 0000000..8462291
284--- /dev/null
285+++ b/requirements.txt
286@@ -0,0 +1 @@
287+# Include python requirements here
288diff --git a/tox.ini b/tox.ini
289new file mode 100644
290index 0000000..1cdd9ba
291--- /dev/null
292+++ b/tox.ini
293@@ -0,0 +1,27 @@
294+[tox]
295+skipsdist=True
296+envlist = unit
297+skip_missing_interpreters = True
298+
299+[testenv]
300+basepython = python3
301+setenv =
302+ PYTHONPATH = .
303+
304+[testenv:unit]
305+commands = pytest -v --ignore {toxinidir}/tests --cov=lib --cov=reactive --cov=actions --cov-report=term-missing --cov-branch
306+deps = -r{toxinidir}/unit_tests/requirements.txt
307+ -r{toxinidir}/requirements.txt
308+setenv = PYTHONPATH={toxinidir}/lib
309+
310+[testenv:lint]
311+commands = flake8
312+deps = flake8
313+
314+[flake8]
315+exclude =
316+ .git,
317+ __pycache__,
318+ .tox,
319+max-line-length = 120
320+max-complexity = 10
321diff --git a/unit_tests/requirements.txt b/unit_tests/requirements.txt
322new file mode 100644
323index 0000000..29f9318
324--- /dev/null
325+++ b/unit_tests/requirements.txt
326@@ -0,0 +1,6 @@
327+charmhelpers
328+charms.reactive
329+mock
330+pytest
331+pytest-cov
332+requests
333diff --git a/unit_tests/graylog.py b/unit_tests/test_graylog.py
334similarity index 75%
335rename from unit_tests/graylog.py
336rename to unit_tests/test_graylog.py
337index 3394201..79cdaf0 100644
338--- a/unit_tests/graylog.py
339+++ b/unit_tests/test_graylog.py
340@@ -8,9 +8,10 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
341
342 from reactive.graylog import (
343 set_conf,
344- _check_input_exists)
345-from lib.graylogapi import GraylogAPI
346-from files import check_graylog_health
347+ set_jvm_heap_size,
348+ _check_input_exists) # NOQA: E402
349+from lib.graylogapi import GraylogAPI # NOQA: E402
350+from files import check_graylog_health # NOQA: E402
351
352 initial_conf = u"""#key1 = value1
353 key2 = value2
354@@ -173,3 +174,93 @@ class TestHealthCheck(unittest.TestCase):
355 'uncommitted_journal_entries': ucommitted}
356 state, _ = check_graylog_health.journal_state(mock_api, args)
357 self.assertEqual(state, exp_state)
358+
359+
360+etc_default_conf = u"""
361+# Path to the java executable.
362+JAVA=/usr/bin/java
363+
364+# Default Java options for heap and garbage collection.
365+{}
366+
367+# Pass some extra args to graylog-server. (i.e. "-d" to enable debug mode)
368+GRAYLOG_SERVER_ARGS=""
369+
370+# Program that will be used to wrap the graylog-server command. Useful to
371+# support programs like authbind.
372+GRAYLOG_COMMAND_WRAPPER=""
373+"""
374+
375+heap_default = u"""
376+GRAYLOG_SERVER_JAVA_OPTS="-Xms1g -Xmx1g -XX:NewRatio=1 -server -XX:+ResizeTLAB -XX:+UseConcMarkSweepGC \
377+-XX:+CMSConcurrentMTEnabled -XX:+CMSClassUnloadingEnabled -XX:+UseParNewGC -XX:-OmitStackTraceInFastThrow"
378+"""
379+
380+heap_2g = u"""
381+GRAYLOG_SERVER_JAVA_OPTS="-Xms2g -Xmx2g -XX:NewRatio=1 -server -XX:+ResizeTLAB -XX:+UseConcMarkSweepGC \
382+-XX:+CMSConcurrentMTEnabled -XX:+CMSClassUnloadingEnabled -XX:+UseParNewGC -XX:-OmitStackTraceInFastThrow"
383+"""
384+
385+heap_not_present = u"""
386+GRAYLOG_SERVER_JAVA_OPTS="-XX:NewRatio=1 -server -XX:+ResizeTLAB -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled \
387+-XX:+CMSClassUnloadingEnabled -XX:+UseParNewGC -XX:-OmitStackTraceInFastThrow -Xms4g -Xmx4g"
388+"""
389+
390+heap_unset = u"""
391+GRAYLOG_SERVER_JAVA_OPTS="-XX:NewRatio=1 -server -XX:+ResizeTLAB -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled \
392+-XX:+CMSClassUnloadingEnabled -XX:+UseParNewGC -XX:-OmitStackTraceInFastThrow"
393+"""
394+
395+heap_not_present = u"""
396+GRAYLOG_SERVER_JAVA_OPTS="-XX:NewRatio=1 -server -XX:+ResizeTLAB -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled \
397+-XX:+CMSClassUnloadingEnabled -XX:+UseParNewGC -XX:-OmitStackTraceInFastThrow -Xms4g -Xmx4g"
398+"""
399+
400+heap_twice = u"""
401+GRAYLOG_SERVER_JAVA_OPTS="-XX:NewRatio=1 -server -XX:+ResizeTLAB -XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled \
402+-XX:+CMSClassUnloadingEnabled -XX:+UseParNewGC -XX:-OmitStackTraceInFastThrow -Xms8g -Xmx8g"
403+"""
404+
405+
406+class TestSetJVMHeapSize(unittest.TestCase):
407+
408+ def setUp(self):
409+ temp_file = tempfile.NamedTemporaryFile(delete=False)
410+ temp_file.write(etc_default_conf.format(heap_default).encode())
411+ temp_file.close()
412+ self.conf_file = temp_file.name
413+
414+ def tearDown(self):
415+ os.remove(self.conf_file)
416+
417+ def conf_equals(self, want):
418+ with open(self.conf_file, 'r') as conf:
419+ got = conf.read()
420+ if got == want:
421+ return True
422+ print('{}\n--- != ---\n{}'.format(got, want))
423+ return False
424+
425+ def test_same(self):
426+ set_jvm_heap_size('1g', self.conf_file)
427+ self.assertTrue(self.conf_equals(etc_default_conf.format(heap_default)))
428+
429+ def test_update(self):
430+ set_jvm_heap_size('2g', self.conf_file)
431+ self.assertTrue(self.conf_equals(etc_default_conf.format(heap_2g)))
432+
433+ def test_unset(self):
434+ # Remove Xms and Xmx
435+ set_jvm_heap_size('', self.conf_file)
436+ self.assertTrue(self.conf_equals(etc_default_conf.format(heap_unset)))
437+
438+ def test_add(self):
439+ set_jvm_heap_size('', self.conf_file)
440+ set_jvm_heap_size('4g', self.conf_file)
441+ self.assertTrue(self.conf_equals(etc_default_conf.format(heap_not_present)))
442+
443+ def test_add_twice(self):
444+ set_jvm_heap_size('', self.conf_file)
445+ set_jvm_heap_size('6g', self.conf_file)
446+ set_jvm_heap_size('8g', self.conf_file)
447+ self.assertTrue(self.conf_equals(etc_default_conf.format(heap_twice)))
448diff --git a/unit_tests/test_logextract.py b/unit_tests/test_logextract.py
449index fd04bf1..69d87d8 100644
450--- a/unit_tests/test_logextract.py
451+++ b/unit_tests/test_logextract.py
452@@ -2,20 +2,19 @@ import unittest
453 from unittest import mock
454
455 from lib import logextract
456-from lib.graylogapi import GraylogAPI
457
458
459 testdata_simple_pipeline_source = '\npipeline "testpipe"\nstage 1 match either\n rule "test-rule";\nend\n'
460
461 testdata_simple_pipeline = [{'description': None, 'stages':
462- [{'match_all': False, 'rules':
463- ['test-rule'], 'stage': 1}],
464- 'source': testdata_simple_pipeline_source,
465- 'created_at': '2018-06-18T14:49:27.427Z',
466- 'modified_at': '2018-06-18T14:49:27.427Z',
467- 'id': '5b27c6778ac7b4647301a562',
468- 'errors': None,
469- 'title': 'juju-logextract-pipeline'}]
470+ [{'match_all': False, 'rules':
471+ ['test-rule'], 'stage': 1}],
472+ 'source': testdata_simple_pipeline_source,
473+ 'created_at': '2018-06-18T14:49:27.427Z',
474+ 'modified_at': '2018-06-18T14:49:27.427Z',
475+ 'id': '5b27c6778ac7b4647301a562',
476+ 'errors': None,
477+ 'title': 'juju-logextract-pipeline'}]
478
479 testdata_streams = {'total': 1,
480 'streams': [
481@@ -30,18 +29,19 @@ testdata_rules = [
482 {
483 "title": "from ceph mon",
484 "description": "",
485- "source": "rule \"from ceph mon\"\nwhen\n has_field(\"logdir\") && $message.logdir == \"ceph\" && regex(\"^ceph-mon\", to_string($message.juju_principal_unit)).matches == true\nthen\n set_field(\"subsystem\", \"ceph-mon\");\nend;\n",
486+ "source": "rule \"from ceph mon\"\nwhen\n has_field(\"logdir\") && $message.logdir == \"ceph\" && regex(\"^ceph-mon\", to_string($message.juju_principal_unit)).matches == true\nthen\n set_field(\"subsystem\", \"ceph-mon\");\nend;\n", # NOQA: E501
487 "created_at": "2018-06-17T14:32:13.632Z",
488 "modified_at": "2018-06-17T14:42:25.818Z",
489 "errors": None,
490 "id": "5b2670ed6140b943aef99ae9"
491- },]
492+ }]
493
494 testdata_stream_connect = {
495 "stream_id": "000000000000000000000001",
496- "pipeline_ids" : ["5b35061c6140b943ae096769"]
497+ "pipeline_ids": ["5b35061c6140b943ae096769"]
498 }
499
500+
501 class TestGraylogPipelines(unittest.TestCase):
502
503 @mock.patch('lib.graylogapi.GraylogAPI')
504@@ -69,7 +69,6 @@ class TestGraylogRules(unittest.TestCase):
505 self.assertEqual(rules[0]['title'], "from ceph mon")
506
507
508-
509 class TestGraylogStreams(unittest.TestCase):
510
511 @mock.patch('lib.graylogapi.GraylogAPI')
512@@ -77,18 +76,16 @@ class TestGraylogStreams(unittest.TestCase):
513 mock_api.request.return_value = testdata_streams
514 gstreams = logextract.GraylogStreams(mock_api)
515 allmsgs = gstreams.get_stream_id_for("All messages")
516- self.assertEqual(allmsgs,
517- testdata_streams['streams'][0]['id'])
518+ self.assertEqual(allmsgs, testdata_streams['streams'][0]['id'])
519 nope = gstreams.get_stream_id_for("I don't exist")
520 self.assertIsNone(nope)
521
522+
523 class TestLogExtractPipeline(unittest.TestCase):
524
525 @mock.patch('lib.graylogapi.GraylogAPI')
526 def test_get_rules(self, mock_api):
527 mock_api.request.return_value = testdata_simple_pipeline
528- logext = logextract.LogExtractPipeline(mock_api)
529- rules = logext.get_rules()
530
531 def test_parse_rule_title(self):
532 logext = logextract.LogExtractPipeline()

Subscribers

People subscribed via source and target branches

to all changes: