Skip to content

Commit 19ee88d

Browse files
committed
Merge branch 'develop' into s3
* develop: 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
2 parents 240dd60 + 88bd2e0 commit 19ee88d

File tree

13 files changed

+1014
-1
lines changed

13 files changed

+1014
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from .base import BaseEnvelope
2+
from .cloudwatch import CloudWatchLogsEnvelope
23
from .dynamodb import DynamoDBStreamEnvelope
34
from .event_bridge import EventBridgeEnvelope
45
from .sns import SnsEnvelope
56
from .sqs import SqsEnvelope
67

7-
__all__ = ["DynamoDBStreamEnvelope", "EventBridgeEnvelope", "SnsEnvelope", "SqsEnvelope", "BaseEnvelope"]
8+
__all__ = [
9+
"CloudWatchLogsEnvelope",
10+
"DynamoDBStreamEnvelope",
11+
"EventBridgeEnvelope",
12+
"SnsEnvelope",
13+
"SqsEnvelope",
14+
"BaseEnvelope",
15+
]
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

+9
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 .s3 import S3Model, S3RecordModel
@@ -6,6 +8,13 @@
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",
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
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

docs/content/utilities/parser.mdx

+3
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ 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
159161

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

@@ -292,6 +294,7 @@ Envelope name | Behaviour | Return
292294
**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]]]`
293295
**EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`. <br/> 2. Parses `detail` key using your model and returns it. | `Model`
294296
**SqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]`
297+
**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]`
295298

296299
### Bringing your own envelope
297300

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)