Skip to content

Commit c822713

Browse files
committed
Merge branch 'develop' into kinesis
* develop: docs: add S3 as a supported model docs: add CW Logs as a supported envelope fix: cloudwatch logs envelope typo docs: add CW Logs as a supported model docs: add Alb as a supported model docs: shadow sidebar to remain expanded cr fixes feat: Add cloudwatch lambda event support to Parser utility feat: Add alb lambda event support to Parser utility #228 feat: Add S3 lambda event support to Parser utility #224
2 parents 9b55817 + b82ea7a commit c822713

File tree

16 files changed

+1215
-0
lines changed

16 files changed

+1215
-0
lines changed

aws_lambda_powertools/utilities/parser/envelopes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from .base import BaseEnvelope
2+
from .cloudwatch import CloudWatchLogsEnvelope
23
from .dynamodb import DynamoDBStreamEnvelope
34
from .event_bridge import EventBridgeEnvelope
45
from .kinesis import KinesisDataStreamEnvelope
56
from .sns import SnsEnvelope
67
from .sqs import SqsEnvelope
78

89
__all__ = [
10+
"CloudWatchLogsEnvelope",
911
"DynamoDBStreamEnvelope",
1012
"EventBridgeEnvelope",
1113
"KinesisDataStreamEnvelope",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
from typing import Any, Dict, List, Optional, Union
3+
4+
from ..models import CloudWatchLogsModel
5+
from ..types import Model
6+
from .base import BaseEnvelope
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class CloudWatchLogsEnvelope(BaseEnvelope):
12+
"""CloudWatch Envelope to extract a List of log records.
13+
14+
The record's body parameter is a string (after being base64 decoded and gzipped),
15+
though it can also be a JSON encoded string.
16+
Regardless of its type it'll be parsed into a BaseModel object.
17+
18+
Note: The record will be parsed the same way so if model is str
19+
"""
20+
21+
def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> List[Optional[Model]]:
22+
"""Parses records found with model provided
23+
24+
Parameters
25+
----------
26+
data : Dict
27+
Lambda event to be parsed
28+
model : Model
29+
Data model provided to parse after extracting data using envelope
30+
31+
Returns
32+
-------
33+
List
34+
List of records parsed with model provided
35+
"""
36+
logger.debug(f"Parsing incoming data with SNS model {CloudWatchLogsModel}")
37+
parsed_envelope = CloudWatchLogsModel.parse_obj(data)
38+
logger.debug(f"Parsing CloudWatch records in `body` with {model}")
39+
output = []
40+
for record in parsed_envelope.awslogs.decoded_data.logEvents:
41+
output.append(self._parse(data=record.message, model=model))
42+
return output

aws_lambda_powertools/utilities/parser/models/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from .alb import AlbModel, AlbRequestContext, AlbRequestContextData
2+
from .cloudwatch import CloudWatchLogsData, CloudWatchLogsDecode, CloudWatchLogsLogEvent, CloudWatchLogsModel
13
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
24
from .event_bridge import EventBridgeModel
35
from .kinesis import KinesisDataStreamModel, KinesisDataStreamRecord, KinesisDataStreamRecordPayload
@@ -6,13 +8,22 @@
68
from .sqs import SqsModel, SqsRecordModel
79

810
__all__ = [
11+
"CloudWatchLogsData",
12+
"CloudWatchLogsDecode",
13+
"CloudWatchLogsLogEvent",
14+
"CloudWatchLogsModel",
15+
"AlbModel",
16+
"AlbRequestContext",
17+
"AlbRequestContextData",
918
"DynamoDBStreamModel",
1019
"EventBridgeModel",
1120
"DynamoDBStreamChangedRecordModel",
1221
"DynamoDBStreamRecordModel",
1322
"KinesisDataStreamModel",
1423
"KinesisDataStreamRecord",
1524
"KinesisDataStreamRecordPayload",
25+
"S3Model",
26+
"S3RecordModel",
1627
"SesModel",
1728
"SesRecordModel",
1829
"SnsModel",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Dict
2+
3+
from pydantic import BaseModel
4+
5+
6+
class AlbRequestContextData(BaseModel):
7+
targetGroupArn: str
8+
9+
10+
class AlbRequestContext(BaseModel):
11+
elb: AlbRequestContextData
12+
13+
14+
class AlbModel(BaseModel):
15+
httpMethod: str
16+
path: str
17+
body: str
18+
isBase64Encoded: bool
19+
headers: Dict[str, str]
20+
queryStringParameters: Dict[str, str]
21+
requestContext: AlbRequestContext
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import base64
2+
import json
3+
import logging
4+
import zlib
5+
from datetime import datetime
6+
from typing import List
7+
8+
from pydantic import BaseModel, Field, validator
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class CloudWatchLogsLogEvent(BaseModel):
14+
id: str # noqa AA03 VNE003
15+
timestamp: datetime
16+
message: str
17+
18+
19+
class CloudWatchLogsDecode(BaseModel):
20+
messageType: str
21+
owner: str
22+
logGroup: str
23+
logStream: str
24+
subscriptionFilters: List[str]
25+
logEvents: List[CloudWatchLogsLogEvent]
26+
27+
28+
class CloudWatchLogsData(BaseModel):
29+
decoded_data: CloudWatchLogsDecode = Field(None, alias="data")
30+
31+
@validator("decoded_data", pre=True)
32+
def prepare_data(cls, value):
33+
try:
34+
logger.debug("Decoding base64 cloudwatch log data before parsing")
35+
payload = base64.b64decode(value)
36+
logger.debug("Decompressing cloudwatch log data before parsing")
37+
uncompressed = zlib.decompress(payload, zlib.MAX_WBITS | 32)
38+
return json.loads(uncompressed.decode("utf-8"))
39+
except Exception:
40+
raise ValueError("unable to decompress data")
41+
42+
43+
class CloudWatchLogsModel(BaseModel):
44+
awslogs: CloudWatchLogsData
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from datetime import datetime
2+
from typing import List, Optional
3+
4+
from pydantic import BaseModel
5+
from pydantic.fields import Field
6+
from pydantic.networks import IPvAnyNetwork
7+
from pydantic.types import PositiveInt
8+
from typing_extensions import Literal
9+
10+
11+
class S3EventRecordGlacierRestoreEventData(BaseModel):
12+
lifecycleRestorationExpiryTime: datetime
13+
lifecycleRestoreStorageClass: str
14+
15+
16+
class S3EventRecordGlacierEventData(BaseModel):
17+
restoreEventData: S3EventRecordGlacierRestoreEventData
18+
19+
20+
class S3Identity(BaseModel):
21+
principalId: str
22+
23+
24+
class S3RequestParameters(BaseModel):
25+
sourceIPAddress: IPvAnyNetwork
26+
27+
28+
class S3ResponseElements(BaseModel):
29+
x_amz_request_id: str = Field(None, alias="x-amz-request-id")
30+
x_amz_id_2: str = Field(None, alias="x-amz-id-2")
31+
32+
33+
class S3OwnerIdentify(BaseModel):
34+
principalId: str
35+
36+
37+
class S3Bucket(BaseModel):
38+
name: str
39+
ownerIdentity: S3OwnerIdentify
40+
arn: str
41+
42+
43+
class S3Object(BaseModel):
44+
key: str
45+
size: PositiveInt
46+
eTag: str
47+
sequencer: str
48+
versionId: Optional[str]
49+
50+
51+
class S3Message(BaseModel):
52+
s3SchemaVersion: str
53+
configurationId: str
54+
bucket: S3Bucket
55+
object: S3Object # noqa: A003,VNE003
56+
57+
58+
class S3RecordModel(BaseModel):
59+
eventVersion: str
60+
eventSource: Literal["aws:s3"]
61+
awsRegion: str
62+
eventTime: datetime
63+
eventName: str
64+
userIdentity: S3Identity
65+
requestParameters: S3RequestParameters
66+
responseElements: S3ResponseElements
67+
s3: S3Message
68+
glacierEventData: Optional[S3EventRecordGlacierEventData]
69+
70+
71+
class S3Model(BaseModel):
72+
Records: List[S3RecordModel]

docs/content/utilities/parser.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ Model name | Description
156156
**DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams
157157
**EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge
158158
**SqsModel** | Lambda Event Source payload for Amazon SQS
159+
**AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer
160+
**CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs
161+
**S3Model** | Lambda Event Source payload for Amazon S3
159162

160163
You can extend them to include your own models, and yet have all other known fields parsed along the way.
161164

@@ -292,6 +295,7 @@ Envelope name | Behaviour | Return
292295
**DynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamModel`. <br/> 2. Parses records in `NewImage` and `OldImage` keys using your model. <br/> 3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys | `List[Dict[str, Optional[Model]]]`
293296
**EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`. <br/> 2. Parses `detail` key using your model and returns it. | `Model`
294297
**SqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]`
298+
**CloudWatchLogsEnvelope** | 1. Parses data using `CloudwatchLogsModel` which will base64 decode and decompress it. <br/> 2. Parses records in `message` key using your model and return them in a list. | `List[Model]`
295299

296300
### Bringing your own envelope
297301

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import PropTypes from 'prop-types';
2+
import React, {createContext, useContext, useMemo} from 'react';
3+
import styled from '@emotion/styled';
4+
import {trackCustomEvent} from 'gatsby-plugin-google-analytics';
5+
6+
export const GA_EVENT_CATEGORY_CODE_BLOCK = 'Code Block';
7+
export const MultiCodeBlockContext = createContext({});
8+
export const SelectedLanguageContext = createContext();
9+
10+
const Container = styled.div({
11+
position: 'relative'
12+
});
13+
14+
const langLabels = {
15+
js: 'JavaScript',
16+
ts: 'TypeScript',
17+
'hooks-js': 'Hooks (JS)',
18+
'hooks-ts': 'Hooks (TS)'
19+
};
20+
21+
function getUnifiedLang(language) {
22+
switch (language) {
23+
case 'js':
24+
case 'jsx':
25+
case 'javascript':
26+
return 'js';
27+
case 'ts':
28+
case 'tsx':
29+
case 'typescript':
30+
return 'ts';
31+
default:
32+
return language;
33+
}
34+
}
35+
36+
function getLang(child) {
37+
return getUnifiedLang(child.props['data-language']);
38+
}
39+
40+
export function MultiCodeBlock(props) {
41+
const {codeBlocks, titles} = useMemo(() => {
42+
const defaultState = {
43+
codeBlocks: {},
44+
titles: {}
45+
};
46+
47+
if (!Array.isArray(props.children)) {
48+
return defaultState;
49+
}
50+
51+
return props.children.reduce((acc, child, index, array) => {
52+
const lang = getLang(child);
53+
if (lang) {
54+
return {
55+
...acc,
56+
codeBlocks: {
57+
...acc.codeBlocks,
58+
[lang]: child
59+
}
60+
};
61+
}
62+
63+
if (child.props.className === 'gatsby-code-title') {
64+
const nextNode = array[index + 1];
65+
const title = child.props.children;
66+
const lang = getLang(nextNode);
67+
if (nextNode && title && lang) {
68+
return {
69+
...acc,
70+
titles: {
71+
...acc.titles,
72+
[lang]: title
73+
}
74+
};
75+
}
76+
}
77+
78+
return acc;
79+
}, defaultState);
80+
}, [props.children]);
81+
82+
const languages = useMemo(() => Object.keys(codeBlocks), [codeBlocks]);
83+
const [selectedLanguage, setSelectedLanguage] = useContext(
84+
SelectedLanguageContext
85+
);
86+
87+
if (!languages.length) {
88+
return props.children;
89+
}
90+
91+
function handleLanguageChange(language) {
92+
setSelectedLanguage(language);
93+
trackCustomEvent({
94+
category: GA_EVENT_CATEGORY_CODE_BLOCK,
95+
action: 'Change language',
96+
label: language
97+
});
98+
}
99+
100+
const defaultLanguage = languages[0];
101+
const renderedLanguage =
102+
selectedLanguage in codeBlocks ? selectedLanguage : defaultLanguage;
103+
104+
return (
105+
<Container>
106+
<MultiCodeBlockContext.Provider
107+
value={{
108+
selectedLanguage: renderedLanguage,
109+
languages: languages.map(lang => ({
110+
lang,
111+
label:
112+
// try to find a label or capitalize the provided lang
113+
langLabels[lang] || lang.charAt(0).toUpperCase() + lang.slice(1)
114+
})),
115+
onLanguageChange: handleLanguageChange
116+
}}
117+
>
118+
<div className="gatsby-code-title">{titles[renderedLanguage]}</div>
119+
{codeBlocks[renderedLanguage]}
120+
</MultiCodeBlockContext.Provider>
121+
</Container>
122+
);
123+
}
124+
125+
MultiCodeBlock.propTypes = {
126+
children: PropTypes.node.isRequired
127+
};

0 commit comments

Comments
 (0)