diff --git a/grole.py b/grole.py
index be7559d..d1443fd 100755
--- a/grole.py
+++ b/grole.py
@@ -15,6 +15,7 @@ import pathlib
 import html
 import sys
 import argparse
+import logging
 from collections import defaultdict
 
 __author__ = 'witchard'
@@ -310,6 +311,7 @@ class Grole:
         self._handlers = defaultdict(list)
         self.env = {'doc': []}
         self.env.update(env)
+        self._logger = logging.getLogger('grole')
 
     def route(self, path_regex, methods=['GET'], doc=True):
         """
@@ -338,7 +340,7 @@ class Grole:
         Parses requests, finds appropriate handlers and returns responses
         """
         peer = writer.get_extra_info('peername')
-        print('New connection from {}'.format(peer))
+        self._logger.debug('New connection from {}'.format(peer))
         try:
             # Loop handling requests
             while True:
@@ -361,7 +363,7 @@ class Grole:
                                 res = Response(data=res)
                         except:
                             # Error - log it and return 500
-                            traceback.print_exc()
+                            self._logger.error(traceback.format_exc())
                             res = Response(code=500, reason='Internal Server Error')
                         break
 
@@ -371,9 +373,9 @@ class Grole:
 
                 # Respond
                 await res._write(writer)
-                print('{}: {} -> {}'.format(peer, req.path,  res.code))
+                self._logger.info('{}: {} -> {}'.format(peer, req.path,  res.code))
         except EOFError:
-            print('Connection closed from {}'.format(peer))
+            self._logger.debug('Connection closed from {}'.format(peer))
 
     def run(self, host='localhost', port=1234):
         """
@@ -390,7 +392,7 @@ class Grole:
         server = loop.run_until_complete(coro)
 
         # Run the server
-        print('Serving on {}'.format(server.sockets[0].getsockname()))
+        self._logger.info('Serving on {}'.format(server.sockets[0].getsockname()))
         try:
             loop.run_forever()
         except KeyboardInterrupt:
@@ -414,6 +416,11 @@ def parse_args(args=sys.argv[1:]):
                                 default='.')
     parser.add_argument('-n', '--noindex', help='do not show directory indexes',
                                 default=False, action='store_true')
+    loglevel = parser.add_mutually_exclusive_group()
+    loglevel.add_argument('-v', '--verbose', help='verbose logging',
+                                default=False, action='store_true')
+    loglevel.add_argument('-q', '--quiet', help='quiet logging',
+                                default=False, action='store_true')
     return parser.parse_args(args)
 
 def main(args=sys.argv[1:]):
@@ -421,6 +428,12 @@ def main(args=sys.argv[1:]):
     Run Grole static file server
     """
     args = parse_args(args)
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG)
+    elif args.quiet:
+        logging.basicConfig(level=logging.ERROR)
+    else:
+        logging.basicConfig(level=logging.INFO)
     app = Grole()
     serve_static(app, '', args.directory, not args.noindex)
     app.run(args.address, args.port)

Use logging library instead of print
diff --git a/test/test_args.py b/test/test_args.py
index 5c50383..97a2df6 100644
--- a/test/test_args.py
+++ b/test/test_args.py
@@ -10,11 +10,21 @@ class TestArgs(unittest.TestCase):
         self.assertEqual(args.port, 1234)
         self.assertEqual(args.directory, '.')
         self.assertEqual(args.noindex, False)
+        self.assertEqual(args.verbose, False)
+        self.assertEqual(args.quiet, False)
 
     def test_override(self):
-        args = grole.parse_args(['-a', 'foo', '-p', '27', '-d', 'bar', '-n'])
+        args = grole.parse_args(['-a', 'foo', '-p', '27', '-d', 'bar', '-n', '-v'])
         self.assertEqual(args.address, 'foo')
         self.assertEqual(args.port, 27)
         self.assertEqual(args.directory, 'bar')
         self.assertEqual(args.noindex, True)
+        self.assertEqual(args.verbose, True)
+        self.assertEqual(args.quiet, False)
 
+    def test_error(self):
+        try:
+            grole.parse_args(['-q', '-v'])
+        except SystemExit:
+            return
+        self.fail('Did not error on mutually exclusive args')

pip install -e .
parse_log_pytest
pytest
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
-e git+https://github.com/witchard/grole.git@58338694c664442a76e3c64e277c1873d13e2420#egg=grole
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work

name: grole
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
prefix: /opt/conda/envs/grole

test/test_args.py::TestArgs::test_defaults test/test_args.py::TestArgs::test_override
test/test_args.py::TestArgs::test_error
grole.py
grole.py