Skip to content

Commit f9c5530

Browse files
authored
Support loading Python agent through sw-python CLI (#156)
1 parent 97bf7fd commit f9c5530

File tree

16 files changed

+556
-4
lines changed

16 files changed

+556
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Feature:
66
- Support collecting and reporting logs to backend (#147)
7+
- Add a new `sw-python` CLI that enables agent non-intrusive integration (#156)
78

89
- New plugins:
910
- Falcon Plugin (#146)

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ in SkyWalking backend, the port of gRPC protocol is `11800`, and the port of HTT
4949
you should configure `collector_address` (or environment variable `SW_AGENT_COLLECTOR_BACKEND_SERVICES`)
5050
according to the protocol you want.
5151

52+
### Non-intrusive integration (CLI)
53+
54+
SkyWalking Python agent supports running and attaching to your awesome applications without adding any code to your
55+
project. The package installation comes with a new command-line script named `sw-python`, which you can use to run your Python-based
56+
applications and programs in the following manner `sw-python run python abc.py` or `sw-python run program arg0 arg1`
57+
58+
Please do read the [CLI Guide](docs/CLI.md) for a detailed introduction to this new feature before using in production.
59+
60+
You can always fall back to our traditional way of integration as introduced below,
61+
which is by importing SkyWalking into your project and starting the agent.
62+
5263
### Report data via gRPC protocol (Default)
5364

5465
For example, if you want to use gRPC protocol to report data, configure `collector_address`
@@ -97,6 +108,7 @@ Alternatively, you can also pass the configurations via environment variables (s
97108
All supported environment variables can be found [here](docs/EnvVars.md)
98109

99110
## Report logs with Python Agent
111+
100112
The Python agent is capable of reporting collected logs to the backend(SkyWalking OAP), enabling Log & Trace Correlation.
101113

102114
Please refer to the [Log Reporter Doc](docs/LogReporter.md) for a detailed guide.

docs/CLI.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# SkyWalking Python Agent Command-Line Interface(CLI)
2+
3+
In earlier releases than 0.7.0, you would at least need to add the following lines to your applications to get the agent attached and running.
4+
5+
```python
6+
from skywalking import agent
7+
agent.start()
8+
```
9+
10+
11+
Now the SkyWalking Python agent implements a command-line interface that can be utilized to attach the agent to your
12+
awesome applications during deployment **without changing any application code**,
13+
just like the [SkyWalking Java Agent](https://github.com/apache/skywalking).
14+
15+
## Usage
16+
17+
Upon successful [installation of the SkyWalking Python agent via pip](../README.md#install),
18+
a command-line script `sw-python` is installed in your environment(virtual env preferred).
19+
20+
### The `run` option
21+
22+
Currently, the `sw-python` CLI provides a `run` option, which you can use to execute your applications
23+
(either begins with the `python` command or Python-based programs like `gunicorn` on your path)
24+
just like you invoke them normally, plus a prefix, the following example demonstrates the usage.
25+
26+
If your previous command to run your gunicorn application is:
27+
28+
`gunicorn app.wsgi`
29+
30+
Please change it to:
31+
32+
`sw-python run gunicorn app.wsgi`
33+
34+
The SkyWalking Python agent will startup along with your application shortly.
35+
36+
Note that the command does work with multiprocessing and subprocess as long as the `PYTHONPATH` is inherited,
37+
please configure the environment variables configuration accordingly based on the general documentation.
38+
39+
When executing commands with `sw-python run command`, your command's Python interpreter will pick up the SkyWalking loader module.
40+
41+
It is not safe to attach SkyWalking Agent to those commands that resides in another Python installation
42+
because incompatible Python versions and mismatched SkyWalking versions can cause problems.
43+
Therefore, any attempt to pass a command that uses a different Python interpreter/ environment will not bring up
44+
SkyWalking Python Agent even if another SkyWalking Python agent is installed there(no matter the version),
45+
and will force exit with an error message indicating the reasoning.
46+
47+
#### Disabling child processes from starting new agents
48+
49+
Sometimes you don't actually need the agent to monitor anything in a child process.
50+
51+
If you do not need the agent to get loaded for application child processes, you can turn off the behavior by setting an environment variable.
52+
53+
`SW_PYTHON_BOOTSTRAP_PROPAGATE` to `False`
54+
55+
Note the auto bootstrap depends on the environment inherited by child processes,
56+
thus prepending a new sitecustomize path to or removing the loader path from the `PYTHONPATH` could prevent the agent from loading in a child process.
57+
58+
### Configuring the agent
59+
60+
You would normally want to provide additional configurations other than the default ones.
61+
62+
#### Through environment variables
63+
64+
The currently supported method is to provide the environment variables listed
65+
in [EnvVars Doc](EnvVars.md) as instructed in the [README](../README.md).
66+
67+
#### Through a sw-config.yaml
68+
69+
Currently, only environment variable configuration is supported; an optional `yaml` configuration is to be implemented.
70+
71+
### Enabling CLI DEBUG mode
72+
73+
Note the CLI is a new feature that manipulates the Python interpreter bootstrap behaviour, there could be unsupported cases.
74+
75+
If you encounter unexpected problems, please turn on the DEBUG mode by adding the `-d` or `--debug` flag to your `sw-python` command, as shown below.
76+
77+
From: `sw-python run command`
78+
79+
To: `sw-python -d run command`
80+
81+
Please attach the debug logs to the [SkyWalking Issues](https://github.com/apache/skywalking/issues) section if you believe it is a bug,
82+
[idea discussions](https://github.com/apache/skywalking/discussions) and [pull requests](https://github.com/apache/skywalking-python/pulls) are always welcomed.
83+
84+
#### Known limitations
85+
86+
1. The CLI may not work properly with arguments that involve double quotation marks in some shells.
87+
2. The CLI and bootstrapper stdout logs could get messy in Windows shells.

docs/EnvVars.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Environment Variable | Description | Default
4444
| `SW_AGENT_LOG_REPORTER_FORMATTED` | If `True`, the log reporter will transmit the logs as formatted. Otherwise, puts logRecord.msg and logRecord.args into message content and tags(`argument.n`), respectively. Along with an `exception` tag if an exception was raised. | `True` |
4545
| `SW_AGENT_LOG_REPORTER_LAYOUT` | The log reporter formats the logRecord message based on the layout given. | `%(asctime)s [%(threadName)s] %(levelname)s %(name)s - %(message)s` |
4646
| `SW_AGENT_CAUSE_EXCEPTION_DEPTH` | This config limits agent to report up to `limit` stacktrace, please refer to [Python traceback](https://docs.python.org/3/library/traceback.html#traceback.print_tb) for more explanations. | `5` |
47+
| `SW_PYTHON_BOOTSTRAP_PROPAGATE`| This config controls the child process agent bootstrap behavior in `sw-python` CLI, if set to `False`, a valid child process will not boot up a SkyWalking Agent. Please refer to the [CLI Guide](CLI.md) for details. | unset |

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,10 @@
7070
"Programming Language :: Python :: 3.9",
7171

7272
"Topic :: Software Development",
73-
]
73+
],
74+
entry_points={
75+
"console_scripts": [
76+
'sw-python = skywalking.bootstrap.cli.sw_python:start'
77+
]
78+
},
7479
)

skywalking/bootstrap/__init__.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
""" This sub-package is for the convenience of deployment and automation
19+
A CLI for running Python scripts and programs with SkyWalking Python Agent automatically attached.
20+
`loader/sitecustomize.py` is invoked by the Python interpreter at startup.
21+
"""
22+
23+
import logging
24+
25+
26+
def get_cli_logger():
27+
""" A logger used by sw-python CLI """
28+
logger = logging.getLogger('skywalking-cli')
29+
ch = logging.StreamHandler()
30+
formatter = logging.Formatter('%(name)s [%(levelname)s] %(message)s')
31+
ch.setFormatter(formatter)
32+
logger.addHandler(ch)
33+
logger.propagate = False
34+
35+
return logger
36+
37+
38+
cli_logger = get_cli_logger()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
19+
class SWRunnerFailure(Exception):
20+
""" Exception runner fails to execute given user command """
21+
pass
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
""" This module is installed during package setup """
19+
import argparse
20+
import logging
21+
22+
from skywalking.bootstrap import cli_logger
23+
from skywalking.bootstrap.cli import SWRunnerFailure
24+
from skywalking.bootstrap.cli.utility import runner
25+
26+
_options = {
27+
'run': runner,
28+
}
29+
30+
31+
def start() -> None:
32+
""" Entry point of CLI """
33+
parser = argparse.ArgumentParser(description='SkyWalking Python Agent CLI',
34+
epilog='Append your command, with SkyWalking agent attached for you automatically',
35+
allow_abbrev=False)
36+
37+
parser.add_argument('option', help='CLI options, now only supports `run`, for help please type `sw-python -h` '
38+
'or refer to the CLI documentation',
39+
choices=list(_options.keys()))
40+
41+
# TODO support parsing optional sw_config.yaml
42+
# parser.add_argument('-config', nargs='?', type=argparse.FileType('r'),
43+
# help='Optionally takes a sw_python.yaml config file')
44+
45+
# The command arg compress all remaining args into itself
46+
parser.add_argument('command', help='Your original commands e.g. gunicorn app.wsgi',
47+
nargs=argparse.REMAINDER, metavar='command')
48+
49+
parser.add_argument('-d', '--debug', help='Print CLI debug logs to stdout', action='store_true')
50+
51+
# To handle cases with flags and positional args in user commands
52+
args = parser.parse_args() # type: argparse.Namespace
53+
54+
cli_logger.setLevel(logging.DEBUG if args.debug else logging.INFO)
55+
56+
cli_logger.debug("Args received {}".format(args))
57+
58+
if not args.command:
59+
cli_logger.error("Command is not provided, please type `sw-python -h` for the list of command line arguments")
60+
return
61+
try:
62+
dispatch(args)
63+
except SWRunnerFailure:
64+
cli_logger.debug('Failed to run the given user application command `{}`, '
65+
'please make sure given command is valid.'.
66+
format(' '.join(args.command)))
67+
return
68+
69+
70+
def dispatch(args: argparse.Namespace) -> None:
71+
""" Dispatches parsed args to a worker """
72+
cli_option, actual_command = args.option, args.command
73+
74+
cli_logger.debug("SkyWalking Python agent with CLI option '{}' and command {}".format
75+
(cli_option, actual_command))
76+
77+
# Dispatch actual user application command to runner
78+
_options[cli_option].execute(actual_command)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
""" User application command runner """
19+
import logging
20+
import os
21+
import platform
22+
import sys
23+
from typing import List
24+
25+
from skywalking.bootstrap import cli_logger
26+
from skywalking.bootstrap.cli import SWRunnerFailure
27+
28+
29+
def execute(command: List[str]) -> None:
30+
""" Set up environ and invokes the given command to replace current process """
31+
32+
cli_logger.debug("SkyWalking Python agent `runner` received command {}".format(command))
33+
34+
cli_logger.debug("Adding sitecustomize.py to PYTHONPATH")
35+
36+
from skywalking.bootstrap.loader import __file__ as loader_dir
37+
38+
loader_path = os.path.dirname(loader_dir)
39+
new_path = ""
40+
41+
python_path = os.environ.get('PYTHONPATH')
42+
if python_path: # If there is already a different PYTHONPATH, PREPEND to it as we must get loaded first.
43+
partitioned = python_path.split(os.path.pathsep)
44+
if loader_path not in partitioned: # check if we are already there
45+
new_path = os.path.pathsep.join([loader_path, python_path]) # type: str
46+
47+
# When constructing sys.path PYTHONPATH is always
48+
# before other paths and after interpreter invoker path, which is here or none
49+
os.environ['PYTHONPATH'] = new_path if new_path else loader_path
50+
cli_logger.debug("Updated PYTHONPATH - {}".format(os.environ['PYTHONPATH']))
51+
52+
# Used in sitecustomize to compare command's Python installation with CLI
53+
# If not match, need to stop agent from loading, and kill the process
54+
os.environ['SW_PYTHON_PREFIX'] = os.path.realpath(os.path.normpath(sys.prefix))
55+
os.environ['SW_PYTHON_VERSION'] = platform.python_version()
56+
57+
# Pass down the logger debug setting to the replaced process, need a new logger there
58+
os.environ['SW_PYTHON_CLI_DEBUG_ENABLED'] = 'True' if cli_logger.level == logging.DEBUG else 'False'
59+
60+
try:
61+
cli_logger.debug('New process starting with file - `{}` args - `{}`'.format(command[0], command))
62+
os.execvp(command[0], command)
63+
except OSError:
64+
raise SWRunnerFailure

0 commit comments

Comments
 (0)