TUN-4082: Test logging when running as a service

This commit is contained in:
Nuno Diegues 2021-03-23 14:28:29 +00:00
parent 8250b67a9f
commit 6106737281
2 changed files with 141 additions and 75 deletions

View File

@ -1,24 +1,79 @@
#!/usr/bin/env python #!/usr/bin/env python
import json import json
import os import os
from util import start_cloudflared, wait_tunnel_ready, send_requests, LOGGER import subprocess
from util import start_cloudflared, wait_tunnel_ready, send_requests
# Rolling logger rotate log files after 1 MB
rotate_after_size = 1000 * 1000
default_log_file = "cloudflared.log"
expect_message = "Starting tunnel"
def assert_log_to_terminal(cloudflared):
stderr = cloudflared.stderr.read(200)
# cloudflared logs the following when it first starts
# 2021-03-10T12:30:39Z INF Starting tunnel tunnelID=<tunnel ID>
assert expect_message.encode() in stderr, f"{stderr} doesn't contain {expect_message}"
def assert_log_in_file(file):
with open(file, "r") as f:
log = f.read(200)
# cloudflared logs the following when it first starts
# {"level":"info","tunnelID":"<tunnel ID>","time":"2021-03-10T12:21:13Z","message":"Starting tunnel"}
assert expect_message in log, f"{log} doesn't contain {expect_message}"
def assert_json_log(file):
def assert_in_json(j, key):
assert key in j, f"{key} is not in j"
with open(file, "r") as f:
line = f.readline()
json_log = json.loads(line)
assert_in_json(json_log, "level")
assert_in_json(json_log, "time")
assert_in_json(json_log, "message")
def assert_log_to_dir(config, log_dir):
max_batches = 3
batch_requests = 1000
for _ in range(max_batches):
send_requests(config.get_url(),
batch_requests, require_ok=False)
files = os.listdir(log_dir)
if len(files) == 2:
current_log_file_index = files.index(default_log_file)
current_file = log_dir / files[current_log_file_index]
stats = os.stat(current_file)
assert stats.st_size > 0
assert_json_log(current_file)
# One file is the current log file, the other is the rotated log file
rotated_log_file_index = 0 if current_log_file_index == 1 else 1
rotated_file = log_dir / files[rotated_log_file_index]
stats = os.stat(rotated_file)
assert stats.st_size > rotate_after_size
assert_log_in_file(rotated_file)
assert_json_log(current_file)
return
raise Exception(
f"Log file isn't rotated after sending {max_batches * batch_requests} requests")
class TestLogging: class TestLogging:
# TODO: Test logging when running as a service https://jira.cfops.it/browse/TUN-4082
# Rolling logger rotate log files after 1 MB
rotate_after_size = 1000 * 1000
default_log_file = "cloudflared.log"
expect_message = "Starting tunnel"
def test_logging_to_terminal(self, tmp_path, component_tests_config): def test_logging_to_terminal(self, tmp_path, component_tests_config):
config = component_tests_config() config = component_tests_config()
with start_cloudflared(tmp_path, config, new_process=True) as cloudflared: with start_cloudflared(tmp_path, config, new_process=True) as cloudflared:
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
self.assert_log_to_terminal(cloudflared) assert_log_to_terminal(cloudflared)
def test_logging_to_file(self, tmp_path, component_tests_config): def test_logging_to_file(self, tmp_path, component_tests_config):
log_file = tmp_path / self.default_log_file log_file = tmp_path / default_log_file
extra_config = { extra_config = {
# Convert from pathlib.Path to str # Convert from pathlib.Path to str
"logfile": str(log_file), "logfile": str(log_file),
@ -26,8 +81,8 @@ class TestLogging:
config = component_tests_config(extra_config) config = component_tests_config(extra_config)
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False): with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
self.assert_log_in_file(log_file) assert_log_in_file(log_file)
self.assert_json_log(log_file) assert_json_log(log_file)
def test_logging_to_dir(self, tmp_path, component_tests_config): def test_logging_to_dir(self, tmp_path, component_tests_config):
log_dir = tmp_path / "logs" log_dir = tmp_path / "logs"
@ -39,55 +94,4 @@ class TestLogging:
config = component_tests_config(extra_config) config = component_tests_config(extra_config)
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False): with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
self.assert_log_to_dir(config, log_dir) assert_log_to_dir(config, log_dir)
def assert_log_to_terminal(self, cloudflared):
stderr = cloudflared.stderr.read(200)
# cloudflared logs the following when it first starts
# 2021-03-10T12:30:39Z INF Starting tunnel tunnelID=<tunnel ID>
assert self.expect_message.encode(
) in stderr, f"{stderr} doesn't contain {self.expect_message}"
def assert_log_in_file(self, file, expect_message=""):
with open(file, "r") as f:
log = f.read(200)
# cloudflared logs the following when it first starts
# {"level":"info","tunnelID":"<tunnel ID>","time":"2021-03-10T12:21:13Z","message":"Starting tunnel"}
assert self.expect_message in log, f"{log} doesn't contain {self.expect_message}"
def assert_json_log(self, file):
with open(file, "r") as f:
line = f.readline()
json_log = json.loads(line)
self.assert_in_json(json_log, "level")
self.assert_in_json(json_log, "time")
self.assert_in_json(json_log, "message")
def assert_in_json(self, j, key):
assert key in j, f"{key} is not in j"
def assert_log_to_dir(self, config, log_dir):
max_batches = 3
batch_requests = 1000
for _ in range(max_batches):
send_requests(config.get_url(),
batch_requests, require_ok=False)
files = os.listdir(log_dir)
if len(files) == 2:
current_log_file_index = files.index(self.default_log_file)
current_file = log_dir / files[current_log_file_index]
stats = os.stat(current_file)
assert stats.st_size > 0
self.assert_json_log(current_file)
# One file is the current log file, the other is the rotated log file
rotated_log_file_index = 0 if current_log_file_index == 1 else 1
rotated_file = log_dir / files[rotated_log_file_index]
stats = os.stat(rotated_file)
assert stats.st_size > self.rotate_after_size
self.assert_log_in_file(rotated_file)
self.assert_json_log(current_file)
return
raise Exception(
f"Log file isn't rotated after sending {max_batches * batch_requests} requests")

View File

@ -1,12 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
from contextlib import contextmanager
import os import os
from pathlib import Path
import platform import platform
import pytest
import subprocess import subprocess
from contextlib import contextmanager
from pathlib import Path
from util import start_cloudflared, cloudflared_cmd, wait_tunnel_ready, LOGGER import pytest
import test_logging
from util import start_cloudflared, wait_tunnel_ready
def select_platform(plat): def select_platform(plat):
@ -22,34 +24,94 @@ def default_config_file():
return os.path.join(default_config_dir(), "config.yml") return os.path.join(default_config_dir(), "config.yml")
class TestServiceMode(): class TestServiceMode:
@select_platform("Darwin") @select_platform("Darwin")
@pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path") @pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
def test_launchd_service(self, component_tests_config): def test_launchd_service_log_to_file(self, tmp_path, component_tests_config):
# On Darwin cloudflared service defaults to run classic tunnel command log_file = tmp_path / test_logging.default_log_file
additional_config = { additional_config = {
# On Darwin cloudflared service defaults to run classic tunnel command
"hello-world": True, "hello-world": True,
"logfile": str(log_file),
} }
config = component_tests_config( config = component_tests_config(additional_config=additional_config, named_tunnel=False)
additional_config=additional_config, named_tunnel=False)
def assert_log_file():
test_logging.assert_log_in_file(log_file)
test_logging.assert_json_log(log_file)
self.launchd_service_scenario(config, assert_log_file)
@select_platform("Darwin")
@pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
def test_launchd_service_rotating_log(self, tmp_path, component_tests_config):
log_dir = tmp_path / "logs"
additional_config = {
# On Darwin cloudflared service defaults to run classic tunnel command
"hello-world": True,
"loglevel": "debug",
"log-directory": str(log_dir),
}
config = component_tests_config(additional_config=additional_config, named_tunnel=False)
def assert_rotating_log():
test_logging.assert_log_to_dir(config, log_dir)
self.launchd_service_scenario(config, assert_rotating_log)
def launchd_service_scenario(self, config, extra_assertions):
with self.run_service(Path(default_config_dir()), config): with self.run_service(Path(default_config_dir()), config):
self.launchctl_cmd("list") self.launchctl_cmd("list")
self.launchctl_cmd("start") self.launchctl_cmd("start")
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
extra_assertions()
self.launchctl_cmd("stop") self.launchctl_cmd("stop")
os.remove(default_config_file()) os.remove(default_config_file())
self.launchctl_cmd("list", success=False) self.launchctl_cmd("list", success=False)
@select_platform("Linux") @select_platform("Linux")
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"), reason=f"There is already a config file in default path") @pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
def test_sysv_service(self, tmp_path, component_tests_config): reason=f"There is already a config file in default path")
config = component_tests_config() def test_sysv_service_log_to_file(self, tmp_path, component_tests_config):
log_file = tmp_path / test_logging.default_log_file
additional_config = {
"logfile": str(log_file),
}
config = component_tests_config(additional_config=additional_config)
def assert_log_file():
test_logging.assert_log_in_file(log_file)
test_logging.assert_json_log(log_file)
self.sysv_service_scenario(config, tmp_path, assert_log_file)
@select_platform("Linux")
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
reason=f"There is already a config file in default path")
def test_sysv_service_rotating_log(self, tmp_path, component_tests_config):
log_dir = tmp_path / "logs"
additional_config = {
"loglevel": "debug",
"log-directory": str(log_dir),
}
config = component_tests_config(additional_config=additional_config)
def assert_rotating_log():
# We need the folder to have executable permissions for the "stat" command in the assertions to work.
subprocess.check_call(['sudo', 'chmod', 'o+x', log_dir])
test_logging.assert_log_to_dir(config, log_dir)
self.sysv_service_scenario(config, tmp_path, assert_rotating_log)
def sysv_service_scenario(self, config, tmp_path, extra_assertions):
with self.run_service(tmp_path, config, root=True): with self.run_service(tmp_path, config, root=True):
self.sysv_cmd("start") self.sysv_cmd("start")
self.sysv_cmd("status") self.sysv_cmd("status")
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
extra_assertions()
self.sysv_cmd("stop") self.sysv_cmd("stop")
# Service install copies config file to /etc/cloudflared/config.yml # Service install copies config file to /etc/cloudflared/config.yml
subprocess.run(["sudo", "rm", "/etc/cloudflared/config.yml"]) subprocess.run(["sudo", "rm", "/etc/cloudflared/config.yml"])
self.sysv_cmd("status", success=False) self.sysv_cmd("status", success=False)