diff --git a/docs/conf.py b/docs/conf.py
index 6123867..6bfaa58 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -21,14 +21,14 @@ import sys
 
 sys.path.insert(0, os.path.abspath(".."))
 
-from sqllineage import name, version  # noqa
+from sqllineage import NAME, VERSION  # noqa
 
-project = name
+project = NAME
 copyright = "2019, Reata"  # noqa
 author = "Reata"
 
 # The full version, including alpha/beta/rc tags
-release = version
+release = VERSION
 
 # -- General configuration ---------------------------------------------------
 
diff --git a/setup.py b/setup.py
index 7e0720c..ff627bb 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
 from setuptools import find_packages, setup
 
-from sqllineage import name, version
+from sqllineage import NAME, VERSION
 
 with open("README.md", "r") as f:
     long_description = f.read()
@@ -21,8 +21,8 @@ ci_requires = [
 ]
 
 setup(
-    name=name,
-    version=version,
+    name=NAME,
+    version=VERSION,
     author="Reata",
     author_email="reddevil.hjw@gmail.com",
     description="SQL Lineage Analysis Tool powered by Python",
diff --git a/sqllineage/__init__.py b/sqllineage/__init__.py
index 896ffc8..e969c0e 100644
--- a/sqllineage/__init__.py
+++ b/sqllineage/__init__.py
@@ -1,2 +1,25 @@
-name = "sqllineage"
-version = "0.3.0"
+import logging.config
+
+NAME = "sqllineage"
+VERSION = "0.3.0"
+DEFAULT_LOGGING = {
+    "version": 1,
+    "disable_existing_loggers": False,
+    "formatters": {"default": {"format": "%(levelname)s: %(message)s"}},
+    "handlers": {
+        "console": {
+            "level": "WARNING",
+            "class": "logging.StreamHandler",
+            "formatter": "default",
+        }
+    },
+    "loggers": {
+        "": {
+            "handlers": ["console"],
+            "level": "WARNING",
+            "propagate": False,
+            "filters": [],
+        }
+    },
+}
+logging.config.dictConfig(DEFAULT_LOGGING)
diff --git a/sqllineage/runner.py b/sqllineage/runner.py
index d8fe8c2..abe8280 100644
--- a/sqllineage/runner.py
+++ b/sqllineage/runner.py
@@ -1,6 +1,6 @@
 import argparse
 import importlib
-import sys
+import logging
 from typing import List, Type
 
 import sqlparse
@@ -11,6 +11,9 @@ from sqllineage.core import LineageAnalyzer
 from sqllineage.models import Table
 
 
+logger = logging.getLogger(__name__)
+
+
 class LineageRunner(object):
     def __init__(
         self,
@@ -111,20 +114,16 @@ def main(args=None) -> None:
             combiner_module, combiner_name = args.c.rsplit(".", maxsplit=1)
             combiner = getattr(importlib.import_module(combiner_module), combiner_name)
         except ValueError:
-            print(
-                "ERROR: Combiner should be named as module.class, got {} instead".format(
-                    args.c
-                ),
-                file=sys.stderr,
+            logger.exception(
+                "Combiner should be named as module.class, got %s instead", args.c
             )
-            raise
+            exit(1)
         except (ImportError, AttributeError):
-            print("ERROR: No such combiner: {}".format(args.c), file=sys.stderr)
-            raise
+            logger.exception("No such combiner: %s", args.c)
+            exit(1)
     if args.e and args.f:
-        print(
-            "WARNING: Both -e and -f options are specified. -e option will be ignored",
-            file=sys.stderr,
+        logging.warning(
+            "Both -e and -f options are specified. -e option will be ignored"
         )
     if args.f:
         try:
@@ -132,19 +131,14 @@ def main(args=None) -> None:
                 sql = f.read()
             print(LineageRunner(sql, combiner=combiner, verbose=args.verbose))
         except IsADirectoryError:
-            print(
-                "ERROR: {} is a directory".format(args.f), file=sys.stderr,
-            )
-            raise
+            logger.exception("%s is a directory", args.f)
+            exit(1)
         except FileNotFoundError:
-            print("ERROR: No such file: {}".format(args.f), file=sys.stderr)
-            raise
+            logger.exception("No such file: %s", args.f)
+            exit(1)
         except PermissionError:
-            print(
-                "ERROR: Permission denied when reading file '{}'".format(args.f),
-                file=sys.stderr,
-            )
-            raise
+            logger.exception("Permission denied when reading file '%s'", args.f)
+            exit(1)
     elif args.e:
         print(LineageRunner(args.e, combiner=combiner, verbose=args.verbose))
     else:

Replace print to stderr with logging
diff --git a/tests/test_runner.py b/tests/test_runner.py
index 388105e..08e14d5 100644
--- a/tests/test_runner.py
+++ b/tests/test_runner.py
@@ -20,16 +20,18 @@ insert overwrite table tab3 select * from tab2""",
 
 
 def test_combiner_exception():
-    with pytest.raises(ValueError):
-        main(["-c", "malformatcombiner"])
-    with pytest.raises(ImportError):
-        main(["-c", "nonexist_package.nonexist_combiner"])
-    with pytest.raises(AttributeError):
-        main(["-c", "sqllineage.combiners.nonexist_combiner"])
+    for args in (
+        ["-c", "malformatcombiner"],
+        ["-c", "nonexist_package.nonexist_combiner"],
+        ["-c", "sqllineage.combiners.nonexist_combiner"],
+    ):
+        with pytest.raises(SystemExit) as e:
+            main(args)
+        assert e.value.code == 1
 
 
 def test_file_exception():
-    with pytest.raises(IsADirectoryError):
-        main(["-f", str(pathlib.Path().absolute())])
-    with pytest.raises(FileNotFoundError):
-        main(["-f", "nonexist_file"])
+    for args in (["-f", str(pathlib.Path().absolute())], ["-f", "nonexist_file"]):
+        with pytest.raises(SystemExit) as e:
+            main(args)
+        assert e.value.code == 1

pip install -e .[all]
parse_log_pytest
requirements.txt
pytest pytest-cov flake8 black bandit mypy
apt-get update apt-get install -y graphviz libgraphviz-dev
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
bandit==1.8.3
black==25.1.0
click==8.1.8
coverage==7.8.0
exceptiongroup==1.2.2
flake8==7.2.0
iniconfig==2.1.0
markdown-it-py==3.0.0
mccabe==0.7.0
mdurl==0.1.2
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
pathspec==0.12.1
pbr==6.1.1
platformdirs==4.3.7
pluggy==1.5.0
pycodestyle==2.13.0
pyflakes==3.3.1
Pygments==2.19.1
pytest==8.3.5
pytest-cov==6.0.0
PyYAML==6.0.2
rich==14.0.0
-e git+https://github.com/reata/sqllineage.git@9c6a28f8baccb08502e93e3f6708443ac3b9b5e1#egg=sqllineage
sqlparse==0.3.1
stevedore==5.4.1
tomli==2.2.1
typing_extensions==4.13.0

name: sqllineage
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
  - 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
  - pip=25.0=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
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - bandit==1.8.3
      - black==25.1.0
      - click==8.1.8
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - iniconfig==2.1.0
      - markdown-it-py==3.0.0
      - mccabe==0.7.0
      - mdurl==0.1.2
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - pathspec==0.12.1
      - pbr==6.1.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pyyaml==6.0.2
      - rich==14.0.0
      - sqlparse==0.3.1
      - stevedore==5.4.1
      - tomli==2.2.1
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/sqllineage

tests/test_runner.py::test_combiner_exception tests/test_runner.py::test_file_exception
tests/test_runner.py::test_dummy
docs/conf.py sqllineage/runner.py sqllineage/__init__.py setup.py
docs/conf.py sqllineage/runner.py sqllineage/__init__.py setup.py