Processor¶
sosw.app.Processor is the core class of the framework. Your Lambda implements a subclass of it
(conventionally called Processor in your app.py), declares its dependencies and defaults in
DEFAULT_CONFIG, and puts the business logic into __call__.
from sosw.app import LambdaGlobals, get_lambda_handler, Processor as SoswProcessor
class Processor(SoswProcessor):
DEFAULT_CONFIG = {
'init_clients': ['DynamoDb'],
'dynamo_db_config': {
'table_name': 'things',
'row_mapper': {'thing_id': 'S'},
},
}
def __call__(self, event, **kwargs):
super().__call__(event)
...
global_vars = LambdaGlobals()
lambda_handler = get_lambda_handler(Processor, global_vars)
Initialization pipeline¶
Processor.__init__ runs the following steps:
Resolve the effective
testflag: an explicitly passedtestkeyword always wins, otherwise it derives from theSTAGEenvironment variable (test/autoteststages run in test mode).Capture the AWS account id from the Lambda context (when available in
global_vars).Assemble
self.config(see Configuration layering below).Initialize
self.statsandself.resultcounters (collections.defaultdict(int)).Register the clients listed in the
init_clientsconfig parameter (see Client registration).
Configuration layering¶
The configuration of a Processor is assembled by init_config() from three layers, each
recursively updating the previous one:
DEFAULT_CONFIG— the class attribute of your Processor. Code-level defaults.The per-function config from DynamoDB / SSM: the record named
{AWS_LAMBDA_FUNCTION_NAME}_config. This is how you change the behavior of a deployed Lambda without redeploying it. Read more: Configuration.custom_config— the argument passed to the constructor (usually viaget_lambda_handler(Processor, global_vars, custom_config)). The final word.
The external lookup of layer 2 can be skipped — for functions that have no config table, no
permissions to read it, or simply no use for runtime overrides — in either of two ways:
class Processor(SoswProcessor):
DISABLE_DDB_CONFIG = True # class attribute, or:
DEFAULT_CONFIG = {'disable_ddb_config': True} # config flag (works in custom_config too)
DEFAULT_CONFIG and custom_config are still applied in this case.
Note
disable_ddb_config was absorbed from community PR #376.
Anywhere in your code, read configuration values through the self._c() shortcut with dot
notation and an optional default:
chunk_size = self._c('processing.chunk_size', 100)
Client registration¶
register_clients() initializes the clients listed in the init_clients config parameter and
assigns them to the Processor with the _client suffix. For every name it tries, in order:
a module in the
componentsormanagerspackage of your Lambda;a module in
sosw.components— names are the CamelCase class stem, e.g.'DynamoDb'→ DynamoDbClient,'Siblings'→SiblingsManager;a plain
boto3.client()— e.g.'sts'→self.sts_client = boto3.client('sts').
Class-based clients receive their config from the {module_name}_config key of the Processor
config (as custom_config for *Manager classes and config for *Client classes).
For DynamoDB specifically, prefer the lazy factory get_ddbc(prefix): declare any number of
{prefix}_dynamo_db_config blocks in the config and get fully configured, cached
DynamoDbClient instances on demand:
DEFAULT_CONFIG = {
'things_dynamo_db_config': {'table_name': 'things', 'row_mapper': {'thing_id': 'S'}},
'orders_dynamo_db_config': {'table_name': 'orders', 'row_mapper': {'order_id': 'S'}},
}
def __call__(self, event, **kwargs):
super().__call__(event)
things = self.get_ddbc('things').get_by_scan()
The stats / result contract¶
Two counters with two different lifetimes:
self.resultPer-invocation state. Reset to an empty
defaultdict(int)at the beginning of every__call__(unless you callsuper().__call__(event, reset_result=False)). Put anything here that describes the outcome of the current invocation.self.statsPer-container counters.
__call__incrementsprocessor_calls; the@benchmarkdecorator adds execution times; your code adds anything else. After every invocation the generated handler logsget_stats()and callsreset_stats(recursive=True)exactly once: ordinary counters are folded intototal_*accumulators which survive for the container lifetime, and the parameters listed in thelifetime_stats_paramsconfig are preserved as-is.
Note
In the 0.7.x line the handler called reset_stats() twice per invocation; it is now
called once (recursively). See the migration guide.
Custom clients participate in both get_stats() and reset_stats() recursively when they
implement these methods themselves.
Logging of the event and the result¶
The handler built by get_lambda_handler logs the incoming event at the beginning and the final
result at the end of every invocation. Both log lines carry redacted copies: the Processor still
receives the original event object, and the caller gets the original result object.
What is redacted:
Sensitive keys. Values of dict keys whose lowercase name contains any substring of
sosw.app.LOG_SENSITIVE_KEY_PARTS—Authorization,Cookie,X-Origin-Verify,X-Api-Key, anything withtoken/secret/password/credential/signaturein it — are replaced withsosw.app.LOG_REDACTED_VALUE('***REDACTED***') at any nesting depth, in the event and in the result.Noneand booleans are kept; numbers survive only under counter-like keys that contain a whole word ofsosw.app.LOG_COUNTER_KEY_WORDS(tokens,count— words are split on non-alphanumerics and camelCase), somax_tokensandtokenCountkeep their counters while a numericpasswordorotp_tokenis masked.Raw query strings. A
rawQueryStringvalue (HTTP API v2 / Function URL events) has the values of its sensitive parameters masked inside the string; the other parameters stay readable.Bodies. A string
bodyis redacted when it carries JSON (parsed, redacted recursively and re-serialized withensure_ascii=False); when it is form-encoded — declared by the siblingheaderscontent-typeofapplication/x-www-form-urlencoded(any key case, whitespace around the value is trimmed) or by itsk=v&k=vshape — it is redacted like a query string; and it is replaced with the marker entirely when the siblingisBase64Encodedflag is set (base64 is reversible), when thecontent-typestarts withmultipart/or the body itself has the multipart shape (starts with--and containscontent-disposition:), or when it looks like JSON but fails to parse (a truncated payload may still carry secrets).
All three constants are plain module globals read at call time, so a deployment can extend the
matching without touching sosw code:
import sosw.app
sosw.app.LOG_SENSITIVE_KEY_PARTS = sosw.app.LOG_SENSITIVE_KEY_PARTS + ('x-auth-key',)
Known limits: the redaction is best-effort, and secrets must not be sent in fields that are bound
for the logs by design. Tokens nested inside the values of non-sensitive parameters or JSON
fields (e.g. a redirect URL carrying an access_token) are not masked; JSON strings under keys
other than body (e.g. an SNS Records[].Sns.Message payload) are not parsed; header names
outside LOG_SENSITIVE_KEY_PARTS (e.g. X-Auth-Key, X-Session-Id) are kept; a value too
deeply nested to redact — or that fails to redact for any other reason — is logged as the marker
with a warning instead: logging never fails the invocation.
Failing loudly¶
sosw code is expected to fail fast and loud. When a Processor cannot continue, call
self.die(message): it logs the message with the current stats, attempts a best-effort SNS
notification to the topic from the dead_sns_topic config parameter (default
SoswWorkerErrors), and terminates the invocation.
The single deliberate exception to the rule is LambdaApi, which converts exceptions into well-formed HTTP error responses — an API must always answer its clients.
API reference¶
View Licence Agreement- class sosw.app.Processor(custom_config=None, **kwargs)[source]¶
Core Processor class template. This is the base class of the framework: every Lambda built on
soswsubclasses it (directly, or through specializations likeLambdaApi). It provides layered configuration, automatic client registration, statistics counters and a uniform entry point.get_ddbc(prefix: str) -> DynamoDbClient:Lazily initializes and retrieves a DynamoDB client configured for a specific table and schema validation.
This method initializes custom DynamoDB clients based on the provided prefix. If a client with the specified prefix has already been initialized, it returns the existing client. If not, it looks in the Processor config for a prefixed dynamodb config (e.g. for prefix
project_a->project_a_dynamo_db_config). The dynamo_db client will be initialized asself.project_a_dynamo_db_client.This is particularly useful for scenarios requiring schema validation, transformation of DynamoDB syntax to dictionary format, and other operations beyond the capabilities of the raw boto3 client.
- Parameters:
prefix (str) – The prefix for the DynamoDB client configuration and naming.
- Raises:
ValueError – If the provided prefix is not supported by the available configuration.
- init_config(custom_config: Dict = None)[source]¶
By default, tries to initialize config from
DEFAULT_CONFIGor as an empty dictionary. After that, a specific custom config of the Lambda will recursively update the existing one. The last step is update config recursively with a passed custom_config.The lookup of the specific custom config of the Lambda (
{AWS_LAMBDA_FUNCTION_NAME}_configfrom DynamoDB / SSM) can be skipped either by setting the class attributeDISABLE_DDB_CONFIG = Truein your Processor, or by passing a truthydisable_ddb_configkey in theDEFAULT_CONFIGorcustom_config.DEFAULT_CONFIGandcustom_configare still applied in this case.Overwrite this method if custom logic of recursive updates in configs is required.
Note
Read more about Config Source
- Parameters:
custom_config (Dict) – dict with custom configurations
- static get_config(name)[source]¶
Returns config by name from DynamoDB config or SSM. Override this to provide your config handling method.
- Parameters:
name – Name of the config
- Return type:
dict
- property _account¶
Get current AWS Account to construct different ARNs.
We don’t have this parameter in Environmental variables, only can parse from Context. It is stored in
global_varsand is supposed to be passed by your lambda_handler during initialization.As a fallback for cases when we use
Processornot in the Lambda environment, we have a lazy autodetection mechanism using STS, but it is pretty heavy (~0.3 seconds).- Some things to note:
We store this value in class variable for fast access
It uses Lazy initialization.
We first try from context and only if not provided - use the autodetection.
- property _region¶
Property fetched from AWS Lambda Environmental variables.
- _c(path: str, default: Any = None) Any | None[source]¶
Shortcut to access values from the Processor config.
E.g.
val = self._c('path.to.param')Is similar to:
val = self.config.get('path', {}).get('to', {}).get('param', default)The value specified in default or None is returned if the path is not found.
- get_stats(recursive: bool = True)[source]¶
Return statistics of operations performed by current instance of the Class.
Statistics of custom clients existing in the Processor is also aggregated by default. Clients must be initialized as self.some_client ending with _client suffix (e.g. self.dynamo_db_client). Clients must also have their own get_stats() methods implemented.
Be careful about circular get_stats() calls from child classes. If required overwrite get_stats() with recursive = False.
- Parameters:
recursive – Merge stats from self.***_client.
- Return type:
dict
- Returns:
Statistics counter of current Processor instance.
- reset_stats(recursive: bool = True)[source]¶
Cleans statistics other than specified for the lifetime of processor. All the parameters with prefix ‘total_’ are also preserved.
The function makes sense if your Processor lives outside the scope of lambda_handler.
Be careful about circular get_stats() calls from child classes. If required overwrite reset_stats() with recursive = False.
- Parameters:
recursive – Reset stats from self.***_client.
- die(message='Unknown Failure')[source]¶
Logs current Processor stats and message. Then raises RuntimeError with message.
If there is access to publish SNS messages, the method will also try to publish to the topic configured as dead_sns_topic or ‘SoswWorkerErrors’.
- Parameters:
message (str) – Description of failure.
- class sosw.app.LambdaGlobals[source]¶
Global placeholder for global_vars that we want to preserve in the lifetime of the Lambda Container. e.g. once initiailised the given Processor, we keep it alive in the container to minimize warm-run time.
This namespace also contains the lambda_context which should be reset by get_lambda_handler method. See the Processor examples in documentation for more info.
- sosw.app.get_lambda_handler(processor_class, global_vars=None, custom_config=None)[source]¶
Return a reference to the entry point of the lambda function.
The handler caches the initialized Processor in global_vars and reuses it in the warm invocations for the lifetime of the Lambda container. Per-invocation state belongs to
processor.result(reset on every call), whileprocessor.statscarries the counters of the container lifetime:reset_stats()runs once after every invocation and preserves thetotal_*counters and the ones configured inlifetime_stats_params.The logged copies of the event and the result are redacted: values of keys whose lowercase name contains any substring from
LOG_SENSITIVE_KEY_PARTS(e.g.Authorization,Cookie,X-Origin-Verify) are replaced withLOG_REDACTED_VALUE—None, booleans and numbers of counter-like keys (a whole word ofLOG_COUNTER_KEY_WORDS, e.g.max_tokens) are kept. Sensitive parameters ofrawQueryString, secrets inside JSONbodystrings and form-encoded bodies are masked, and a body with a truthy siblingisBase64Encoded, amultipart/body (declared by a whitespace-trimmed content type or detected by shape) or one that looks like JSON but does not parse is replaced with the marker (base64 is reversible, and an unparsable payload may still carry secrets). The Processor still receives the original event, and the original result object is returned to the caller.- Parameters:
processor_class – Callable processor class.
global_vars – Lambda’s global variables (processor, context).
custom_config – Custom configuration to pass the processor constructor.
- Returns:
Function reference for the lambda handler.
- sosw.app.LOG_REDACTED_VALUE = '***REDACTED***'¶
Value substituted in logs for anything a sensitive key holds. See
_redact_for_logging.
- sosw.app.LOG_SENSITIVE_KEY_PARTS = ('authorization', 'cookie', 'token', 'secret', 'password', 'passwd', 'api-key', 'api_key', 'apikey', 'x-origin-verify', 'credential', 'signature', 'private-key', 'private_key')¶
A key of a dict is considered sensitive (and its value is redacted in logs) when the lowercase form of the key contains any of these substrings. Consumers may extend the matching by reassigning
sosw.app.LOG_SENSITIVE_KEY_PARTS(read from the module globals at call time).
- sosw.app.LOG_COUNTER_KEY_WORDS = ('tokens', 'count')¶
A sensitive key that has one of these words among the words of its own name keeps a numeric value: counters like
max_tokens,inputTokensortokenCountare useful in logs and carry no secret. Words are the lowercase chunks of the key split on any non-alphanumeric character and on camelCase boundaries, soaccount_passworddoes not matchcount. Reassignsosw.app.LOG_COUNTER_KEY_WORDSto extend the matching (read at call time, like the others).
Running this in production? SOSW LTD offers commercial support — see Commercial Support.