2018/1/15

Typescript


TypeScript 是在 MS 工作的 Anders Hejlsberg (C#, TurboPascal 之父)提出的一個新的程式語言,不過他並不是一個無中生有的語言,TypeScript 是 JavaScript ES5 及 ES6 的 superset,可以跟既有的 JavaScript 程式完全相容,他主要是將若資料型別的 JavaScript 轉變為強資料型別的程式語言,在開發及編譯時,就能夠察覺一些程式語法的錯誤,同時增加物件導向的概念,它可以幫助 JavaScript 開發人員更容易撰寫及維護大規模的應用程式。


安裝


TypeScript 的編譯器 (tsc) 可透過 npm 安裝,另外 tsserver 是 node 執行檔,包含了 TypeScript 編譯器及 language service,介面為 JSON protocol,適用於 editors 及 IDE。


$ sudo npm install -g typescript
/opt/local/bin/tsc -> /opt/local/lib/node_modules/typescript/bin/tsc
/opt/local/bin/tsserver -> /opt/local/lib/node_modules/typescript/bin/tsserver
+ typescript@2.6.1
added 1 package in 2.927s

測試:建立一個新的 greeter.ts 檔案


function greeter(person) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);

透過 tsc 將 greeter.ts 編譯為 greeter.js


tsc greeter.ts

編譯後的 greeter.js 可在 console 用 nodejs 執行,或是放在一個網頁 greeter.html 裡面


$ node greeter.js
Hello, Jane User

greeter.html


<!DOCTYPE html>
<html>
    <head><title>TypeScript Greeter</title></head>
    <body>
        <script src="greeter.js"></script>
    </body>
</html>

開發的 IDE


雖然 Anders Hejlsberg 在官方網頁告訴我們要使用 Visual Studio plugin,但我們還是別的選擇



TypeScript Handbook


翻閱 TypeScript Handbook 手冊會發現,文件的編排方式,就像是在說明一個物件導向的程式語言一樣。


從基本的資料型別 Basic Types 開始,然後說明如何宣告變數,再來就是物件導向的核心: Interface, Classes 及 Functions,最後是 module 與 namespace。


Basic Types

Boolean, Number, String, Array, Tuple, Enum, Any, Void, Null and Undefined, never


// boolean
let isDone: boolean = false;

// number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

// string  " 或是 ' 都可以
let color: string = "blue";
color = 'red';

特殊的 template strings `


let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.

I'll be ${ age + 1 } years old next month.`;

// 等同

let sentence: string = "Hello, my name is " + fullName + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

Array,有兩種宣告方式


let list: number[] = [1, 2, 3];

let list: Array<number> = [1, 2, 3];

Tuple: 就是固定 elements 個數的 array,且各元素的資料型別要相同


// Declare a tuple type
let x: [string, number];

// Initialize it
x = ["hello", 10]; // OK

// Initialize it incorrectly
// error TS2322: Type '[number, string]' is not assignable to type '[string, number]'. Type 'number' is not assignable to type 'string'.
x = [10, "hello"]; // Error

Enum


// 預設第一個元素,數字為 0
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

let colorName: string = Color[2];
alert(colorName); // Displays 'Blue' as it's value is 2 above

// 可強制設定 enum numbers
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

Any: 宣告變數時,不知道這個變數的資料型別是什麼,可以任意變換自己的資料型別


let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

類似 Object 的功能,但 Object 只能讓我們指定 value,不能使用該 value 資料型別的任何一個 functions


let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'

Void: 通常用在表示 function 沒有 return value


function warnUser(): void {
    alert("This is my warning message");
}

// 將變數宣告為 void,只能設定為 undefined 或是 null
let unusable: void = undefined;

Null and Undefined: 預設 null 及 undefined 是所有其他資料型別的 subtypes,換句話說,可以將 undefined 指定給 number,但如果編譯時加上 --strictNullChecks,就可以限制只能將 null 及 undefined 指定給 void


// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

Never: 代表 type of values that never occur,例如可以設定某個只會 throw exception 的 function 的 return value 為 never。他是獨立的,不是任何一種資料型別的 subtype,即使是 any 也不能指定給 never 型別的變數。


// Function returning never must have unreachable end point
function error(message: string): never {
    throw new Error(message);
}

// Inferred return type is never
function fail() {
    return error("Something failed");
}

// Function returning never must have unreachable end point
function infiniteLoop(): never {
    while (true) {
    }
}

Type assertions: 利用 compiler 檢查(確認) 資料型別,有兩種寫法 (someValue) 或是 (someValue as string)


let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

let someValue2: any = "this is a string";

let strLength2: number = (someValue as string).length;

Variable Declaration

let 與 var 的差異


JavaScript Hositing: 在 JavaScript,變數可在使用後才被宣告,換句話說,變數可在宣告前,就使用它。在執行時期時,所有var變數都會自動被hoisting。如果程式中有參考到使用var定義過的變數時,會變成undefined,不會產生ERROR。


但 let 宣告的變數,不會被 Hositing,他只能作用在 { } 區塊範圍中。若程式中有參考到let定義過的變數時,因作用區塊不同會產生ERROR,此行為比較接近常用的程式語言寫法。


在 for 裡面,i 會持續被重新定義,所以在 setTimeout 後,最後使用的 i 都會是 5


for (var i = 0; i < 5 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}

$ node greeter.js
5
5
5
5
5

let 的變數不會被重新宣告,能夠維持 i 的實際變數 value


for (let i = 0; i < 5 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}

$ node greeter.js
0
1
2
3
4



const: 不能被 re-assign 的變數


const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error,以 const 宣告的 kitty 是一個有 readonly 屬性的object
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

Functions

typescript 跟 javascript 一樣,可以建立 named 及 anonymous 兩種 functions


// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };



爲 function 參數及 return value 加上 data type


function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

增加 function type,下面的 (x: number, y: number) => number 就是 myAdd 的 function type,function type 裡面的變數名稱只是輔助使用幫助閱讀而已,實際上跟後面的程式本體沒有關係。


let myAdd: (x: number, y: number) => number =
    function(x: number, y: number): number { return x + y; };
    
let myAdd: (baseValue: number, increment: number) => number =
    function(x: number, y: number): number { return x + y; };



Optional 及 Default Parameters


function 的所有參數,在呼叫該 funciton 時,預設都是必要的。如果是 Optional 參數,要在定義時加上 ? ,也可以為參數寫上 default value


以下這兩個 function 的 function type 都是 (firstName: string, lastName?: string) => string


function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

function buildName2(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}



Rest Parameters: ... 將剩下的參數集合為一個 array


function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;



this: 在 javascript 的 function 被呼叫時,同時會設定 this 這個變數。但通常 function 會先被定義,而在後面才被呼叫,因此常常會弄錯 this 指定的對象。


Arrow function: ()=> 在 arrow function 中的 this,會指向該 function 定義時的 object,而不是使用該 function 時的 object


foo(x, y) => {
    x++; 
    y--; 
    return x+y;
}

沒有使用arrow function,在呼叫 says 時,setTimeout 裡面的 this 就不是 Animal 而是 window.this


//沒有使用arrow function

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout(function(){
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}

var animal = new Animal()
animal.says('hi')  //undefined says hi

將 function() 改為 () =>


class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout( () => {
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}
var animal = new Animal()
animal.says('hi')  //animal says hi



Overloads: 因為 javascript 是非常動態的語言,常常會遇到某個 function 會在不同狀況,回傳不同資料型別的資料。


定義該 function 時會很直覺地將該 function 的 return value 定義為 any。但這樣卻失去了 TypeScript 的強資料型別的檢查功能。


解決方式是在前面明確地將 function 的各種參數及 return 的狀況都定義出來。


function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

Interfaces

interface 就跟 java interface 功能一樣,可提供 function 定義的檢查,限制 function 必須在實作時,遵循 interface 的定義。


例如 printLabel 需要一個參數 lavelledObj,且該參數要有 label 屬性。


function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

增加一個 interface 定義,讓 labelledObj 定義為 LabelledValue 型別,就能在編譯時,檢查 myObj 是否有遵循 LabelledValue 的介面定義。


interface LabelledValue {
    label: string;
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);



Optional interface properties: interface 的屬性,可用 ? 代表該屬性可有可無


interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({color: "black"});



readonly properties: 限制該屬性在 assign 後,就不能被修改


interface Point {
    readonly x: number;
    readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

作用跟 const 很像,差別是 properties 是用 readonly,而 variables 是用 const




Function Types: 可利用 interface 描述 Function Types


interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

因 compiler 的檢查機制,可以簡化 function 定義裡面原本要寫的 參數及 return value 的 data types


let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}



Indexable Types: have an index signature that describes the types we can use to index into the object


interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];



Class Types: 就跟 c#, java 的 interface 與 class 的關係一樣,class 可 implements interfaces。interface 可定義 properties 及 functions


interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}



static 與 instance sides of classes 的差異


class 有兩種面向: static side 與 instance side


如果需要一個特殊的 constructor,以下是有問題的程式,因為當 class 實作 interface 時,只會檢查 instance side,但 constructor 是 static side。


interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

必須將 static side 及 instance side 分成兩個 interfaces: ClockConstructor 是給 constructor 用的, ClockInterface 是 instance methods


interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
    tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
digital.tick();
let analog = createClock(AnalogClock, 7, 32);
analog.tick();



Extending Interfaces: interfaces 可互相 extend,也就是可以從一個 interface 複製 members 到另一個


interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

Classes

這是最簡單的 class,用 new 語法產生 instance


class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");



Inheritance: Dog 繼承 Animal,多了 move 這個 method


class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();



更複雜的例子 Animal: Horse and Snake,用 super 呼叫上層的 method


class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);



public(預設), private, protected


private: 不能從 class 外面使用該 member
protected: 可在 subclass 使用該 member


class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
// console.log(howard.name); // error

也可以將 constructor 設定為 protected,表示該 class 不能直接被 instantiated,但還是可以被繼承


class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee can extend Person
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
// let john = new Person("John"); // Error: The 'Person' constructor is protected



readonly: 可將 properties 設定為 readonly,但 readonly properties 必須在宣告或是 constructor 初始化


class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
// dad.name = "Man with the 3-piece suit"; // error! name is readonly.

可為 readonly 欄位加上 get set methods


let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

如果直接編譯會發生 error,可用 tsconfig.json 或是編譯的參數解決


error TS1056: Accessors are only available when targeting ECMAScript 5 and higher.

注意編譯時要加上 --target ES5


tsc --target ES5 greeter.ts



static properties: 直接用 Grid.origin 存取 origin


class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}



abstract class: 不能直接 instantiated,只有部分已經實作的 methods


abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

Iterators

for..of 及 for..in


for..in 會回傳 a list of keys
for..of 會回傳 a list of values


let list = [4, 5, 6];

for (let i in list) {
   console.log(i); // "0", "1", "2",
}

for (let i of list) {
   console.log(i); // "4", "5", "6"
}

Modules

自 ECMAScript 2015 開始,JavaScript 支援了 modules,modules 可讓 variables, functions, classes 運作在 modules 中,外面的程式只能 import 使用 export 的部分。


StringValidator.ts


export interface StringValidator {
    isAcceptable(s: string): boolean;
}

LettersOnlyValidator.ts


import { StringValidator } from "./StringValidator";

const lettersRegexp = /^[A-Za-z]+$/;

export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

ZipCodeValidator.ts


import { StringValidator } from "./StringValidator";

const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// 定義 ZipCodeValidator 時沒有 export,可將 export 獨立寫成一行
//export { ZipCodeValidator };
// export 時,以 as 語法 rename class name
export { ZipCodeValidator as mainValidator };

AllValidators.ts


// 可將其他三個 ts 的 export 合併在一起

export * from "./StringValidator"; // exports interface 'StringValidator'
export * from "./LettersOnlyValidator"; // exports class 'LettersOnlyValidator'
export * from "./ZipCodeValidator";  // exports class 'ZipCodeValidator'

Test.ts


import { StringValidator } from "./StringValidator";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
strings.forEach(s => {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
});

namespaces

namespaces 為 internal modules,
modules 為 external modules。


將所有 validator 相關的程式放在 Validation 這個 namespace 裡面,但同樣要用 export 開放使用的介面。


namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

Declaration Files


在 TypeScript 要使用 JavaScript 的 libraries,必須要有該 libray 的定義檔


以 jQuery 為例,通常會使用 $('#id') 或是 jQuery('#id') 這樣的語法,在 TypeScript 編譯時,並不知道 $ 或是 jQuery 的意思,這時需要用 declare 語法定義 jQuery。


通常會把 Declaration File 放在獨立的檔案中,例如


// jQuery.d.ts

declare var jQuery: (string) => any;

然後再以 /// 語法引用


/// <reference path="./jQuery.d.ts" />

jQuery('#foo');

完整的 jQuery Declaration File 已經有人寫好了,可以直接下載 DefinitelyTyped/types/jquery/index.d.ts


但在 TypeScript 2.0+ 已經不建議這樣做,而是改用 @types 來管理所有 library 的 Declaration Files,可以用 npm 安裝 jquery 的 @types


npm install --save-dev @types/jquery

使用 jQuery


/// <reference types="jquery" />

$('#id');

編譯


tsc --target ES6 test.ts

如果要搜尋其他 libraries 的定義檔,可在這個網站 DefinitelyTyped: The repository for high quality TypeScript type definitions 搜尋


ref:


How to use jQuery with TypeScript


TypeScript 聲明文件


References


TypeScript新手入門


學習TypeScript:初體驗


我用 TypeScript 語言的七個月


How to use jQuery with TypeScript


Importing jqueryui with Typescript and RequireJS


Adding jQuery and jQueryUI to your TypeScript project




JS ECMAScript 6 compatibility table

2018/1/8

ECMAScript 6, ES6


ECMAScript 是由 ECMA 組織通過的 ECMA-262 標準化的腳本式程式語言,在瀏覽器上被廣泛使用的 JavaScript 就是 ECMAScript 的其中一種實作的成品。換句話說,ECMAScript 是語言的標準規格,而比較常聽到的 JavaScript 是一這個語言標準的其中一種實作。


一般初學者學習的 JavaScript 是 ECMAScript 3.0 的語法,目前最新的標準 ECMAScript 6 (ES6) ,是在 2015年6月 發布,其後因標準每年6月固定的更新機制,2015, 2016, 2017 這三年都有發表新版的 ES6,所以後來就標記為 ES2015, ES2016, ES2017。



ECMAScript 和 JavaScript 的關係


1995/6 Sun 與 Netscape 合作發表了 JavaScript,1996/3 Netscape 發表支援 JavaScript 的網頁瀏覽器 Netscape Navigator 2.0,因為該語言的成功,1996/8 MS IE 3.0 支援了 JavaScript。


1996/11 Netscape 將 JavaScript 交給 ECMA 進行標準化,但由於 Java 是 Sun 的商標,JavaScript 是 Netscape 的商標,所以改名為 ECMAScript,也讓該規範更具有開放及中立性。


ECMAScript 是語言的標準規格,而比較常聽到的 JavaScript 是一這個語言標準的其中一種實作,還有其他實作的語言,包含 Adobe ActionScript, TypeScript 等等。


花了 15 年的 ES6 標準


1997/6 ECMA 發佈了 ECMAScript 1.0 後,1998/6 及 1999/12 分別發表了 ECMAScript 2.0 及 3.0,3.0 是非常成功也是最普及的版本。


2000年開始,開始制訂ES4,直到 2007/10 發表了 ES4 草案。但 ECMA Technical Committee 39 (TC39) 的部分成員因 ES4 太過激進,不願意通過這個版本。


2008/7 開會決定中止 ES4 的規格,將其中以小部分放到 ECMAScript 3.1 發佈,將專案代號改為 Harmony,後來ECMAScript 3.1 改為 ECMAScript 5。


2011/6 發表 ECMAScript 5.1,同時是 ISO/IEC 16262:2011 標準。


2013/2 發表了 ECMAScript 6 草案


2015/6 正式通過 ECMAScript 6


ES2015, ES2016, ES2017


2015 通過的 ES6 就是 ECMA2015,這是 ES6 的第一個正式版本。因 ECMA 委員會認為,ES6 的規範已經成熟了,後續會有微幅的修訂,為了讓這個修訂過程標準化,決定每年 6 月就發布一次該年度的最新正式版本。


所以在 2016/6 及 2017/6 都有新版本的 ES2016 及 ES2017,但其實都是 ES6 的標準。


ES6 可說是新一代JavaScript 語言的代名詞。


ECMAScript 6 compatibility table


Mozilla 給開發者的網頁技術文件 JavaScript


ES6 - Quick Guide


References


ECMAScript 6 簡介


ECMAScript wiki

2017/12/25

Oracle Fn Project


Oracle發佈了Fn Project ,Fn是一個新的 open source、與雲平台無關的Serverless平台,未來 Fn 的發展將會作為 Oracle Cloud 服務的一部分。


Fn 是一個 container native serverless platform,可稱為 FaaS (Function-as-a-service) 的平台,目前支援了 Java, Go, Ruby, Python, PHP, and Node.js。Fn 是以 Go 撰寫的,包含四個主要的元件: Fn Server、FDK、Flow和Fn負載平衡器。


Fn 最基本的執行單元是 containers,每個 function 定義都會獨立封裝在自己的 container 中,目前以 docker 為最主要的 container 平台。


Programmer 可利用某個程式語言透過 FDK(Function Development Kit) 撰寫 function,然後部署到 Fn Server,Fn Flow 提供工作流程及順序的工具,用以實現更高階的商業邏輯。


Fn Project 這篇文章的內容得知,當 Fn 啟動時,就會產生新的 docker image,並將 function 移動到該 image 內進行運算。


Serverless Architecture


Serverless 架構是由 Amazon 帶領的一種架構,也有人說是新型態的 Internet OS,採用 Serverless 架構,一方面除了改變了 IT 架構,沒有了 Server 甚至連 VM 都沒有,開發者只需要設計 function 跟 流程,其他的部分就交給 serverless 架構服務的提供者處理,不需要煩惱 conncurrent 使用量的硬體問題。


另一方面是成本的考量,Serverless 的計價方式是依照使用量來決定的,而不是 VM 的資源,換句話說,只要使用次數很低,Serverless 的成本會比使用 VM 還低,但一旦使用量持續都很大,那麼 VM 就會比 Serverless 划算。


可口可樂的Serverless之旅 的文章為例,可口可樂北美集團的開發團隊,在開發飲料自動販賣機的會員忠誠行銷計畫時,導入無伺服器運算架構。原本用了 6 台 EC2 T2.M,包含作業系統、系統管理、資安軟體、自動化部署軟體等成本,一年下來總體成本為12,864美元;然而在使用AWS Lambda的情況下,使用量每月3千萬次,一年總體成本是4,490美元,成本省了65%。


一旦達到每月8千萬次的使用量,Lambda的總體成本就會與EC2相當,使用量大於每月8千萬次,Lambda在價格上就沒有優勢了。但因為大多數的促銷活動都有長尾效應,例如可口可樂的行銷計畫可能一推出吸引很多人使用,但過了一段時間可能使用量就降低,因此採用Serverless架構是絕佳的決定。


References


Oracle開源Fn,加入Serverless之爭


Announcing Fn–An Open Source Serverless Functions Platform


fn project github




AWS Lambda大躍進的真義:Part 1


AWS Lambda大躍進的真義:Part 2


serverless


淺析 serverless 架構與實作

2017/12/18

Elastic Stack (ELK)


Elastic 公司推出的 ELK 模組,E: Elasticsearch L: Logstash K: Kibana,Logstash 為資料收集工具,它將數據進行過濾和格式化,處理後將資料傳送給 ElasticSearch 儲存資料,最後再由 Kibana 前端網頁介面將資料由 ElasticSearch 取出來,可進行搜尋或是繪製圖表。Logstash 和 Elasticsearch 是用 Java 寫的,kibana 使用 node.js。


ELK 在 5.0 版後,加入 Beats 套件後稱為 Elastic Stack。Beats 是安裝在被監控端 Server 的監控 Agent,能夠直接將資料送給 Elasticsearch 或是透過 Logstash 轉換資料後,發送給 Elasticsearch。


安裝 Elastic Stack 建議用以下安裝順序,且建議都使用相同的版本


  1. Elasticsearch

    • X-Pack for Elasticsearch
  2. Kibana

    • X-Pack for Kibana
  3. Logstash
  4. Beats
  5. Elasticsearch Hadoop

X-Pack 是 Elastic Stack extension,將 security, alerting, monitoring, reporting, machine learning 及 graph capability 合併在一個套件中。


這幾個套件之間的關係如下


比較簡單的方式,可以將 Beats 直接連接到 Elasticsearch,再交給 Kibana UI使用。


Logstash 增加了資料轉換的功能,也加強了整個平台的穩定性。


ref: Deploying and Scaling Logstash


以 docker 測試


啟動一個測試用的 docker machine,安裝了 CentOS 7 及 sshd


#elasticsearch TCP 9200
#logstash beats input TCP 5044
#kibana web TCP 5601

docker run -d \
 -p 10022:22\
 -p 80:80\
 -p 9200:9200\
 -p 5044:5044\
 -p 5601:5601\
 --sysctl net.ipv6.conf.all.disable_ipv6=1\
 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name elktest centosssh /usr/sbin/init

Elasticsearch


ref: Installing the Elastic Stack


ref: 如何在 CentOS 7 上安装 Elastic Stack


Elasticsearch 有下列幾種套件的安裝方式:zip/tar.gz, deb, rpm, msi, docker。


首先安裝 OpenJDK


yum -y install java-1.8.0-openjdk

設定環境變數


vi /etc/profile


export JAVA_HOME=/usr/lib/jvm/java-openjdk
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

source /etc/profile



安裝 Elasticsearch PGP Key


rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch

接下來有兩種方式,一種是設定 RPM Respository,或是直接下載 RPM


  • RPM Repository

vi /etc/yum.repos.d/elasticsearch.repo


[elasticsearch-5.x]
name=Elasticsearch repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

安裝並啟動 elasticsearch


sudo yum install -y elasticsearch

systemctl daemon-reload
systemctl enable elasticsearch.service

systemctl start elasticsearch.service
systemctl stop elasticsearch.service

查看啟動 log


journalctl -f
journalctl --unit elasticsearch

  • RPM

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.3.rpm
sudo rpm --install elasticsearch-5.6.3.rpm

啟動後,以 netstat 檢查 server


> netstat -napl|grep java
tcp        0      0 127.0.0.1:9200          0.0.0.0:*               LISTEN      439/java
tcp        0      0 127.0.0.1:9300          0.0.0.0:*               LISTEN      439/java

TCP 9200 是接收 HTTP Request 的 port,也是 elasticsearch 對外服務的 port
TCP 9300 是給多個 elasticsearch nodes 之間溝通使用的




安裝後的相關檔案路徑


  1. /usr/share/leasticsearch/


    elasticsearch home directory

  2. /etc/elasticsearch/*.conf


    config 目錄

  3. /etc/sysconfig/elasticsearch


    環境變數,包含 heap size, file descriptors

  4. /var/lib/elasticsearch/


    data files 的目錄

  5. /var/log/elasticsearch/*.log


    log files

  6. /usr/share/elasticsearch/plugins/


    Plugin files location

  7. /etc/elasticsearch/scripts/


    script files




設定檔的位置在 /etc/elasticsearch/elasticsearch.yml


參考 ES節點memory lock重要性與實現方式 的說明,系統發生 memory swap 時,會嚴重影響到節點的效能及穩定性,導致 Java GC 由數毫秒變成幾分鐘,因此要避免 memory swap。


note: 因為目前是用 docker 測試,docker 在 ulimit 的設定有些限制跟問題,這個部分的設定就跳過,但正視環境必須要處理這個問題。


用下列指令檢查各節點有沒有啟用 memory lock


# curl -XGET 'localhost:9200/_nodes?filter_path=**.mlockall&pretty'
{
  "nodes" : {
    "AhjDVEQJQL6avw43nl3AFQ" : {
      "process" : {
        "mlockall" : false
      }
    }
  }
}

vim /etc/elasticsearch/elasticsearch.yml


# 取消這一行的註解
bootstrap.memory_lock: true

同時要修改系統設定,要不然啟動時會出現 memory locking requested for elasticsearch process but memory is not locked 這樣的錯誤訊息


vi /etc/security/limits.conf


* soft memlock unlimited
* hard memlock unlimited
elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited

ulimit -l unlimited
systemctl restart elasticsearch

Kibana


安裝 Elasticsearch PGP Key,剛剛在 Elasticsearch 安裝過了,就不用再安裝一次


rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch

  • 以 RPM Repository 安裝

note: 這個 repository 跟剛剛的 elasticsearch.repo 是一樣的,不用重複,直接跳到下面安裝的步驟。


vi /etc/yum.repos.d/kibana.repo


[kibana-5.x]
name=Kibana repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

安裝 kibana


sudo yum install -y kibana

systemctl daemon-reload
systemctl enable kibana.service

systemctl start kibana.service
systemctl stop kibana.service

查看啟動 log


journalctl -f
journalctl --unit kibana

啟動後,以 netstat 檢查 server


> netstat -napl|grep node
tcp        0      0 127.0.0.1:5601          0.0.0.0:*               LISTEN      340/node
tcp        0      0 127.0.0.1:43968         127.0.0.1:9200          ESTABLISHED 340/node
tcp        0      0 127.0.0.1:43970         127.0.0.1:9200          ESTABLISHED 340/node
unix  3      [ ]         STREAM     CONNECTED     19517    340/node

TCP Port 5601 是 kibana 對外服務的網頁 Port




安裝後的相關檔案路徑


  1. /usr/share/kibana


    kibana home

  2. /etc/kibana/


    設定檔目錄

  3. /var/lib/kibana/


    資料 data files 目錄

  4. /usr/share/kibana/optimize/


    Transpiled source code

  5. /usr/share/kibana/plugins/


    plugin 目錄


kibana 的服務網頁為 http://localhost:5601/




也可以安裝 Nginx 並設定reverse proxy,就可改用 80 Port 存取 kibana。


yum -y install epel-release
yum -y install nginx httpd-tools

cd /etc/nginx/
vim nginx.conf

刪除 server { } 這個區塊。


vim /etc/nginx/conf.d/kibana.conf


server {
    listen 80;
    server_name elk-stack.co;
    auth_basic "Restricted Access";
    auth_basic_user_file /etc/nginx/.kibana-user;
    location / {
        proxy_pass http://localhost:5601;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

sudo htpasswd -c /etc/nginx/.kibana-user admin

輸入密碼


# 測試 nginx 的設定
nginx -t

# 啟動 nginx
systemctl enable nginx
systemctl start nginx

檢查 nginx service


> netstat -napltu | grep nginx
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      510/nginx: master p

kibana 的服務網頁為 http://localhost/



Logstash


安裝 Elasticsearch PGP Key,剛剛在 Elasticsearch 安裝過了,就不用再安裝一次


rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch

  • 以 RPM Repository 安裝

note: 這個 repository 跟剛剛的 elasticsearch.repo 是一樣的,不用重複,直接跳到下面安裝的步驟。


vi /etc/yum.repos.d/logstash.repo


[logstash-5.x]
name=Elastic repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

安裝 Logstash


sudo yum install -y logstash

  • RPM

wget https://artifacts.elastic.co/downloads/logstash/logstash-5.6.3.rpm
sudo rpm --install logstash-5.6.3.rpm



修改 Logstash 設定,建立 beat input,使用 SSL,也可以不使用 SSL。


設定 openssl


cd /etc/pki/tls
vim openssl.cnf

在 v3_ca 的區塊,增加 server name


[ v3_ca ]
# Server IP Address
subjectAltName = IP: 127.0.0.1

產生 CA 證書到 /etc/pki/tls/certs/ 和 /etc/pki/tls/private/


openssl req -config /etc/pki/tls/openssl.cnf -x509 -days 3650 -batch -nodes -newkey rsa:2048 -keyout /etc/pki/tls/private/logstash-forwarder.key -out /etc/pki/tls/certs/logstash-forwarder.crt

設定 Logstash的 input, filter, output


vim /etc/logstash/conf.d/filebeat-input.conf


input {
  beats {
    port => 5044
    ssl => true
    ssl_certificate => "/etc/pki/tls/certs/logstash-forwarder.crt"
    ssl_key => "/etc/pki/tls/private/logstash-forwarder.key"
  }
}

使用 grok filter 解析 syslog 文件


vim /etc/logstash/conf.d/syslog-filter.conf


filter {
  if [type] == "syslog" {
    grok {
      match => { "message" => "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{DATA:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{GREEDYDATA:syslog_message}" }
      add_field => [ "received_at", "%{@timestamp}" ]
      add_field => [ "received_from", "%{host}" ]
    }
    date {
      match => [ "syslog_timestamp", "MMM  d HH:mm:ss", "MMM dd HH:mm:ss" ]
    }
  }
}

輸出到 elasticsearch


vim /etc/logstash/conf.d/output-elasticsearch.conf


output {
  elasticsearch { hosts => ["localhost:9200"]
    hosts => "localhost:9200"
    manage_template => false
    index => "%{[@metadata][beat]}-%{+YYYY.MM.dd}"
    document_type => "%{[@metadata][type]}"
  }
}



啟動


systemctl daemon-reload
systemctl enable logstash.service

systemctl start logstash.service
systemctl stop logstash.service

查看啟動 log


journalctl -f
journalctl --unit logstash

啟動後,以 netstat 檢查 server


# netstat -naptul |grep java
tcp        0      0 127.0.0.1:9600          0.0.0.0:*               LISTEN      788/java
tcp        0      0 0.0.0.0:5044            0.0.0.0:*               LISTEN      788/java
tcp        0      0 127.0.0.1:9200          0.0.0.0:*               LISTEN      196/java
tcp        0      0 127.0.0.1:9300          0.0.0.0:*               LISTEN      196/java
tcp        0      0 127.0.0.1:9200          127.0.0.1:43986         ESTABLISHED 196/java
tcp        0      0 127.0.0.1:44280         127.0.0.1:9200          ESTABLISHED 788/java
tcp        0      0 127.0.0.1:9200          127.0.0.1:44280         ESTABLISHED 196/java
tcp        0      0 127.0.0.1:9200          127.0.0.1:43988         ESTABLISHED 196/java

TCP Port 5044(SSL) 是 logstash 對外服務的網頁 Port


Beats


Bests 是在客戶端機器上收集資料的 Agent,可將資料發送到 Logstash 或是 Elasticsearch,目前有四種 Beats


  1. Packetbeat: real-time 分析網路封包,搭配 elasticsearch 就可當作 application monitoring 及 performance analytics 的工具。目前可解析以下這些 protocol 的封包: ICMP (v4, v6), DNS, HTTP, AMQP 0.9.1, Cassandra, MySQL, PostgreSQL, Redis, Thrift-RPC, MongoDB, Memcache
  2. Metricbeat: 收集 OS 及 一些 Service 的統計指標,目前支援 Apache, HAProxy, MongoDB, MySQL, Nginx, PostgreSQL, Redis, System, Zookeeper
  3. Filebeat: 檔案類型的 log file
  4. Winlogbeat: Windows event log,包含 application, hardware, security, system events
  5. Heartbeat: 定時檢查 service 狀態,只會知道 service 是 up or down


  • 以 RPM Repository 安裝

使用剛剛的 elasticsearch.repo。


vi /etc/yum.repos.d/elasticsearch.repo


[elasticsearch-5.x]
name=Elasticsearch repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

安裝 filebeat


sudo yum install -y filebeat

  • RPM 直接安裝

wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-5.6.3-x86_64.rpm
sudo rpm -vi filebeat-5.6.3-x86_64.rpm



filebeat 預設以 output.elasticsearch 為輸出對象,資料寫入到 localhost:9200。以下修改為 監控 /var/log/secure (ssh) 及 /var/log/messages (server log),並輸出到 logstash


vim /etc/filebeat/filebeat.yml


filebeat.prospectors:

- input_type: log
  paths:
    - /var/log/secure
    - /var/log/messages
  document_type: syslog

#--------- Elasticsearch output --------------
#output.elasticsearch:
# Array of hosts to connect to.
#  hosts: ["localhost:9200"]

#--------- Logstash output --------
output.logstash:
  # The Logstash hosts
  hosts: ["localhost:5044"]
  bulk_max_size: 1024
  #ssl.certificate_authorities: ["/etc/pki/tls/certs/logstash-forwarder.crt"]
  template.name: "filebeat"
  template.path: "filebeat.template.json"
  template.overwrite: false

如果剛剛有設定 logstash beat input 有包含 SSL 的部分,必須將 logstash 的 /etc/pki/tls/certs/logstash-forwarder.crt 複製到客戶端機器上,並將這個設定打開。


  ssl.certificate_authorities: ["/etc/pki/tls/certs/logstash-forwarder.crt"]

設定測試


# /usr/bin/filebeat.sh -configtest -e
2017/11/03 05:58:10.538291 beat.go:297: INFO Home path: [/usr/share/filebeat] Config path: [/etc/filebeat] Data path: [/var/lib/filebeat] Logs path: [/var/log/filebeat]
2017/11/03 05:58:10.538350 beat.go:192: INFO Setup Beat: filebeat; Version: 5.6.3
2017/11/03 05:58:10.538463 metrics.go:23: INFO Metrics logging every 30s
2017/11/03 05:58:10.539115 logstash.go:90: INFO Max Retries set to: 3
2017/11/03 05:58:10.539679 outputs.go:108: INFO Activated logstash as output plugin.
2017/11/03 05:58:10.539884 publish.go:300: INFO Publisher name: c0ba72624128
2017/11/03 05:58:10.540376 async.go:63: INFO Flush Interval set to: 1s
2017/11/03 05:58:10.540415 async.go:64: INFO Max Bulk Size set to: 1024
Config OK

啟動 filebeat


sudo systemctl enable filebeat
sudo systemctl start filebeat

查看啟動 log


journalctl -f
journalctl --unit filebeat

References


ELSstack 中文指南


Elastic Stack and Product Documentation


Logstash Reference Docs


logstash日誌分析的配置和使用


How To Install Elasticsearch, Logstash, and Kibana (ELK Stack) on Ubuntu 14.04


Elasticsearch 5.0和ELK/Elastic Stack指南




Elasticsearch 權威指南


ELKstack 中文指南


用 ElasticSearch + FluentD 打造 Log 神器與數據分析工具


Collecting Logs In Elasticsearch With Filebeat and Logstash


ELK+Filebeat 集中式日誌解決方案詳解




Handling stack traces in Elasticsearch Logstash Kibana (ELK)


Handling Stack Traces with Logstash



2017/12/11

Fluentd

Fluentd 將 data source 及 backend system 分離,提供兩者之間的一個 Unified Logging Layer,可讓 developers 及 data analysts 能同時使用多種資料源,同時也解決格式錯誤的資料所造成的系統變慢或解譯錯誤的問題。


Fluentd 有三種版本,全部都是以 Apache2 License 釋出。


  1. Fluentd


    社群版本,只能用 ruby gems 安裝,沒有 init scripts,如果想要修改 Fluentd 或是做更多事情,可以用這個社群版

  2. ta-agent


    這是 Treasure Data, Inc 這家公司維護並測試的版本,可直接用 rpm/deb/dmg 套件安裝,安裝時同時安裝了一些預設設定值。如果是第一次使用 Fluentd,建議安裝 ta-agent。

  3. Fluent Bit


    Fluent Bit 是 Fluentd 的 lightweight data forwarder,用在 forward 資料給 Fluentd aggregators。可安裝在 embedded system 或是嵌入到 server 系統中。


Architecture


Fluentd 的架構圖為



由於 data inputs 及 output 透過 Fluentd 中繼資料,Fluentd 這個 Unified Logging Layer 野食作為 pluggable 架構,可不斷地增加不同的 inputoutput plugins,目前已經有超過 500+ 的 plugins。


假設有 M 種 data input,N 種 data output,pluggable 架構可讓原本複雜度 O(M*N) 的系統,變成 O(M+N) 的系統。


安裝


Download Fluentd 有列出所有安裝方式的資訊。我們選擇 Installing Fluentd Using rpm Package 安裝到 CentOS 7。


產生一個新的有 sshd 的 docker machine


docker run -d \
 -p 10022:22\
 -p 80:80\
 -p 8888:8888\
 --sysctl net.ipv6.conf.all.disable_ipv6=1\
 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name fluentd centosssh /usr/sbin/init

在安裝前,Before Installing Fluentd 必須要先處理幾項系統設定。


  1. NTP


    要同步時間,確保 log 的 timestamp 是正確的


    CentOS 7 修改 timezone,校正時間


    timedatectl set-timezone Asia/Taipei
    /usr/sbin/ntpdate time.stdtime.gov.tw && /sbin/hwclock -w
  2. Max # of File Descriptors


    ulimit -n 65535

    vi /etc/security/limits.conf


    root soft nofile 65535
    root hard nofile 65535
    * soft nofile 65535
    * hard nofile 65535
  3. Network Kernel Parameters


    解決 TCP_WAIT 的問題,(如果在 docker 測試,會無法修改 kernel 參數,跳過這個步驟就好了,參考這邊的說明 對docker container進行內核參數調優


    vi /etc/sysctl.conf


    net.ipv4.tcp_tw_recycle = 1
    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.ip_local_port_range = 10240 65535

    sysctl -p 或是 reboot




以 script 安裝 FluentD,daemon 名稱為 td-agent


curl -L https://toolbelt.treasuredata.com/sh/install-redhat-td-agent2.sh | sh

安裝後會增加 /etc/yum.repos.d/td.repo,以及 td-agent service


啟動 daemon


systemctl enable td-agent
systemctl status td-agent

systemctl start td-agent

/etc/init.d/td-agent start
/etc/init.d/td-agent stop
/etc/init.d/td-agent restart
/etc/init.d/td-agent status

設定檔在 /etc/td-agent/td-agent.conf
預設是由 HTTP 接收 logs 轉至 stdout /var/log/td-agent/td-agent.log


發送測試資料


curl -X POST -d 'json={"json":"message"}' http://localhost:8888/debug.test

Use Cases


  • Centralized App Logging
    收集不同語言實作的 Applcation 的 Log

  • Log Management & Search
    Fluentd + Elasticsearch 的整合替代 Splunk

  • Data Analysis
    將 Log 儲存到 Hadoop 或 MongoDB,以供後續分析處理

  • Data Archiving
    將 Log 儲存到 Amazon S3/Riak/GlusterFS Logs

  • Stream Processing

  • Windows Event Collection
    收集 Windows Event Logs (目前 stable 版本 v0.12 還不支援 Windows,要到 v0.14 才有支援)

  • IoT Data Logger


    Cloud Data Logger by Raspberry Pi 說明可在 Raspberry Pi 整合其他 Sensor 後,透過 Fluentd 收集資料。


Life of a Fluentd event


以實例解釋 event 是如何倍 Fluentd 處理的,包含 Setup, Inputs, Filters, Matches, and Labels


使用 inhttp 及 outstdout plugins 解釋 events cycle,首先修改 /etc/td-agent/td-agent.conf


# listening for HTTP Requests
<source>
  @type http
  port 8888
  bind 0.0.0.0
</source>

# print the data arrived on each incoming request to standard output
<match test.cycle>
  @type stdout
</match>

發送兩個 curl 測試


# curl -X POST -d 'json={"json":"message"}' http://localhost:8888/debug.test

# curl -i -X POST -d 'json={"action":"login","user":2}' http://localhost:8888/test.cycle
HTTP/1.1 200 OK
Content-type: text/plain
Connection: Keep-Alive
Content-length: 0

tail -f /var/log/td-agent/td-agent.log


2017-10-31 15:15:40 +0800 [info]: adding match pattern="test.cycle" type="stdout"
2017-10-31 15:15:40 +0800 [info]: adding source type="http"
2017-10-31 15:15:40 +0800 [info]: using configuration file: <ROOT>
  <source>
    @type http
    port 8888
    bind 0.0.0.0
  </source>
  <match test.cycle>
    @type stdout
  </match>
</ROOT>
2017-10-31 15:15:48 +0800 [warn]: no patterns matched tag="debug.test"
2017-10-31 15:15:58 +0800 test.cycle: {"action":"login","user":2}

Event structure

Fluentd event 包含 tag, time, record 三個部分


  • tag: event 來自哪裡
  • time: Epoch time,event 發生時間
  • record: log content,JSON object

以 apache log 為例,利用 in_tail 會由一行一行的 text line log 產生 event


192.168.0.1 - - [28/Feb/2013:12:00:00 +0900] "GET / HTTP/1.1" 200 777

tag: apache.access # set by configuration
time: 1362020400   # 28/Feb/2013:12:00:00 +0900
record: {"user":"-","method":"GET","code":200,"size":777,"host":"192.168.0.1","path":"/"}

tag 是由 a.b.c 這樣的字串組成的,用 "." 組合不同部分的字串


設定檔 td-agent.conf


  • source: input source

標準 input 有兩個: http 及 forward,可同時使用


http 將 fluentd 轉變為 HTTP endpoint,由 HTTP 接收 event message


forward 將 fluentd 轉變為 TCP endpoint,接收 TCP packets


ex:


# Receive events from 24224/tcp
# This is used by log forwarding and the fluent-cat command
<source>
  @type forward
  port 24224
</source>

# http://this.host:8888/myapp.access?json={"event":"data"}
<source>
  @type http
  port 8888
</source>

  • match: output destination

比對 event 的 tag,並處理符合定義 tag 的 event


fluentd 的 stdout output plugin 為 file 及 forward


ex:


# Match events tagged with "myapp.access" and
# store them to /var/log/fluent/access.%Y-%m-%d
# Of course, you can control how you partition your data
# with the time_slice_format option.
<match myapp.access>
  @type file
  path /var/log/fluent/access
</match>

match 後面的參數有以下規則,依照在設定檔中的順序進行比對


    • matches a single tag part

    ex: a.* matches a.b
    a.* not match a or a.b.c

  1. ** matches zero or more tag parts


    a.** matches a, a.b and a.b.c

  2. {X,Y,Z} matches X, Y, or Z, where X, Y, and Z are match patterns


    {a,b} matches a and b
    a.{b,c}.*
    a.{b,c.**}

  3. 可用 填寫多個 patterns


    match a and b
    match a, a.b, a.b.c, and b.d


  • filter: 決定 event processing pipelines

Input -> filter 1 -> ... -> filter N -> Output


ex:


# http://this.host:9880/myapp.access?json={"event":"data"}
<source>
  @type http
  port 9880
</source>

<filter myapp.access>
  @type record_transformer
  <record>
    host_param "#{Socket.gethostname}"
  </record>
</filter>

<match myapp.access>
  @type file
  path /var/log/fluent/access
</match>

event 處理過程


收到 {"event":"data"}
-> 送到 record_transformer filter
-> 增加 "host_param" 欄位
-> {"event":"data","host_param":"webserver1"}
-> 送到 file output

  • system: 設定系統參數

<system>
  # equal to -qq option
  log_level error
  # equal to --without-source option
  without_source
  # suppress_repeated_stacktrace
  # emit_error_log_interval
  # suppress_config_dump
  
  # fluentd’s supervisor and worker process names
  process_name fluentd1
</system>

  • label: group output 及 filter for internal routing

<label @SYSTEM>
  <filter var.log.middleware.**>
    @type grep
    # ...
  </filter>
  <match **>
    @type s3
    # ...
  </match>
</label>

  • @include: include other files

# Include config files in the ./config.d directory
@include config.d/*.conf

Processing Events

在設定 Setup 後,Router Engine 就已經包含了幾個基本的 rules,內部會經過幾個步驟處理 Event。


  • Filters

可用來設定一個 rule,決定要不要接受這個 event


ex: filter test.cycle 放棄不處理 logout,這是用 @grep 處理的,判斷 action 的部分,有沒有 "logout" 這個字串


<source>
  @type http
  port 8888
  bind 0.0.0.0
</source>

<filter test.cycle>
  @type grep
  exclude1 action logout
</filter>

<match test.cycle>
  @type stdout
</match>

測試


# curl -i -X POST -d 'json={"action":"login","user":2}' http://localhost:8888/test.cycle
HTTP/1.1 200 OK
Content-type: text/plain
Connection: Keep-Alive
Content-length: 0

# curl -i -X POST -d 'json={"action":"logout","user":2}' http://localhost:8888/test.cycle
HTTP/1.1 200 OK
Content-type: text/plain
Connection: Keep-Alive
Content-length: 0

結果在 log 裡面只有看到 login


2017-10-31 15:50:55 +0800 test.cycle: {"action":"login","user":2}

Labels

可用來定義新的 Routing sections,且不遵循 top-bottom 的順序,類似 linked references 的行為。


ex: 在 source 增加了 @label,表示要跳到 @STAGING 處理 event,而不是用上面的 filter


<source>
  @type http
  bind 0.0.0.0
  port 8880
  @label @STAGING
</source>

<filter test.cycle>
  @type grep
  exclude1 action login
</filter>

<label @STAGING>
  <filter test.cycle>
    @type grep
    exclude1 action logout
  </filter>

  <match test.cycle>
    @type stdout
  </match>
</label>

Buffers

在範例中,使用 stdout 是 non-buffered output,但在正式環境,會需要對 output 增加 buffer,例如 forward, mongodb, s3 ...


buffered output plugins 會儲存收到的 events 到 buffers,並在達到 flush condition 時,再將資料一次寫入目標。換句話說,database 可能不會馬上看到新進的 event。


Execution unit

Fluentd events 預設是在 input plugin thread 中處理的,例如 intail -> filtergrep -> outstdout pipeline,就是在 intail 的 thread 中處理的。filtergrep 及 outstdout 並沒有自己的 thread。


但 buffered output plugin 中,另外有一個自己的 thread 可處理 flushing buffer。


Sample


Collecting Tomcat logs using Fluentd and Elasticsearch


fluentd-catch-all-config


Tomcat容器日誌收集方案fluentd+elasticsearch+kilbana


安裝 fluentd 的 elasticsearch plugin


td-agent-gem install fluent-plugin-elasticsearch

定義 tomcat catalina.out 的 source


<source>
  @type tail
  format none
  path /var/log/tomcat*/localhost_access_log.%Y-%m-%d.txt
  pos_file /var/lib/google-fluentd/pos/tomcat.pos
  read_from_head true
  tag tomcat-localhost_access_log
</source>

<source>
  @type tail
  format multiline
  # Match the date at the beginning of each entry, which can be in one of two
  # different formats.
  format_firstline /^(\w+\s\d+,\s\d+)|(\d+-\d+-\d+\s)/
  format1 /(?<message>.*)/
  path /var/log/tomcat*/catalina.out,/var/log/tomcat*/localhost.*.log
  pos_file /var/lib/google-fluentd/pos/tomcat-multiline.pos
  read_from_head true
  tag tomcat.logs
</source>

<match tomcat.logs>
    @type elasticsearch
    host localhost
    port 9200
    logstash_format true
    logstash_prefix tomcat.logs
    flush_interval 1s
</match>

References


fluentd architecture


用 ElasticSearch + FluentD 打造 Log 神器與數據分析工具




logstash + kibana - Make sense of a mountain of logs


LogStash::Inputs::Syslog 性能測試與優化




使用LogHub進行日誌實時採集


Docker日志收集新方案:fluentd-pilot