aws kinesis
当API出现故障时,您是否不讨厌它,您绝对不知道为什么? 现在,假设您无权访问运行软件的VM,群集或容器。 要我继续这场噩梦吗?
是的,这就是调试AWS Lambda函数的样子。 不知道发生了什么,也不知道为什么失败的可怕噩梦。 本文将向您展示一种记录函数调用的方法。 使您可以跟踪和监视故障和错误,同时还为您提供了一个很好的结构,用于记录信息和调试日志,以供您在需要对行为进行故障排除时使用。
关键是将所有日志发送到中央位置,您以后可以在其中进行分组,筛选和使用它们。 Sematext是整个软件堆栈的全栈可观察性解决方案。 这意味着您可以与任何现有基础架构(例如Kubernetes集群和容器)一起实现功能日志记录。
准备? 让我们开始吧!
使用CloudWatch for Logs
CloudWatch是用于显示AWS Lambda日志的默认解决方案。
CloudWatch以日志,指标和事件的形式收集监视和运营数据,为您提供在AWS和本地服务器上运行的AWS资源,应用程序和服务的统一视图。
— AWS文档
用外行的话来说,它是一项AWS服务,用于显示所有AWS服务中的日志。 我们很想知道它如何处理AWS Lambda日志。 执行Lambda函数时,无论您写到控制台中的什么内容,Go中的fmt.printf()或Node.js中的console.log()都会在后台异步发送到CloudWatch。 对我们来说幸运的是,它不会为函数执行时间增加任何开销。
在函数运行时中使用日志记录代理将增加执行开销,并增加不必要的延迟。 我们希望避免这种情况,并在将日志添加到CloudWatch后对其进行处理。 在下面,您可以看到从通用Hello World函数生成的示例日志事件。
让我们退后一步,看看大局。 每个功能都会在CloudWatch中创建一个称为日志组的东西。 单击特定的日志组。
这些日志组将包含日志流 ,这些流实际上等同于来自特定功能实例的日志事件。
对于系统洞察力和正确了解软件的运行状况,这几乎不是一个足够好的解决方案。 由于其结构,很难查看和区分日志。 使用中心位置存储日志更有意义。 您可以使用自己的Elasticsearch或托管设置。 Sematext为您提供基础结构各个部分的全栈可观察性,并公开了Elasticsearch API 。 让我向您展示为您的AWS Lambda函数创建CloudWatch日志处理并将其通过管道传递到Sematext Logs App多么容易。
创建集中式日志记录解决方案
通过使用CloudWatch日志组订阅和Kinesis,您可以将所有Lambda日志集中到一个专用功能,该功能会将其发送到Sematext的Elasticsearch API。 在那里,您可以找到所有日志的中央位置。 您可以搜索和过滤所有功能的日志,而无需花费太多精力就可以了解功能的行为和运行状况。
我将演示如何构建一个可以自己使用的单命令部署解决方案 。 它使用无服务器框架和Node.js构建。 但是,您可以随时使用AWS SAM或Terraform以及所需的任何编程语言。 这个概念将保持不变。
这就是最终的样子。
比CloudWatch更漂亮,您实际上可以找到所需的内容!
设置无服务器项目
首先,安装无服务器框架,配置IAM用户,然后创建一个新项目。 完整的指南可以在这里找到。
$ npm install -g serverless
$ sls config credentials \
--provider aws \
--key xxxxxxxxxxxxxx \
--secret xxxxxxxxxxxxxx
$ sls create --template aws-nodejs --path lambda-cwlogs-to-logsene
$ cd lambda-cwlogs-to-logsene
$ npm init -y
$ npm i logsene-js zlib serverless-iam-roles-per-function
甜! 现在移至serverless.yml。
配置资源
在代码编辑器中打开lambda-cwlogs-to-logsene目录,并检出serverless.yml 。 随时删除所有内容并将其粘贴。
# serverless.yml
service: lambda-cwlogs-to-logsene
plugins:
- serverless-iam-roles-per-function
custom :
stage : $ {opt:stage, self :provider.stage}
secrets: ${file(secrets.json)}
provider:
name: aws
runtime: nodejs8 .10
stage: dev
region: ${ self :custom.secrets.REGION, 'us-east-1' }
versionFunctions: false
functions:
shipper:
handler: shipper.handler
description: Sends CloudWatch logs from Kinesis to Sematext Elastic Search API
memorySize: 128
timeout: 3
events:
- stream:
type: kinesis
arn:
Fn::GetAtt:
- LogsKinesisStream
- Arn
batchSize: ${ self :custom.secrets.BATCH_SIZE}
startingPosition: LATEST
enabled: true
environment:
LOGS_TOKEN: ${ self :custom.secrets.LOGS_TOKEN}
LOGS_BULK_SIZE: 100
LOG_INTERVAL: 2000
subscriber:
handler: subscriber.handler
description: Subscribe all CloudWatch log groups to Kinesis
memorySize: 128
timeout: 30
events:
- http:
path: subscribe
method: get
- cloudwatchEvent:
event:
source:
- aws.logs
detail-type:
- AWS API Call via CloudTrail
detail:
eventSource:
- logs.amazonaws.com
eventName:
- CreateLogGroup
- schedule:
rate: rate( 60 minutes)
iamRoleStatements:
- Effect: "Allow"
Action:
- "iam:PassRole"
- "sts:AssumeRole"
- "logs:PutSubscriptionFilter"
- "logs:DeleteSubscriptionFilter"
- "logs:DescribeSubscriptionFilters"
- "logs:DescribeLogGroups"
- "logs:PutRetentionPolicy"
Resource: "*"
environment:
filterName: ${ self :custom.stage}-${ self :provider.region}
region: ${ self :provider.region}
shipperFunctionName: "shipper"
subscriberFunctionName: "subscriber"
prefix: "/aws/lambda"
retentionDays: ${ self :custom.secrets.LOG_GROUP_RETENTION_IN_DAYS}
kinesisArn:
Fn::GetAtt:
- LogsKinesisStream
- Arn
roleArn:
Fn::GetAtt:
- CloudWatchLogsRole
- Arn
resources:
Resources:
LogsKinesisStream:
Type: AWS::Kinesis::Stream
Properties:
Name: ${ self :service}-${ self :custom.stage}-logs
ShardCount: ${ self :custom.secrets.KINESIS_SHARD_COUNT}
RetentionPeriodHours: ${ self :custom.secrets.KINESIS_RETENTION_IN_HOURS}
CloudWatchLogsRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- logs.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: root
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- kinesis:PutRecords
- kinesis:PutRecord
Resource:
Fn::GetAtt:
- LogsKinesisStream
- Arn
RoleName: ${ self :service}-${ self :custom.stage}-cloudwatchrole
让我们将其分解。 发货人功能将由Kinesis流触发,它具有一些用于配置Sematext Logs的环境变量。 Kinesis流本身在资源部分的底部定义,并通过使用其ARN在函数事件中引用。
转到订户功能。 它可以通过三种方式触发。 由您选择。 如果您有很多现有的日志组,则可能需要单击HTTP端点以最初将它们全部订阅。 否则,每隔一段时间或仅在创建新的日志组时触发它会很好。
LogsKinesisStream是我们要订阅日志组的Kinesis流,而CloudWatchLogsRole是IAM角色,它将允许CloudWatch将记录放入Kinesis。
这样,您现在可以看到我们缺少了secrets.json文件。 但是,在继续之前,请跳至Sematext , 登录并创建一个Logs App 。 按下绿色的小按钮以添加Logs App。
添加应用程序名称和一些基本信息后,您会看到一个等待数据屏幕弹出。 按下集成指南并复制您的令牌。
现在,您可以将令牌粘贴到secrets.json
文件中。
{"LOGS_TOKEN" : "your-token" ,
"REGION" : "us-east-1" ,
"BATCH_SIZE" : 1000 ,
"LOG_GROUP_RETENTION_IN_DAYS" : 1 ,
"KINESIS_RETENTION_IN_HOURS" : 24 ,
"KINESIS_SHARD_COUNT" : 1
}
添加订户功能
我喜欢说Kinesis是Kafka的简单版本。 基本上是管道。 您订阅要发送到其中的数据,并告诉它在满足一定的批处理大小后触发Lambda函数作为事件。
具有订阅者功能的目的是为所有日志组订阅Kinesis流。 理想情况下,应在创建时对其进行订阅,当然,当然也要在最初时想要将所有现有日志组订阅到新的Kinesis流中。 作为备用,当我要手动触发订户时,我还希望有一个HTTP端点。
在代码编辑器中,创建一个新文件,并将其命名为subscriber.js
。 粘贴此代码段。
// subscriber.js
const AWS = require ( 'aws-sdk' )
AWS.config.region = process.env.region
const cloudWatchLogs = new AWS.CloudWatchLogs()
const prefix = process.env.prefix
const kinesisArn = process.env.kinesisArn
const roleArn = process.env.roleArn
const filterName = process.env.filterName
const retentionDays = process.env.retentionDays
const shipperFunctionName = process.env.shipperFunctionName
const filterPattern = ''
const setRetentionPolicy = async (logGroupName) => {
const params = {
logGroupName : logGroupName,
retentionInDays : retentionDays
}
await cloudWatchLogs.putRetentionPolicy(params).promise()
}
const listLogGroups = async (acc, nextToken) => {
const req = {
limit : 50 ,
logGroupNamePrefix : prefix,
nextToken : nextToken
}
const res = await cloudWatchLogs.describeLogGroups(req).promise()
const newAcc = acc.concat(res.logGroups.map( logGroup => logGroup.logGroupName))
if (res.nextToken) {
return listLogGroups(newAcc, res.nextToken)
} else {
return newAcc
}
}
const upsertSubscriptionFilter = async (options) => {
console .log( 'UPSERTING...' )
const { subscriptionFilters } = await cloudWatchLogs.describeSubscriptionFilters({ logGroupName : options.logGroupName }).promise()
const { filterName, filterPattern } = subscriptionFilters[ 0 ]
if (filterName !== options.filterName || filterPattern !== options.filterPattern) {
await cloudWatchLogs.deleteSubscriptionFilter({
filterName : filterName,
logGroupName : options.logGroupName
}).promise()
await cloudWatchLogs.putSubscriptionFilter(options).promise()
}
}
const subscribe = async (logGroupName) => {
const options = {
destinationArn : kinesisArn,
logGroupName : logGroupName,
filterName : filterName,
filterPattern : filterPattern,
roleArn : roleArn,
distribution : 'ByLogStream'
}
try {
await cloudWatchLogs.putSubscriptionFilter(options).promise()
} catch (err) {
console .log( `FAILED TO SUBSCRIBE [ ${logGroupName} ]` )
console .error( JSON .stringify(err))
await upsertSubscriptionFilter(options)
}
}
const subscribeAll = async (logGroups) => {
await Promise .all(
logGroups.map( async logGroupName => {
if (logGroupName.endsWith(shipperFunctionName)) {
console .log( `SKIPPING [ ${logGroupName} ] BECAUSE IT WILL CREATE CYCLIC EVENTS FROM IT'S OWN LOGS` )
return
}
console .log( `SUBSCRIBING [ ${logGroupName} ]` )
await subscribe(logGroupName)
console .log( `UPDATING RETENTION POLICY TO [ ${retentionDays} DAYS] FOR [ ${logGroupName} ]` )
await setRetentionPolicy(logGroupName)
})
)
}
const processAll = async () => {
const logGroups = await listLogGroups([])
await subscribeAll(logGroups)
}
exports.handler = async () => {
console .log( 'subscriber start' )
await processAll()
console .log( 'subscriber done' )
return {
statusCode : 200 ,
body : JSON .stringify({ message : `Subscription successful!` })
}
}
processAll()
出processAll()
函数。 它将从CloudWatch中获取所有与前缀匹配的日志组 ,并将它们放入易于访问的数组中。 然后,将它们传递给subscribeAll()
函数,该函数将在将它们订阅到您在serverless.yml
定义的Kinesis流时,通过它们进行映射。
另一个很酷的事情是将保留策略设置为7天。 您几乎不需要更多的东西,而且会削减将日志保存在AWS账户中的成本。
请记住,您还可以编辑filterPattern
通过它可以提取日志。 目前,我选择将其保留为空白并且不过滤任何内容。 但是,根据您的需求,您可以将其与您选择的记录器创建的模式相匹配。
很好,完成之后,让我们继续发送一些日志!
添加托运人功能
Kinesis流从CloudWatch接收日志后,它将触发专用于将日志发送到Elasticsearch端点的Lambda函数。 在此示例中,我们将使用LogseneJS作为日志传送器 。 如果将其分解,这很简单。 一批记录将在event参数中发送到托运人函数。 您解析日志,为其提供所需的结构,然后将其发送到Sematext。 这是它的样子。 创建一个新文件,将其命名为shipper.js,然后将此代码粘贴到其中。
// shipper.js
const Zlib = require ( 'zlib' )
const Logsene = require ( 'logsene-js' )
const logger = new Logsene(process.env.LOGS_TOKEN)
const errorPatterns = [
'error'
]
const configurationErrorPatterns = [
'module initialization error' ,
'unable to import module'
]
const timeoutErrorPatterns = [
'task timed out' ,
'process exited before completing'
]
/**
* Sample of a structured log
* ***************************************************************************
* Timestamp RequestId Message
* 2019-03-08T15:58:45.736Z 53499d7f-60f1-476a-adc8-1e6c6125a67c Hello World!
* ***************************************************************************
*/
const structuredLogPattern = '[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T(2[0-3]|[01][0-9]):[0-5][0-9]:[0-5][0-9].[0-9][0-9][0-9]Z([ \t])[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}([ \t])(.*)'
const regexError = new RegExp (errorPatterns.join( '|' ), 'gi' )
const regexConfigurationError = new RegExp (configurationErrorPatterns.join( '|' ), 'gi' )
const regexTimeoutError = new RegExp (timeoutErrorPatterns.join( '|' ), 'gi' )
const regexStructuredLog = new RegExp (structuredLogPattern)
const lambdaVersion = ( logStream ) => logStream.substring(logStream.indexOf( '[' ) + 1 , logStream.indexOf( ']' ))
const lambdaName = ( logGroup ) => logGroup.split( '/' ).reverse()[ 0 ]
const checkLogError = ( log ) => {
if (log.message.match(regexError)) {
log.severity = 'error'
log.error = {
type : 'runtime'
}
} else if (log.message.match(regexConfigurationError)) {
log.severity = 'error'
log.error = {
type : 'configuration'
}
} else if (log.message.match(regexTimeoutError)) {
log.severity = 'error'
log.error = {
type : 'timeout'
}
}
return log
}
const splitStructuredLog = ( message ) => {
const parts = message.split( '\t' , 3 )
return {
timestamp : parts[ 0 ],
requestId : parts[ 1 ],
msg : parts[ 2 ]
}
}
/**
* Create payload for Logsene API
*/
const parseLog = ( functionName, functionVersion, message, awsRegion ) => {
if (
message.startsWith( 'START RequestId' ) ||
message.startsWith( 'END RequestId' ) ||
message.startsWith( 'REPORT RequestId' )
) {
return
}
// if log is structured
if (message.match(regexStructuredLog)) {
const { timestamp, requestId, msg } = splitStructuredLog(message)
return checkLogError({
message : msg,
function : functionName,
version : functionVersion,
region : awsRegion,
type : 'lambda' ,
severity : 'debug' ,
timestamp : timestamp,
requestId : requestId
})
} else { // when log is NOT structured
return checkLogError({
message : message,
function : functionName,
version : functionVersion,
region : awsRegion,
type : 'lambda' ,
severity : 'debug'
})
}
}
const parseLogs = ( event ) => {
const logs = []
event.Records.forEach( record => {
const payload = Buffer.from(record.kinesis.data, 'base64' )
const json = (Zlib.gunzipSync(payload)).toString( 'utf8' )
const data = JSON .parse(json)
if (data.messageType === 'CONTROL_MESSAGE' ) { return }
const functionName = lambdaName(data.logGroup)
const functionVersion = lambdaVersion(data.logStream)
const awsRegion = record.awsRegion
data.logEvents.forEach( logEvent => {
const log = parseLog(functionName, functionVersion, logEvent.message, awsRegion)
if (!log) { return }
logs.push(log)
})
})
return logs
}
const shipLogs = async (logs) => {
return new Promise ( ( resolve ) => {
if (!logs.length) { return resolve( 'No logs to ship.' ) }
logs.forEach( log => logger.log(log.severity, 'LogseneJS' , log))
logger.send( () => resolve( 'Logs shipped successfully!' ))
})
}
exports.handler = async (event) => {
try {
const res = await shipLogs(parseLogs(event))
console .log(res)
} catch (err) {
console .log(err)
return err
}
return 'shipper done'
}
发货人Lambda的核心在于parseLogs()
和shipLogs()
函数。 前者将采用event参数,提取所有日志事件,解析它们,将它们添加到数组中,然后返回该数组。 尽管后者将采用相同的日志数组,但是将每个单个日志事件添加到LogseneJS缓冲区中,然后一次性发送所有这些事件。 该位置是您在上面创建的Logs App。
您还记得从文章开头看到的典型函数调用的日志事件吗? 在那里您可以看到它生成4种不同类型的日志事件。
START RequestId
...
END RequestId
REPORT RequestId
它们可以从这三种模式中的任何一种开始,其中省略号表示在函数运行时(Node.js中的console.log()
打印到stdout的任何类型的字符串。
parseLog()
函数将完全跳过START,END和REPORT日志事件,并且仅基于调试或错误(根据用户定义的stdout还是函数运行时中的任何类型的错误)返回用户定义的日志事件。 ,配置或持续时间。
日志消息本身可以默认构造,但并非总是如此。 在Node.js运行时中,默认情况下,它的结构如下所示。
Timestamp RequestId Message2019 -03 -08 T15: 58 : 45.736 Z 53499 d7f -60 f1 -476 a-adc8 -1e6 c6125a67c Hello World!
托运人中的代码配置为与上面的结构或仅包含消息部分的结构一起使用。 如果您使用的是其他运行时,建议您使用结构化日志记录,以使日志事件具有通用结构。
完成编码部分后,您就可以部署和测试自定义日志传送程序了。
部署和测试您的集中式日志记录解决方案
使用无服务器框架之类的基础架构作为代码解决方案的好处在于部署的简单性。 您可以使用一个命令将所有内容推送到云中。 跳回到您的终端并在您的项目目录中运行:
$ sls deploy
您将看到输出打印到控制台。
[output]
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (2.15 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
............
Serverless: Stack update finished...
Service Information
service: lambda-cwlogs-to-logsene
stage: dev
region: us-east -1
stack: lambda-cwlogs-to-logsene-dev
api keys:
None
endpoints:
GET - https: //.execute-api.us-east-1.amazonaws.com/dev/subscribe
functions:
shipper: lambda-cwlogs-to-logsene-dev-shipper
subscriber: lambda-cwlogs-to-logsene-dev-subscriber
layers:
None
Serverless: Removing old service artifacts from S3…
而已。 现在,您具有将所有日志从Lambda函数传送到Sematext Cloud的设置 。 确保触发订阅者功能以将日志组订阅到Kinesis流。 触发订阅者后,您将在Sematext中看到订阅者生成的日志,您可以放心它可以工作。
在上方,您可以看到我如何添加严重性过滤。 您可以轻松选择要过滤的值,从而可以轻松地跟踪错误,超时和调试日志。
费用呢?
在您的AWS账户中进行这样的设置的成本相当便宜。 单个分片Kinesis流的固定成本大约为$ 14 /月,其中包含流数据量的额外成本 。 单个分片的摄取容量为1MB /秒或1000条记录/秒,这对大多数用户来说都可以。
Kinesis成本分为碎片时间和大小为25KB的PUT有效负载单元 。 一个分片每天的费用为$ 0.36,而一百万个PUT有效负载单位的费用为$ 0.014。 假设,如果您有一个分片和每秒100个PUT有效负载单元,那么在30天的时间里 ,分片的成本为$ 10.8,有效负载单元的成本为$ 3.6288 。
Lambda函数被配置为使用最小的内存量,即128MB,这意味着在适度使用期间,成本通常会停留在免费层上。 那是您最少的担心。
结语
拥有日志中心位置至关重要。 尽管CloudWatch以其自己的方式有用,但缺乏概览性。 通过使用中心位置,您无需切换上下文即可调试不同类型的应用程序。 Sematext可以监视您的整个软件堆栈。 在Sematext Logs中拥有Kubernetes日志, 容器日志和Lambda 日志 ,您可以在其中轻松跟踪所有内容,这是一个主要好处。
如果您需要再次检出代码, 这是repo ,如果您希望更多的人在GitHub上看到它,请给它加星号。 您还可以克隆存储库并立即进行部署。 不要忘记先添加Logs App令牌。
如果您需要软件堆栈的可观察性解决方案,请签出Sematext 。 我们正在努力开源产品并产生影响。
希望你们和我喜欢写这本书一样喜欢阅读。 如果喜欢的话,请点一下这个很小的共享按钮,以便更多的人看到本教程。 在下一次之前,请保持好奇并开心。
最初于 2019 年3月15日 发布在 sematext.com 上。
aws kinesis