Skip to content

Add Other Logging Implementations #858

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion launch/launch/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
from .for_loop import ForLoop
from .group_action import GroupAction
from .include_launch_description import IncludeLaunchDescription
from .log_info import LogInfo
from .log import Log
from .log import LogDebug
from .log import LogError
from .log import LogInfo
from .log import LogWarning
from .opaque_coroutine import OpaqueCoroutine
from .opaque_function import OpaqueFunction
from .pop_environment import PopEnvironment
Expand Down Expand Up @@ -51,7 +55,11 @@
'ForLoop',
'GroupAction',
'IncludeLaunchDescription',
'Log',
'LogDebug',
'LogError',
'LogInfo',
'LogWarning',
'OpaqueCoroutine',
'OpaqueFunction',
'PopEnvironment',
Expand Down
8 changes: 4 additions & 4 deletions launch/launch/actions/for_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def generate_launch_description():
<launch>
<arg name="robots" default="{name: 'robotA', id: 1};{name: 'robotB', id: 2}" />
<for_each values="$(var robots)" >
<log message="'$(for-var name)' id=$(for-var id)" />
<log_info message="'$(for-var name)' id=$(for-var id)" />
</for_each>
</launch>

Expand All @@ -104,7 +104,7 @@ def generate_launch_description():
- for_each:
iter: $(var robots)
children:
- log:
- log_info:
message: "'$(for-var name)' id=$(for-var id)"

The above examples would ouput the following log messages by default:
Expand Down Expand Up @@ -284,7 +284,7 @@ def generate_launch_description():
<launch>
<arg name="num" default="2" />
<for len="$(var num)" name="i" >
<log message="i=$(index i)" />
<log_info message="i=$(index i)" />
</for>
</launch>

Expand All @@ -298,7 +298,7 @@ def generate_launch_description():
len: $(var num)
name: i
children:
- log:
- log_info:
message: i=$(index i)

The above examples would ouput the following log messages by default:
Expand Down
133 changes: 133 additions & 0 deletions launch/launch/actions/log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Copyright 2025 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Module for the Log action."""

import logging
from typing import List
import warnings

import launch.logging

from ..action import Action
from ..frontend import Entity
from ..frontend import expose_action
from ..frontend import Parser # noqa: F401
from ..launch_context import LaunchContext
from ..some_substitutions_type import SomeSubstitutionsType
from ..substitution import Substitution
from ..utilities import normalize_to_list_of_substitutions


@expose_action('log')
class Log(Action):
"""Action that logs a message when executed."""

def __init__(self, *, msg: SomeSubstitutionsType,
level: SomeSubstitutionsType, **kwargs):
"""Create a Log action."""
super().__init__(**kwargs)

self.__msg = normalize_to_list_of_substitutions(msg)
self.__level = normalize_to_list_of_substitutions(level)
self.__logger = launch.logging.get_logger('launch.user')

@classmethod
def parse(
cls,
entity: Entity,
parser: 'Parser'
):
"""Parse `log` tag."""
_, kwargs = super().parse(entity, parser)
kwargs['msg'] = parser.parse_substitution(entity.get_attr('message'))

# Check if still using old log action
level = entity.get_attr('level', optional=True)
# TODO: Remove optional level for Release after L-turtle release
if level is None:
warnings.warn(
'The action log now expects a log level.'
' Either provide one or switch to using the log_info action',
stacklevel=2)
level = 'INFO'

kwargs['level'] = parser.parse_substitution(level)
return cls, kwargs

@property
def msg(self) -> List[Substitution]:
"""Getter for self.__msg."""
return self.__msg

@property
def level(self) -> List[Substitution]:
"""Getter for self.__level."""
return self.__level

def execute(self, context: LaunchContext) -> None:
"""Execute the action."""
level_sub = ''.join([context.perform_substitution(sub)
for sub in self.level]).upper()

level_map = logging.getLevelNamesMapping()
if level_sub not in level_map:
raise KeyError(f"Invalid log level '{level_sub}', expected: {level_map.keys()}")

level_int = level_map[level_sub]

self.__logger.log(level_int,
''.join([context.perform_substitution(sub) for sub in self.msg])
)
return None


@expose_action('log_info')
class LogInfo(Log):
"""Action that logs a message with level INFO when executed."""

def __init__(self, *, msg: SomeSubstitutionsType, **kwargs):
"""Create a LogInfo action."""
kwargs.pop('level', None)
super().__init__(msg=msg, level='INFO', **kwargs)


@expose_action('log_warning')
class LogWarning(Log):
"""Action that logs a message with level WARNING when executed."""

def __init__(self, *, msg: SomeSubstitutionsType, **kwargs):
"""Create a LogWarning action."""
kwargs.pop('level', None)
super().__init__(msg=msg, level='WARNING', **kwargs)


@expose_action('log_debug')
class LogDebug(Log):
"""Action that logs a message with level DEBUG when executed."""

def __init__(self, *, msg: SomeSubstitutionsType, **kwargs):
"""Create a LogDebug action."""
kwargs.pop('level', None)
super().__init__(msg=msg, level='DEBUG', **kwargs)


@expose_action('log_error')
class LogError(Log):
"""Action that logs a message with level ERROR when executed."""

def __init__(self, *, msg: SomeSubstitutionsType, **kwargs):
"""Create a LogError action."""
kwargs.pop('level', None)
super().__init__(msg=msg, level='ERROR', **kwargs)
49 changes: 4 additions & 45 deletions launch/launch/actions/log_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,9 @@

"""Module for the LogInfo action."""

from typing import List
import warnings

import launch.logging
from .log import LogInfo as LogInfo # noqa: F401

from ..action import Action
from ..frontend import Entity
from ..frontend import expose_action
from ..frontend import Parser # noqa: F401
from ..launch_context import LaunchContext
from ..some_substitutions_type import SomeSubstitutionsType
from ..substitution import Substitution
from ..utilities import normalize_to_list_of_substitutions


@expose_action('log')
class LogInfo(Action):
"""Action that logs a message when executed."""

def __init__(self, *, msg: SomeSubstitutionsType, **kwargs):
"""Create a LogInfo action."""
super().__init__(**kwargs)

self.__msg = normalize_to_list_of_substitutions(msg)
self.__logger = launch.logging.get_logger('launch.user')

@classmethod
def parse(
cls,
entity: Entity,
parser: 'Parser'
):
"""Parse `log` tag."""
_, kwargs = super().parse(entity, parser)
kwargs['msg'] = parser.parse_substitution(entity.get_attr('message'))
return cls, kwargs

@property
def msg(self) -> List[Substitution]:
"""Getter for self.__msg."""
return self.__msg

def execute(self, context: LaunchContext) -> None:
"""Execute the action."""
self.__logger.info(
''.join([context.perform_substitution(sub) for sub in self.msg])
)
return None
# TODO: Remove log_info.py for Release after L-turtle release
warnings.warn('importing from log_info.py is deprecated; import LogInfo from log.py instead.')
92 changes: 92 additions & 0 deletions launch/test/launch/actions/test_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright 2020 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for the Log action classes."""

from launch import LaunchContext
from launch.actions import Log
from launch.actions import LogDebug
from launch.actions import LogError
from launch.actions import LogInfo
from launch.actions import LogWarning
from launch.utilities import perform_substitutions

import pytest


def test_log_constructors():
"""Test the constructors for Log classes."""
Log(msg='', level='INFO')
Log(msg='', level='DEBUG')
Log(msg='foo', level='WARNING')
Log(msg=['foo', 'bar', 'baz'], level='ERROR')

LogDebug(msg='')
LogDebug(msg='foo')
LogDebug(msg=['foo', 'bar', 'baz'])

LogError(msg='')
LogError(msg='foo')
LogError(msg=['foo', 'bar', 'baz'])

LogInfo(msg='')
LogInfo(msg='foo')
LogInfo(msg=['foo', 'bar', 'baz'])

LogWarning(msg='')
LogWarning(msg='foo')
LogWarning(msg=['foo', 'bar', 'baz'])


def test_log_methods():
"""Test the methods of the LogInfo class."""
launch_context = LaunchContext()

log = Log(msg='', level='INFO')
assert perform_substitutions(launch_context, log.msg) == ''

log = Log(msg='foo', level='INFO')
assert perform_substitutions(launch_context, log.msg) == 'foo'

log = Log(msg=['foo', 'bar', 'baz'], level='INFO')
assert perform_substitutions(launch_context, log.msg) == 'foobarbaz'

log = Log(msg=['foo', 'bar', 'baz'], level=['I', 'N', 'F', 'O'])
assert perform_substitutions(launch_context, log.level) == 'INFO'

log = LogDebug(msg='')
assert perform_substitutions(launch_context, log.level) == 'DEBUG'

log = LogError(msg='')
assert perform_substitutions(launch_context, log.level) == 'ERROR'

log = LogInfo(msg='')
assert perform_substitutions(launch_context, log.level) == 'INFO'

log = LogWarning(msg='')
assert perform_substitutions(launch_context, log.level) == 'WARNING'


def test_log_execute():
"""Test the execute (or visit) of the LogInfo class."""
log = Log(msg='foo', level='ERROR')
launch_context = LaunchContext()
assert log.visit(launch_context) is None


def test_log_level_error():
"""Checks for error message to be raised given invalid level."""
launch_context = LaunchContext()
with pytest.raises(KeyError, match=r'Invalid log level*'):
Log(msg='foo', level='foo').execute(launch_context)
47 changes: 0 additions & 47 deletions launch/test/launch/actions/test_log_info.py

This file was deleted.

Loading