Skip to content

Commit 657c502

Browse files
committed
initial commit
0 parents  commit 657c502

File tree

6 files changed

+297
-0
lines changed

6 files changed

+297
-0
lines changed

.eslintrc.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
env:
3+
es6: true
4+
node: true
5+
extends: 'eslint:recommended'
6+
parserOptions:
7+
sourceType: module
8+
rules:
9+
indent:
10+
- error
11+
- 2
12+
linebreak-style:
13+
- error
14+
- unix
15+
quotes:
16+
- error
17+
- single
18+
semi:
19+
- error
20+
- always
21+
no-console: 0

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Clay Gregory
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
2+
# CloudFront Access Log Parser
3+
4+
This is a log parser for Cloudfront Web Distribution and RTMP Distribution access logs. It can be used for directly or with the Node.js Stream API.
5+
6+
# Installation
7+
8+
```bash
9+
npm install --save cloudfront-log-parser
10+
```
11+
12+
## Usage Examples
13+
14+
### Synchronous API
15+
Given a string or Buffer of a log file, the `parse` function can be called directly, returning an array of parsed log entries.
16+
```javascript
17+
const CloudFrontParser = require('cloudfront-log-parser');
18+
const accesses = CloudFrontParser.parse(<Buffer or string of log file>, { format: 'web' });
19+
//accesses = array of objects, see below for format
20+
```
21+
22+
### Callback API
23+
If `parse` is provided with a callback function, it will be called with an array of parsed entries as the result.
24+
```javascript
25+
const CloudFrontParser = require('cloudfront-log-parser');
26+
CloudFrontParser.parse(<Buffer or string of log file>, { format: 'web' }, function (err, accesses) {
27+
//accesses = array of objects, see below for format
28+
});
29+
```
30+
31+
### Node.js Stream API
32+
33+
The parser also implements `stream.Transform` for use in Node.js Streams.
34+
35+
```javascript
36+
const CloudFrontParser = require('cloudfront-log-parser');
37+
const fs = require('fs');
38+
const zlib = require('zlib');
39+
40+
const parser = new CloudFrontParser({ format: 'web' });
41+
parser.on('readable', function(){
42+
let access;
43+
while(access = parser.read()){
44+
//access = parsed entry object
45+
}
46+
});
47+
48+
fs.createReadStream('./somelogfile.gz')
49+
.pipe(zlib.createGunzip())
50+
.pipe(parser);
51+
```
52+
53+
### Options
54+
55+
Only two configuration options are currently in effect: format and version. The parser defaults to `web` to handle the web distribution file format. If logs are from an RTMP distribution, this value should be set to `rtmp`. Currently all CloudFront logs are on version 1.0; should future versions appear, the `version` option will serve as an override.
56+
57+
```javascript
58+
const options = {
59+
format: 'web|rtmp',
60+
version: '1.0'
61+
};
62+
```
63+
64+
## Result Object
65+
66+
### Web Distribution Format
67+
68+
```javascript
69+
{ 'date': '2017-02-09',
70+
'time': '17:50:17',
71+
'x-edge-location': 'MUC51',
72+
'sc-bytes': '2797',
73+
'c-ip': '192.168.0.123',
74+
'cs-method': 'GET',
75+
'cs-host': 'yourdistribution.cloudfront.net',
76+
'cs-uri-stem': '/',
77+
'sc-status': '200',
78+
'cs-referer': '-',
79+
'cs-user-agent': 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
80+
'cs-uri-query': '-',
81+
'cs-cookie': '-',
82+
'x-edge-result-type': 'Hit',
83+
'x-edge-request-id': 'sjXpb8nMq_1ewovZ6nrojpvxIETPbo7EhF2RNtPZ_zfd0MtOW6pjlg==',
84+
'x-host-header': 'example.com',
85+
'cs-protocol': 'https',
86+
'cs-bytes': '148',
87+
'time-taken': '0.002',
88+
'x-forwarded-for': '-',
89+
'ssl-protocol': 'TLSv1.2',
90+
'ssl-cipher': 'ECDHE-RSA-AES128-GCM-SHA256',
91+
'x-edge-response-result-type': 'Hit',
92+
'cs-protocol-version': 'HTTP/1.1' }
93+
```
94+
95+
### RTMP Distribution Format
96+
97+
```javascript
98+
{ 'date': '2010-03-12',
99+
'time': '23:56:21',
100+
'x-edge-location': 'SEA4',
101+
'c-ip': '192.0.2.199',
102+
'x-event': 'stop',
103+
'sc-bytes': '429822014',
104+
'x-cf-status': 'OK',
105+
'x-cf-client-id': 'bfd8a98bed0840d2b871b7f6adf9908f',
106+
'cs-uri-stem': 'rtmp://yourdistribution.cloudfront.net/cfx/st',
107+
'cs-uri-query': 'key=value',
108+
'c-referrer': 'http://player.example.com/player.swf',
109+
'x-page-url': 'http://www.example.com/video',
110+
'c-user-agent': 'LNX 10,0,32,18' }
111+
```
112+
113+
##License
114+
115+
See the included [LICENSE](LICENSE.md) for rights and limitations under the terms of the MIT license.

index.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
'use strict';
2+
3+
const stream = require('stream');
4+
const util = require('util');
5+
6+
const formats = {
7+
'web_v1.0': [
8+
'date', 'time', 'x-edge-location', 'sc-bytes', 'c-ip',
9+
'cs-method', 'cs-host', 'cs-uri-stem', 'sc-status',
10+
'cs-referer', 'cs-user-agent', 'cs-uri-query', 'cs-cookie',
11+
'x-edge-result-type', 'x-edge-request-id', 'x-host-header',
12+
'cs-protocol', 'cs-bytes', 'time-taken', 'x-forwarded-for',
13+
'ssl-protocol', 'ssl-cipher', 'x-edge-response-result-type',
14+
'cs-protocol-version'
15+
],
16+
17+
'rtmp_v1.0': [
18+
'date', 'time', 'x-edge-location', 'c-ip', 'x-event',
19+
'sc-bytes', 'x-cf-status', 'x-cf-client-id', 'cs-uri-stem',
20+
'cs-uri-query', 'c-referrer', 'x-page-url', 'c-user-agent'
21+
]
22+
};
23+
24+
const option_defaults = {
25+
format: 'web',
26+
version: '1.0'
27+
};
28+
29+
const decode_field = val => {
30+
return decodeURIComponent(
31+
val.replace(/%2522/g, '"')
32+
.replace(/%255C/g, '\\')
33+
.replace(/%2520/g, ' ')
34+
);
35+
};
36+
37+
const CloudFrontTransform = function (options) {
38+
39+
this.options = options || {};
40+
stream.Transform.call(this, { objectMode: true });
41+
};
42+
util.inherits(CloudFrontTransform, stream.Transform);
43+
44+
CloudFrontTransform.prototype._transform = function (chunk, encoding, done) {
45+
46+
encoding = encoding || 'utf8';
47+
if (Buffer.isBuffer(chunk)) {
48+
if (encoding == 'buffer') chunk = chunk.toString();
49+
else chunk = chunk.toString(encoding);
50+
}
51+
52+
if (this._lastLineBuffer) chunk = this._lastLineBuffer + chunk;
53+
54+
const lines = chunk.split('\n');
55+
this._lastLineBuffer = lines.splice(lines.length - 1, 1)[0];
56+
57+
parse(lines, this.options).forEach(this.push.bind(this));
58+
done();
59+
};
60+
61+
CloudFrontTransform.prototype._flush = function (done) {
62+
63+
if (this._lastLineBuffer) {
64+
parse(this._lastLineBuffer, this.options).forEach(this.push.bind(this));
65+
this._lastLineBuffer = null;
66+
}
67+
68+
done();
69+
};
70+
71+
function parse(data, options, callback) {
72+
73+
let parsed;
74+
let err;
75+
try {
76+
77+
if (Buffer.isBuffer(data)) data = data.toString();
78+
79+
if (typeof data === 'string') data = data.split('\n');
80+
81+
parsed = data
82+
.filter(line => !line.startsWith('#'))
83+
.filter(line => line.length > 0)
84+
.map(line => parseLine(line, options));
85+
86+
} catch (e) {
87+
err = e;
88+
if (!callback) throw e;
89+
}
90+
91+
if (callback) callback(err, parsed);
92+
else return parsed;
93+
}
94+
95+
function parseLine(line, options) {
96+
97+
options = options || {};
98+
99+
let format = options.format;
100+
if (format === undefined) format = option_defaults.format;
101+
102+
let version = options.version;
103+
if (version === undefined) version = option_defaults.version;
104+
105+
const headings = formats[`${format}_v${version}`];
106+
if (!headings) throw new Error(`Format not recognized: ${format}`);
107+
108+
const line_arr = line.split('\t');
109+
return _zipLine(line_arr, headings);
110+
}
111+
112+
function _zipLine(arr, headings) {
113+
const result = {};
114+
for (let i = 0; i < Math.min(arr.length, headings.length); i++) {
115+
const field = headings[i];
116+
result[field] = decode_field(arr[i]);
117+
}
118+
119+
return result;
120+
}
121+
122+
module.exports = CloudFrontTransform;
123+
module.exports.parse = parse;
124+
module.exports.parseLine = parseLine;

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "cloudfront-log-parser",
3+
"version": "0.0.1",
4+
"description": "Parser for CloudFront access logs",
5+
"author": "Clay Gregory <[email protected]> (https://claygregory.com/)",
6+
"repository": "claygregory/node-cloudfront-log-parser",
7+
"homepage": "https://github.com/claygregory/node-cloudfront-log-parser",
8+
"license": "MIT",
9+
"main": "index.js",
10+
"dependencies": {
11+
},
12+
"devDependencies": {
13+
"eslint": "^3.13.1"
14+
}
15+
}

0 commit comments

Comments
 (0)