Skip to content

Commit ee45e1b

Browse files
author
David Noble
committed
Resolved these outstanding issues:
+ DVPL-3654: splunklib.searchcommands | logging_configuration option is broken Verified with automated unit tests and ad-hoc integration tests + DVPL-3895: splunklib.searchcommands | Exceptions within user code should log full stack trace and exit with non-zero status code Verified with automated unit tests and ad-hoc integration tests Signed-off-by: David Noble <[email protected]>
1 parent 40c6eed commit ee45e1b

File tree

8 files changed

+230
-36
lines changed

8 files changed

+230
-36
lines changed

splunklib/searchcommands/generating_command.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ def _execute(self, operation, reader, writer):
7272
for record in operation():
7373
writer.writerow(record)
7474
except Exception as e:
75-
import traceback
76-
self.logger.error(traceback.format_exc())
75+
from traceback import format_exc
76+
from sys import exit
77+
self.logger.error(format_exc())
78+
exit(1)
7779

7880
def _prepare(self, argv, input_file):
7981
ConfigurationSettings = type(self).ConfigurationSettings

splunklib/searchcommands/logging.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def configure(name, path=None):
7171
7272
"""
7373
app_directory = os.path.dirname(os.getcwd())
74+
7475
if path is None:
7576
probing_path = [
7677
'local/%s.logging.conf' % name,
@@ -82,6 +83,21 @@ def configure(name, path=None):
8283
if os.path.exists(configuration_file):
8384
path = configuration_file
8485
break
86+
elif not os.path.isabs(path):
87+
found = False
88+
for conf in 'local', 'default':
89+
configuration_file = os.path.join(app_directory, conf, path)
90+
if os.path.exists(configuration_file):
91+
path = configuration_file
92+
found = True
93+
break
94+
if not found:
95+
raise ValueError(
96+
'Logging configuration file "%s" not found in local or default '
97+
'directory' % path)
98+
elif not os.path.exists(path):
99+
raise ValueError('Logging configuration file "%s" not found')
100+
85101
if path is not None:
86102
working_directory = os.getcwd()
87103
os.chdir(app_directory)
@@ -90,5 +106,6 @@ def configure(name, path=None):
90106
fileConfig(path)
91107
finally:
92108
os.chdir(working_directory)
109+
93110
logger = getLogger(name)
94111
return logger, path

splunklib/searchcommands/reporting_command.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ def _execute(self, operation, reader, writer):
6969
for record in operation(SearchCommand.records(reader)):
7070
writer.writerow(record)
7171
except Exception as e:
72-
import traceback
73-
self.logger.error(traceback.format_exc())
72+
from traceback import format_exc
73+
from sys import exit
74+
self.logger.error(format_exc())
75+
exit(1)
7476

7577
def _prepare(self, argv, input_file):
7678
if len(argv) >= 3 and argv[2] == '__map__':

splunklib/searchcommands/search_command.py

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,24 @@ def __str__(self):
6666
text = ' '.join([value for value in values if len(value) > 0])
6767
return text
6868

69-
# Disabled in splunk-sdk-python-1.2.0 due to known issues
70-
#
71-
# #region Options
72-
#
73-
# @Option
74-
# def logging_configuration(self):
75-
# """ **Syntax:** logging_configuration=<path>
76-
#
77-
# **Description:** Loads an alternative logging configuration file for
78-
# a command invocation. The logging configuration file must be in Python
79-
# ConfigParser-format. Path names are relative to the app root directory.
80-
#
81-
# """
82-
# return self._logging_configuration
83-
#
84-
# @logging_configuration.setter
85-
# def logging_configuration(self, value):
86-
# self.logger, self._logging_configuration = logging.configure(
87-
# type(self).__name__, value)
88-
# return
69+
#region Options
70+
71+
@Option
72+
def logging_configuration(self):
73+
""" **Syntax:** logging_configuration=<path>
74+
75+
**Description:** Loads an alternative logging configuration file for
76+
a command invocation. The logging configuration file must be in Python
77+
ConfigParser-format. Path names are relative to the app root directory.
78+
79+
"""
80+
return self._logging_configuration
81+
82+
@logging_configuration.setter
83+
def logging_configuration(self, value):
84+
self.logger, self._logging_configuration = logging.configure(
85+
type(self).__name__, value)
86+
return
8987

9088
@Option
9189
def logging_level(self):
@@ -112,7 +110,7 @@ def logging_level(self, value):
112110
messages header for this command invocation. Defaults to `false`.
113111
114112
''', default=False, validate=Boolean())
115-
#
113+
116114
# #endregion
117115

118116
#region Properties
@@ -177,10 +175,11 @@ def process(self, args=argv, input_file=stdin, output_file=stdout):
177175
try:
178176
self.parser.parse(args, self)
179177
except (SyntaxError, ValueError) as e:
178+
from sys import exit
180179
self.messages.append("error_message", e)
181180
self.messages.write(output_file)
182181
self.logger.error(e)
183-
return
182+
exit(1)
184183

185184
self._configuration = ConfigurationSettings(self)
186185

@@ -193,13 +192,17 @@ def process(self, args=argv, input_file=stdin, output_file=stdout):
193192
self._execute(operation, reader, writer)
194193

195194
else:
195+
file_name = path.basename(args[0])
196196
message = (
197-
'Static configuration is unsupported. Please configure this '
198-
'command as follows in default/commands.conf:\n\n'
199-
'[%s]\n'
200-
'filename = %s\n'
201-
'supports_getinfo = true' %
202-
(type(self).__name__, path.basename(argv[0])))
197+
'Command {0} appears to be statically configured and static '
198+
'configuration is unsupported by splunklib.searchcommands. '
199+
'Please ensure that default/commands.conf contains this '
200+
'stanza: '
201+
'[{0}] | '
202+
'filename = {1} | '
203+
'supports_getinfo = true | '
204+
'supports_rawargs = true | '
205+
'outputheader = true'.format(type(self).name, file_name))
203206
self.messages.append('error_message', message)
204207
self.messages.write(output_file)
205208
self.logger.error(message)

splunklib/searchcommands/streaming_command.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ def _execute(self, operation, reader, writer):
6767
for record in operation(SearchCommand.records(reader)):
6868
writer.writerow(record)
6969
except Exception as e:
70-
import traceback
71-
self.logger.error(traceback.format_exc())
70+
from traceback import format_exc
71+
from sys import exit
72+
self.logger.error(format_exc())
73+
exit(1)
7274

7375
def _prepare(self, argv, input_file):
7476
ConfigurationSettings = type(self).ConfigurationSettings
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[loggers]
2+
keys = root, SearchCommand
3+
4+
[logger_root]
5+
level = DEBUG ; Default: WARNING
6+
handlers = stderr ; Default: stderr
7+
8+
[logger_SearchCommand]
9+
qualname = SearchCommand
10+
level = NOTSET ; Default: WARNING
11+
handlers = stderr ; Default: stderr
12+
13+
[handlers]
14+
keys=stderr
15+
16+
[handler_stderr]
17+
class = logging.StreamHandler
18+
level = NOTSET
19+
args = (sys.stderr,)
20+
formatter = search_command
21+
22+
[formatters]
23+
keys = search_command
24+
25+
[formatter_search_command]
26+
format=%(levelname)s:%(module)s: %(message)s
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2011-2013 Splunk, Inc.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# 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, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
try:
18+
import unittest2 as unittest
19+
except ImportError:
20+
import unittest
21+
22+
from splunklib.searchcommands import Configuration, StreamingCommand
23+
from cStringIO import StringIO
24+
25+
26+
@Configuration()
27+
class SearchCommand(StreamingCommand):
28+
29+
def stream(self, records):
30+
value = 0
31+
for record in records:
32+
action = record['Action']
33+
if action == 'raise_error':
34+
raise RuntimeError('Testing')
35+
yield {'Data': value}
36+
value += 1
37+
return
38+
39+
class TestSearchCommandsCommand(unittest.TestCase):
40+
41+
def setUp(self):
42+
super(TestSearchCommandsCommand, self).setUp()
43+
return
44+
45+
def test_process(self):
46+
47+
# Command.process should complain if supports_getinfo == False
48+
# We support dynamic configuration, not static
49+
50+
expected = \
51+
'error_message=Command search appears to be statically configured and static configuration is unsupported by splunklib.searchcommands. Please ensure that default/commands.conf contains this stanza: [search] | filename = foo.py | supports_getinfo = true | supports_rawargs = true | outputheader = true\r\n' \
52+
'\r\n'
53+
54+
command = SearchCommand()
55+
result = StringIO()
56+
command.process(['foo.py'], output_file=result)
57+
result.reset()
58+
observed = result.read()
59+
self.assertEqual(observed, expected)
60+
61+
# Command.process should return configuration settings on Getinfo probe
62+
63+
expected = \
64+
'\r\n' \
65+
'changes_colorder,clear_required_fields,enableheader,generating,local,maxinputs,needs_empty_results,outputheader,overrides_timeorder,passauth,perf_warn_limit,required_fields,requires_srinfo,retainsevents,run_in_preview,stderr_dest,streaming,supports_multivalues,supports_rawargs,__mv_changes_colorder,__mv_clear_required_fields,__mv_enableheader,__mv_generating,__mv_local,__mv_maxinputs,__mv_needs_empty_results,__mv_outputheader,__mv_overrides_timeorder,__mv_passauth,__mv_perf_warn_limit,__mv_required_fields,__mv_requires_srinfo,__mv_retainsevents,__mv_run_in_preview,__mv_stderr_dest,__mv_streaming,__mv_supports_multivalues,__mv_supports_rawargs\r\n' \
66+
'1,0,1,0,0,0,1,1,0,0,0,,0,1,1,log,1,1,1,,,,,,,,,,,,,,,,,,,\r\n'
67+
68+
command = SearchCommand()
69+
result = StringIO()
70+
command.process(['foo.py', '__GETINFO__'], output_file=result)
71+
result.reset()
72+
observed = result.read()
73+
self.assertEqual(observed, expected)
74+
75+
# Command.process should produce an error record on parser errors, if
76+
# invoked to get configuration settings
77+
78+
expected = \
79+
'\r\n' \
80+
'ERROR,__mv_ERROR\r\n' \
81+
'Unrecognized option: undefined_option = value,\r\n'
82+
83+
command = SearchCommand()
84+
result = StringIO()
85+
command.process(['foo.py', '__GETINFO__', 'undefined_option=value'], output_file=result)
86+
result.reset()
87+
observed = result.read()
88+
self.assertEqual(observed, expected)
89+
90+
# Command.process should produce an error message and exit on parser
91+
# errors, if invoked to execute
92+
93+
expected = \
94+
'error_message=Unrecognized option: undefined_option = value\r\n' \
95+
'\r\n'
96+
97+
command = SearchCommand()
98+
result = StringIO()
99+
100+
try:
101+
command.process(args=['foo.py', '__EXECUTE__', 'undefined_option=value'], input_file=StringIO('\r\n'), output_file=result)
102+
except SystemExit as e:
103+
result.reset()
104+
observed = result.read()
105+
self.assertEqual(e.code != 0)
106+
self.assertEqual(observed, expected)
107+
except BaseException as e:
108+
self.fail("Expected SystemExit, but caught %s" % type(e))
109+
else:
110+
self.fail("Expected SystemExit, but no exception was raised")
111+
112+
# Command.process should exit on processing exceptions
113+
114+
command = SearchCommand()
115+
result = StringIO()
116+
117+
try:
118+
command.process(args=['foo.py', '__EXECUTE__'], input_file=StringIO('\r\nAction\r\nraise_error'), output_file=result)
119+
except SystemExit as e:
120+
result.reset()
121+
observed = result.read()
122+
self.assertEqual(e.code != 0)
123+
self.assertEqual(observed, expected)
124+
except BaseException as e:
125+
self.fail("Expected SystemExit, but caught %s" % type(e))
126+
else:
127+
self.fail("Expected SystemExit, but no exception was raised")
128+
129+
return

tests/test_searchcommands_decorators.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from splunklib.searchcommands import Configuration, StreamingCommand
2424

2525
import logging
26+
import os
2627

2728
@Configuration()
2829
class SearchCommand(StreamingCommand):
@@ -59,6 +60,18 @@ def test_builtin_options(self):
5960
command.logging_level = variant
6061
self.assertEquals(command.logging_level, warning if level_name == notset else level_name)
6162

63+
# logging_configuration loads a new logging configuration file relative
64+
# to the app root
65+
66+
command = SearchCommand()
67+
directory = os.getcwd()
68+
os.chdir(os.path.join('searchcommands_data', 'app', 'bin'))
69+
command.logging_configuration = 'logging.conf'
70+
os.chdir(directory)
71+
72+
# logging_configuration loads a new logging configuration file on an
73+
# absolute path
74+
6275
# show_configuration accepts Splunk boolean values
6376

6477
boolean_values = {
@@ -78,8 +91,8 @@ def test_builtin_options(self):
7891
command.show_configuration = value
7992
except ValueError:
8093
pass
81-
except Exception as e:
82-
self.fail('Expected ValueError, but a %s was raised' % type(e))
94+
except BaseException as e:
95+
self.fail('Expected ValueError, but %s was raised' % type(e))
8396
else:
8497
self.fail('Expected ValueError, but show_configuration=%s' % command.show_configuration)
8598

0 commit comments

Comments
 (0)