# Copyright 2011-2014 Splunk, 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.
from __future__ import absolute_import
from . search_command_internals import ConfigurationSettingsType
from . streaming_command import StreamingCommand
from . search_command import SearchCommand
from . import csv
class ReportingCommand(SearchCommand):
""" Processes search results and generates a reporting data structure.
Reporting search commands run as either reduce or map/reduce operations. The
reduce part runs on a search head and is responsible for processing a single
chunk of search results to produce the command's reporting data structure.
The map part is called a streaming preop. It feeds the reduce part with
partial results and by default runs on the search head and/or one or more
indexers.
You must implement a :meth:`reduce` method as a generator function that
iterates over a set of event records and yields a reporting data structure.
You may implement a :meth:`map` method as a generator function that iterates
over a set of event records and yields :class:`dict` or :class:`list(dict)`
instances.
**ReportingCommand configuration**
Configure the :meth:`map` operation using a Configuration decorator on your
:meth:`map` method. Configure it like you would a :class:`StreamingCommand`.
Configure the :meth:`reduce` operation using a Configuration decorator on
your :meth:`ReportingCommand` class.
:ivar input_header: :class:`InputHeader`: Collection representing the input
header associated with this command invocation.
:ivar messages: :class:`MessagesHeader`: Collection representing the output
messages header associated with this command invocation.
"""
#region Methods
def map(self, records):
""" Override this method to compute partial results.
You must override this method, if :code:`requires_preop=True`.
"""
self # Turns off ide guidance that method may be static
return NotImplemented
def reduce(self, records):
""" Override this method to produce a reporting data structure.
You must override this method.
"""
raise NotImplementedError('reduce(self, records)')
def _execute(self, operation, reader, writer):
for record in operation(SearchCommand.records(reader)):
writer.writerow(record)
return
def _prepare(self, argv, input_file):
if len(argv) >= 3 and argv[2] == '__map__':
ConfigurationSettings = type(self).map.ConfigurationSettings
operation = self.map
argv = argv[3:]
else:
ConfigurationSettings = type(self).ConfigurationSettings
operation = self.reduce
argv = argv[2:]
if input_file is None:
reader = None
else:
reader = csv.DictReader(input_file)
return ConfigurationSettings, operation, argv, reader
#endregion
#region Types
class ConfigurationSettings(SearchCommand.ConfigurationSettings):
""" Represents the configuration settings for a :code:`ReportingCommand`.
"""
#region Properties
@property
def clear_required_fields(self):
""" Specifies whether `required_fields` are the only fields required
by subsequent commands.
If :const:`True`, :attr:`required_fields` are the *only* fields
required by subsequent commands. If :const:`False`,
:attr:`required_fields` are additive to any fields that may be
required by subsequent commands. In most cases :const:`False` is
appropriate for streaming commands and :const:`True` is appropriate
for reporting commands.
Default: :const:`True`
"""
return type(self)._clear_required_fields
_clear_required_fields = True
@property
def requires_preop(self):
""" Indicates whether :meth:`ReportingCommand.map` is required for
proper command execution.
If :const:`True`, :meth:`ReportingCommand.map` is guaranteed to be
called. If :const:`False`, Splunk considers it to be an optimization
that may be skipped.
Default: :const:`False`
"""
return type(self)._requires_preop
_requires_preop = False
@property
def retainsevents(self):
""" Signals that :meth:`ReportingCommand.reduce` transforms _raw
events to produce a reporting data structure.
Fixed: :const:`False`
"""
return False
@property
def streaming(self):
""" Signals that :meth:`ReportingCommand.reduce` runs on the search
head.
Fixed: :const:`False`
"""
return False
@property
def streaming_preop(self):
""" Denotes the requested streaming preop search string.
Computed.
"""
command = type(self.command)
if command.map == ReportingCommand.map:
return ""
command_line = str(self.command)
command_name = type(self.command).name
text = ' '.join([
command_name, '__map__', command_line[len(command_name) + 1:]])
return text
#endregion
#region Methods
@classmethod
def fix_up(cls, command):
""" Verifies :code:`command` class structure and configures the
:code:`command.map` method.
Verifies that :code:`command` derives from :code:`ReportingCommand`
and overrides :code:`ReportingCommand.reduce`. It then configures
:code:`command.reduce`, if an overriding implementation of
:code:`ReportingCommand.reduce` has been provided.
:param command: :code:`ReportingCommand` class
Exceptions:
:code:`TypeError` :code:`command` class is not derived from :code:`ReportingCommand`
:code:`AttributeError` No :code:`ReportingCommand.reduce` override
"""
if not issubclass(command, ReportingCommand):
raise TypeError('%s is not a ReportingCommand' % command)
if command.reduce == ReportingCommand.reduce:
raise AttributeError('No ReportingCommand.reduce override')
if command.map == ReportingCommand.map:
cls._requires_preop = False
return
f = vars(command)['map'] # Function backing the map method
# There is no way to add custom attributes to methods. See
# [Why does setattr fail on a method](http://goo.gl/aiOsqh)
# for an explanation.
try:
settings = f._settings
except AttributeError:
f.ConfigurationSettings = StreamingCommand.ConfigurationSettings
return
# Create new `StreamingCommand.ConfigurationSettings` class
module = '.'.join([command.__module__, command.__name__, 'map'])
name = 'ConfigurationSettings'
bases = (StreamingCommand.ConfigurationSettings,)
f.ConfigurationSettings = ConfigurationSettingsType(
module, name, bases, settings)
del f._settings
return
#endregion
#endregion