Skip to content

Commit 71ddbe0

Browse files
authored
Merge pull request #1 from strohganoff/feature/better-logging
Feature/better logging
2 parents 5f3298d + afcb493 commit 71ddbe0

10 files changed

Lines changed: 371 additions & 45 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ reports/
1212
*.pyc
1313
.pytest_cache
1414
__pycache__
15+
.DS_Store
1516

1617
*.txt
1718
*.env

README.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ An **Action** represents a specific functionality in your plugin. You can create
4747
from streamdeck import Action
4848

4949
# Create an action with a unique UUID (from your manifest.json)
50-
my_action = Action(uuid="com.example.myaction")
50+
my_action = Action(uuid="com.example.myplugin.myaction")
5151
```
5252

5353
### Registering Event Handlers
@@ -64,6 +64,47 @@ def handle_will_appear(event):
6464
print("Will Appear event received:", event)
6565
```
6666

67+
68+
### Writing Logs
69+
70+
For convenience, a logger is configured with the same name as the last part of the Action's UUID, so you can simply call logging.getLogger(<name>) with the appropriate name to get the already-configured logger that writes to a rotating file. The log file is located in the Stream Deck user log directory.
71+
72+
When creating actions in your plugin, you can configure logging using the logger name that matches the last part of your Action's UUID. For example, consider the following code:
73+
74+
```python
75+
import logging
76+
from streamdeck import Action
77+
78+
logger = logging.getLogger("myaction")
79+
80+
my_action = Action(uuid="com.example.mytestplugin.myaction")
81+
```
82+
83+
Here, the logger name "myaction" matches the last part of the UUID passed in to instantiate the Action ("com.strohganoff.mytestplugin.myaction").
84+
85+
#### Configuring your own Loggers
86+
87+
Loggers can also be easily configured using provided utility functions, allowing for flexibility. If custom logging configurations are prefered over the automatic method shown above, you can use the following functions:
88+
89+
`configure_streamdeck_logger`: Configures a logger for the Stream Deck plugin with a rotating file handler that writes logs to a centralized location.
90+
91+
`configure_local_logger`: Configures a logger for a Stream Deck plugin that writes logs to a local data directory, allowing for plugin-specific logging.
92+
93+
These functions can be used to set up the logging behavior you desire, depending on whether you want the logs to be centralized or specific to each plugin.
94+
95+
For example:
96+
```python
97+
import logging
98+
from streamdeck.utils.logging import configure_streamdeck_logger
99+
100+
configure_streamdeck_logger(name="myaction", plugin_uuid="com.example.mytestplugin")
101+
102+
logger = logging.getLogger("myaction")
103+
```
104+
105+
Using the above code, you can ensure that logs from your action are properly collected and managed, helping you debug and monitor the behavior of your Stream Deck plugins.
106+
107+
67108
### Running the Plugin
68109

69110
Once the plugin's actions and their handlers have been defined, very little else is needed to get this code running. With this library installed, the streamdeck CLI command will handle the setup, loading of action scripts, and running of the plugin automatically, making it much easier to manage.
@@ -117,21 +158,22 @@ Below is a complete example that creates a plugin with a single action. The acti
117158

118159
```python
119160
# main.py
120-
161+
import logging
121162
from streamdeck import Action, PluginManager, events
122163
164+
logger = logging.getLogger("myaction")
165+
123166
# Define your action
124-
my_action = Action(uuid="com.example.myaction")
167+
my_action = Action(uuid="com.example.myplugin.myaction")
125168
126169
# Register event handlers
127170
@my_action.on("keyDown")
128171
def handle_key_down(event):
129-
print("Key Down event received:", event)
172+
logger.debug("Key Down event received:", event)
130173
```
131174

132175
```toml
133176
# pyproject.toml
134-
135177
[tools.streamdeck]
136178
action_scripts = [
137179
"main.py",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
[project]
99
name = "streamdeck-plugin-sdk"
10-
version = "0.2.2"
10+
version = "0.3.0"
1111
description = "Write Streamdeck plugins using Python"
1212
readme = "README.md"
1313
authors = [

streamdeck/__main__.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import importlib.util
44
import json
55
import logging
6-
import os
76
from argparse import ArgumentParser
87
from pathlib import Path
98
from typing import TYPE_CHECKING, cast
@@ -21,6 +20,7 @@
2120
StreamDeckConfigDict,
2221
)
2322
from streamdeck.manager import PluginManager
23+
from streamdeck.utils.logging import configure_streamdeck_logger
2424

2525

2626
if TYPE_CHECKING:
@@ -30,16 +30,7 @@
3030
from typing_extensions import Self # noqa: UP035
3131

3232

33-
logger = logging.getLogger("streamdeck-plugin-sdk")
34-
35-
logger.addHandler(logging.StreamHandler())
36-
37-
# TODO: Add better functionality for setting up logging to save to files in the proper streamdeck log directory.
38-
file_handler = logging.FileHandler(os.path.expanduser("~/plugin.log"))
39-
file_handler.setLevel(logging.DEBUG)
40-
logger.addHandler(file_handler)
41-
42-
logger.info("Starting run...")
33+
logger = logging.getLogger("streamdeck")
4334

4435

4536
def setup_cli() -> ArgumentParser:
@@ -65,14 +56,21 @@ def setup_cli() -> ArgumentParser:
6556

6657
# Options that will always be passed in by the StreamDeck software when running this plugin.
6758
parser.add_argument("-port", dest="port", type=int, help="Port", required=True)
68-
parser.add_argument("-pluginUUID", dest="pluginUUID", type=str, help="pluginUUID", required=True)
69-
parser.add_argument("-registerEvent", dest="registerEvent", type=str, help="registerEvent", required=True)
59+
parser.add_argument(
60+
"-pluginUUID", dest="pluginUUID", type=str, help="pluginUUID", required=True
61+
)
62+
parser.add_argument(
63+
"-registerEvent", dest="registerEvent", type=str, help="registerEvent", required=True
64+
)
7065
parser.add_argument("-info", dest="info", type=str, help="info", required=True)
7166

7267
return parser
7368

7469

75-
def determine_action_scripts(plugin_dir: Path | None, action_scripts: list[str] | None) -> list[str]:
70+
def determine_action_scripts(
71+
plugin_dir: Path | None,
72+
action_scripts: list[str] | None,
73+
) -> list[str]:
7674
"""Determine the action scripts to be loaded based on provided arguments.
7775
7876
plugin_dir and action_scripts cannot both have values -> either only one of them isn't None, or they are both None.
@@ -102,7 +100,6 @@ def determine_action_scripts(plugin_dir: Path | None, action_scripts: list[str]
102100
raise KeyError(msg) from e
103101

104102

105-
106103
def read_streamdeck_config_from_pyproject(plugin_dir: Path) -> StreamDeckConfigDict:
107104
"""Get the streamdeck section from a plugin directory by reading pyproject.toml.
108105
@@ -178,12 +175,12 @@ def _load_module_from_file(filepath: Path) -> ModuleType:
178175
# Create a module specification for a module located at the given filepath.
179176
# A "specification" is an object that contains information about how to load the module, such as its location and loader.
180177
# "module.name" is an arbitrary name used to identify the module internally.
181-
spec: ModuleSpec = importlib.util.spec_from_file_location("module.name", str(filepath)) # type: ignore
178+
spec: ModuleSpec = importlib.util.spec_from_file_location("module.name", str(filepath)) # type: ignore
182179
# Create a new module object from the given specification.
183180
# At this point, the module is created but not yet loaded (i.e. its code hasn't been executed).
184181
module: ModuleType = importlib.util.module_from_spec(spec)
185182
# Load the module by executing its code, making available its functions, classes, and variables.
186-
spec.loader.exec_module(module) # type: ignore
183+
spec.loader.exec_module(module) # type: ignore
187184

188185
return module
189186

@@ -205,6 +202,10 @@ def main():
205202
info = json.loads(args.info)
206203
plugin_uuid = info["plugin"]["uuid"]
207204

205+
# After configuring once here, we can grab the logger in any other module with `logging.getLogger("streamdeck")`, or
206+
# a child logger with `logging.getLogger("streamdeck.mycomponent")`, all with the same handler/formatter configuration.
207+
configure_streamdeck_logger(name="streamdeck", plugin_uuid=plugin_uuid)
208+
208209
action_scripts = determine_action_scripts(
209210
plugin_dir=args.plugin_dir,
210211
action_scripts=args.action_scripts,
@@ -223,6 +224,10 @@ def main():
223224
)
224225

225226
for action in actions:
227+
# Configure a logger for each action, giving it the last part of its uuid as logger name.
228+
action_component_name = action.uuid.split(".")[-1]
229+
configure_streamdeck_logger(name=action_component_name, plugin_uuid=plugin_uuid)
230+
226231
manager.register_action(action)
227232

228233
manager.run()

streamdeck/command_sender.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313

14-
logger = getLogger("streamdeck")
14+
logger = getLogger("streamdeck.command_sender")
1515

1616

1717
class StreamDeckCommandSender:

streamdeck/manager.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
# TODO: Fix this up to push to a log in the apropos directory and filename.
2121
logger = getLogger("streamdeck.manager")
22-
logger.addHandler(logging.StreamHandler())
2322

2423

2524

streamdeck/utils/dirs.py

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
"""This module provides utility functions to get various directory paths related to Elgato Stream Deck plugins and components.
2+
3+
The functions defined here help locate user-specific log directories, local data directories for plugins, and application data directories for Elgato Stream Deck components and plugins.
4+
These paths are useful for accessing log files, unpacked plugin code, and other application data for Stream Deck and its plugins.
5+
6+
Note:
7+
These functions have been tested on macOS only so far, with plans to test on Windows in the future.
8+
"""
19
from __future__ import annotations
210

311
from typing import TYPE_CHECKING
@@ -11,28 +19,76 @@
1119

1220

1321
def streamdeck_log_dir() -> Path:
22+
"""Get the path to the user-specific log directory for Elgato Stream Deck.
23+
24+
The result of this function is the user log directory path that all plugins as well as internal StreamDeck components write to.
25+
26+
Returns:
27+
Path: Path to the Stream Deck log directory.
28+
"""
1429
return platformdirs.user_log_path(appname="ElgatoStreamDeck")
1530

1631

1732
def streamdeck_local_data_dir() -> Path:
33+
"""Get the path to the local user-specific data directory for Elgato Stream Deck.
34+
35+
The result of this function is the directory that contains the unpacked (i.e. unzipped) directories of all of the code of installed plugins.
36+
37+
Returns:
38+
Path: Path to the local data directory for Stream Deck.
39+
"""
1840
return platformdirs.user_data_path("com.elgato.StreamDeck")
1941

2042

21-
def plugin_local_data_dir(plugin_name: str) -> Path:
22-
return streamdeck_local_data_dir() / "Plugins" / plugin_name
43+
def plugin_local_data_dir(plugin_uuid: str) -> Path:
44+
"""Get the path to the local user-specific data directory for a specific Stream Deck plugin.
45+
46+
The result of this function is the directory path for the given plugin's unpacked (i.e. unzipped) code.
47+
48+
Args:
49+
plugin_uuid (str): The UUID of the plugin.
50+
51+
Returns:
52+
Path: Path to the local data directory for the specified plugin.
53+
"""
54+
return streamdeck_local_data_dir() / "Plugins" / f"{plugin_uuid}.sdPlugin"
2355

2456

2557
def streamdeck_application_data_dir() -> Path:
26-
# NOTE: The only directory under this seems to be "QtWebEngine",
27-
# which is a component for embeding web browser functionality into their desktop applications.
28-
return platformdirs.user_data_path('elgato') / "Streamdeck"
58+
"""Get the path to the application-specific data directory for Elgato Stream Deck.
59+
60+
Note:
61+
The only directory under this path seems to be "QtWebEngine", which is a component for embedding web browser
62+
functionality into their desktop applications.
63+
64+
Returns:
65+
Path: Path to the application data directory for Stream Deck.
66+
"""
67+
return platformdirs.user_data_path("elgato") / "Streamdeck"
68+
2969

70+
def plugin_application_data_dir(plugin_uuid: str) -> Path:
71+
"""Get the path to the application-specific data directory for a specific Stream Deck plugin.
3072
31-
def plugin_application_data_dir(plugin_name: str) -> Path:
32-
# NOTE: Not all plugins have a directory here.. A few individual plugins have probably pushed data to this location.
33-
return streamdeck_application_data_dir() / "QtWebEngine" / plugin_name
73+
Note:
74+
Not all plugins have a directory here. Only a few individual plugins have likely pushed data to this location.
75+
76+
Args:
77+
plugin_uuid (str): The UUID of the plugin.
78+
79+
Returns:
80+
Path: Path to the application data directory for the specified plugin.
81+
"""
82+
return streamdeck_application_data_dir() / "QtWebEngine" / plugin_uuid
3483

3584

3685
def elgato_site_data_dir() -> Path:
37-
# NOTE: There doesn't seem to be a "Stream Deck" directory in here, just "Wave Link".
38-
return platformdirs.site_data_path("Elgato")
86+
"""Get the path to the system-wide shared data directory for Elgato products.
87+
88+
Note:
89+
There doesn't seem to be a "Stream Deck" directory in here, only "Wave Link".
90+
91+
Returns:
92+
Path: Path to the shared data directory for Elgato products.
93+
"""
94+
return platformdirs.site_data_path("Elgato")

streamdeck/utils/helper_actions.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@
1212
from streamdeck.models.events import EventBase
1313

1414

15-
16-
logger = getLogger("streamdeck")
17-
18-
19-
def create_logging_action(action_name: str):
15+
def create_logging_action(action_uuid: str) -> Action:
2016
"""Action that logs the event name of every occurring event."""
21-
logging_action = Action(action_name)
17+
logging_action = Action(action_uuid)
18+
19+
action_component_name = action_uuid.split(".")[-1]
20+
logger = getLogger(action_component_name)
2221

2322
def log_event(event_data: EventBase) -> None:
2423
logger.info("Action %s — event %s", logging_action.__class__, event_data.event)
@@ -30,10 +29,12 @@ def log_event(event_data: EventBase) -> None:
3029
return logging_action
3130

3231

33-
34-
def create_file_writing_action(action_name: str, file: TextIOWrapper) -> Action:
32+
def create_file_writing_action(action_uuid: str, file: TextIOWrapper) -> Action:
3533
"""Action that saves the full json of every occurring event."""
36-
file_writing_action = Action(action_name)
34+
file_writing_action = Action(action_uuid)
35+
36+
action_component_name = action_uuid.split(".")[-1]
37+
logger = getLogger(action_component_name)
3738

3839
def write_event(event_data: EventBase) -> None:
3940
logger.info("Action %s — event %s", file_writing_action.__class__, event_data.event)
@@ -46,4 +47,4 @@ def write_event(event_data: EventBase) -> None:
4647
for event_name in available_event_names:
4748
file_writing_action.on(event_name)(write_event)
4849

49-
return file_writing_action
50+
return file_writing_action

0 commit comments

Comments
 (0)