一個非侵入的Go事務管理庫——工作原理

在上一篇文章“一個非侵入的Go事務管理庫——如何使用”中,我講述了如何使用事務庫。有些讀者可能讀過”清晰架構(Clean Architecture)的Go微服務: 事物管理” ,其中描述了事務管理系統的舊版本。那篇文章和本文之間會有一些重疊。因為大多數人可能還沒有讀過那篇文章或者即使讀了也忘記了它的內容。因此為了照顧多數讀者,本文還是從頭開始(假設你沒有讀過前文)。如果你讀過,那你可以直接跳過熟悉的部分。

好的事務庫對於使用它的應用程序是透明的。在Go的“sql”庫中,有兩種類型的數據庫鏈接,“sql.DB”和“sql.Tx”。當你不需要事務支持時,使用“sql.DB”;否則使用“sql.Tx”。為了讓這兩種不同場景共享相同的持久層代碼,我們需要對數據庫鏈接進行一個封裝來同時支持這兩種場景。我從”db transaction in golang” 里得到了這個想法。

數據庫層的接口

數據庫層是事務管理庫中處理數據庫訪問的最低層。應用程序不需要修改該層,只有事務管理庫需要這樣做。

數據庫訪問封裝

下面是可同時支持事務和非事務操作的共享數據庫訪問接口, 它在“gdbc.go”中定義。

// SqlGdbc (SQL Go database connection) is a wrapper for SQL database handler ( can be *sql.DB or *sql.Tx)
// It should be able to work with all SQL data that follows SQL standard.
type SqlGdbc interface {
	Exec(query string, args ...interface{}) (sql.Result, error)
	Prepare(query string) (*sql.Stmt, error)
	Query(query string, args ...interface{}) (*sql.Rows, error)
	QueryRow(query string, args ...interface{}) *sql.Row
	// If need transaction support, add this interface
	Transactioner
}

// Transactioner is the transaction interface for database handler
// It should only be applicable to SQL database
type Transactioner interface {
	// Rollback a transaction
	Rollback() error
	// Commit a transaction
	Commit() error
	// TxEnd commits a transaction if no errors, otherwise rollback
	// txFunc is the operations wrapped in a transaction
	TxEnd(txFunc func() error) error

}

它有兩部分。一個是數據庫接口,它包含了常規的數據庫操作,如查詢表、更新表記錄。另一個事務接口,它包含里支持事務所需要的函數,如“提交”和“回滾”。“SqlGdbc”接口是兩者的結合。該接口將用於連接數據庫。

數據庫訪問接口的實現

下面是數據庫訪問接口的代碼實現。它在“sqlConnWrapper.go”文件中。它定義了兩個結構體,“SqlDBTx”是對“sql.DB”的封裝,將被非事務函數使用。“SqlConnTx”是對“sql.Tx”的封裝,將被事務函數使用。

// SqlDBTx is the concrete implementation of sqlGdbc by using *sql.DB
type SqlDBTx struct {
	DB *sql.DB
}

// SqlConnTx is the concrete implementation of sqlGdbc by using *sql.Tx
type SqlConnTx struct {
	DB *sql.Tx
}

func (sdt *SqlDBTx) Exec(query string, args ...interface{}) (sql.Result, error) {
	return sdt.DB.Exec(query, args...)
}

func (sdt *SqlDBTx) Prepare(query string) (*sql.Stmt, error) {
	return sdt.DB.Prepare(query)
}

func (sdt *SqlDBTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
	return sdt.DB.Query(query, args...)
}

func (sdt *SqlDBTx) QueryRow(query string, args ...interface{}) *sql.Row {
	return sdt.DB.QueryRow(query, args...)
}

func (sdb *SqlConnTx) Exec(query string, args ...interface{}) (sql.Result, error) {
	return sdb.DB.Exec(query, args...)
}

func (sdb *SqlConnTx) Prepare(query string) (*sql.Stmt, error) {
	return sdb.DB.Prepare(query)
}

func (sdb *SqlConnTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
	return sdb.DB.Query(query, args...)
}

func (sdb *SqlConnTx) QueryRow(query string, args ...interface{}) *sql.Row {
	return sdb.DB.QueryRow(query, args...)
}

事務接口的實現

下面是“Transactioner”接口的代碼實現,它在文件 “txConn.go”中。我從”database/sql Tx — detecting Commit or Rollback”中得到這個想法。

因為“SqlDBTx”不支持事務,所以它的所有函數都返回“nil”。

// DB doesn't rollback, do nothing here
func (cdt *SqlDBTx) Rollback() error {
	return nil
}

//DB doesnt commit, do nothing here
func (cdt *SqlDBTx) Commit() error {
	return nil
}

// DB doesnt rollback, do nothing here
func (cdt *SqlDBTx) TxEnd(txFunc func() error) error {
	return nil
}

func (sct *SqlConnTx) TxEnd(txFunc func() error) error {
	var err error
	tx := sct.DB

	defer func() {
		if p := recover(); p != nil {
			log.Println("found p and rollback:", p)
			tx.Rollback()
			panic(p) // re-throw panic after Rollback
		} else if err != nil {
			log.Println("found error and rollback:", err)
			tx.Rollback() // err is non-nil; don't change it
		} else {
			log.Println("commit:")
			err = tx.Commit() // if Commit returns error update err with commit err
		}
	}()
	err = txFunc()
	return err
}

func (sct *SqlConnTx) Rollback() error {
	return sct.DB.Rollback()
}

func (sct *SqlConnTx) Commit() error {
	return sct.DB.Commit()
}

持久層的接口

在數據庫層之上是持久層,應用程序使用持久層來訪問數據庫表中的記錄。你需要定義一個函數在本層中實現對事務的支持。下面是持久層的事務接口,它位於“txDataService.go”文件中。

// TxDataInterface represents operations needed for transaction support.
type TxDataInterface interface {
	// EnableTx is called at the end of a transaction and based on whether there is an error, it commits or rollback the
	// transaction.
	// txFunc is the business function wrapped in a transaction
	EnableTx(txFunc func() error) error
}

以下是它的實現代碼。它只是調用下層數據庫中的函數“TxEnd()”,該函數已在數據庫層實現。下面的代碼不是事務庫的代碼(它是本文中惟一的不是事務庫中的代碼),你需要在應用程序中實現它。

func (uds *UserDataSql) EnableTx(txFunc func() error) error {
	return uds.DB.TxEnd(txFunc)
}

獲取數據庫鏈接的代碼

除了我們上面描述的調用接口之外,在應用程序中你還需要先獲得數據庫鏈接。事務庫中有兩個函數可以完成這個任務。

返回”SqlGdbc”接口的函數

函數”Build()”(在”factory.go”中)將返回”SqlGdbc”接口。根據傳入的參數,它講返回滿足”SqlGdbc”接口的結構,如果需要事務支持就是“SqlConnTx”,不需要就是“SqlDBTx”。如果你不需要在應用程序中直接使用數據庫鏈接,那麼調用它是最好的。

// Build returns the SqlGdbc interface. This is the interface that you can use directly in your persistence layer
// If you don't need to cache sql.DB connection, you can call this function because you won't be able to get the sql.DB
// in SqlGdbc interface (if you need to do it, call BuildSqlDB()
func Build(dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error) {
	db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	// check the connection
	err = db.Ping()
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	dt, err := buildGdbc(db, dsc)
	if err != nil {
		return nil, err
	}
	return dt, nil
}

func buildGdbc(sdb *sql.DB,dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error){
	var sdt gdbc.SqlGdbc
	if dsc.Tx {
		tx, err := sdb.Begin()
		if err != nil {
			return nil, err
		}
		sdt = &gdbc.SqlConnTx{DB: tx}
		log.Println("buildGdbc(), create TX:")
	} else {
		sdt = &gdbc.SqlDBTx{sdb}
		log.Println("buildGdbc(), create DB:")
	}
	return sdt, nil
}

返回數據庫鏈接的函數

函數”BuildSqlDB()”(在”factory.go”中)將返回”sql.DB”。它會忽略傳入的事務標識參數。應用程序在調用這個函數獲得數據庫鏈接后,還需要根據事務標識自己生成“SqlConnTx”或“SqlDBTx”。如果你需要在應用程序里緩存”sql.DB”,那麼你必須調用這個函數。

// BuildSqlDB returns the sql.DB. The calling function need to generate corresponding gdbc.SqlGdbc struct based on
// sql.DB in order to use it in your persistence layer
// If you need to cache sql.DB connection, you need to call this function
func BuildSqlDB(dsc *config.DatabaseConfig) (*sql.DB, error) {
	db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	// check the connection
	err = db.Ping()
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	return db, nil

}

局限性

首先,它只支持SQL數據庫的事務。如果你有一個NoSql數據庫,那麼它不支持(大多數NoSql數據庫不支持事務)。

其次,如果你的事務跨越數據庫(例如在不同的微服務之間),那麼它將無法工作。常用的做法是使用“Saga Pattern”。你可以為事務中的每個操作編寫一個補償操作,並在回滾階段逐個執行補償操作。在應用程序中添加“Saga”解決方案並不困難。你可能會問,為什麼不把“Saga”加到事務庫中呢? 這是一個有趣的問題。我覺得還是單獨為“Saga”建一個庫比較合適。

第三,它不支持嵌套事務(Nested Transaction),因此你需要手動確保在代碼中沒有嵌套事務。如果代碼庫不是太複雜,這很容易做到。如果你有一個非常複雜的代碼庫,其中有很多事務和非事務代碼混在一起,那麼你需要一個支持嵌套事務的解決方案。我沒有花時間研究如何添加嵌套事務,但它應該有一定的工作量。如果你對此感興趣,可以從”database/sql: nested transaction or save point support”開始。到目前為止,對於大多數場景,當前的解決方案可能是在代價不大的情況下的最佳方案。

如何擴展庫的功能

“SqlGdbc”接口沒有列出“sql”包中的所有函數,只列出我的應用程序中需要的函數。你可以輕鬆地擴展該接口以包含其他函數。

例如,如果需要將全鏈路跟蹤(詳情請見”Go微服務全鏈路跟蹤詳解”)擴展到數據庫中,則可能需要在上下文中傳遞到數據庫函數中。“sql”庫已經支持具有上下文的數據庫函數。你只需要找到它們並將它們添加到”SqlGdbc”接口中,然後在”sqlConnWrapper “中實現它們。然後在持久層中,需要使用上下文作為參數調用函數。

源碼:

完整源碼: “jfeng45/gtransaction”

索引:

1 “一個非侵入的Go事務管理庫——如何使用”

2 “清晰架構(Clean Architecture)的Go微服務: 事物管理”

3 “db transaction in golang”

4 “database/sql Tx — detecting Commit or Rollback”

5 “Applying the Saga Pattern – GOTO Conference”

6 “database/sql: nested transaction or save point support”

7 “Go微服務全鏈路跟蹤詳解”

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

再看rabbitmq的交換器和隊列的關係

最近又要用到rabbitmq,業務上要求服務器只發一次消息,需要多個客戶端都去單獨消費。但我們知道rabbitmq的機制里,每個隊列里的消息只能消費一次,所以客戶端要單獨消費信息,就必須得每個客戶端單獨監聽一個queue。所以我最終想實現的是服務端只聲明exchange,客戶端來創建queue和綁定exchange。但是在看各種rabbitmq博文和討論的時候,我覺得對exchange的模式和queue間的關係講的都不是很清楚。所以我決定自己驗證一下

fanout模式和direct模式

本文主要驗證fanout模式和direct模式下以上猜想是否可行。fanout模式就是大名鼎鼎的廣播模式了,只要queue綁定了fanout的交換器,就可以直接的收到消息,無需routingkey的參与。而direct模式就是通過routing key直接發送到綁定了同樣routing key的隊列中。那麼,在這兩種exchange的模式下,是否都可以實現服務端僅創建exchange,客戶端創建queue並綁定exchange呢?

Direct模式驗證

我們先把交換器、routingkey、隊列的名稱定義好:

  1. 交換器為directTest
  2. routingkey為direct_routing_key
  3. 隊列測試3個,首先測試Direct_test_queue_1,再行測試Direct_test_queue_2,再行測試Direct_test_queue_3

代碼使用spring boot框架快速搭建。我們先規劃好需要幾個類來完成這個事情:

  1. 針對生產者,需要RabbitmqConfig,用來配置exchange的
  2. 針對生產者,需要DirectRabbitSender,用來實現Direct模式的消息發送
  3. 針對消費者,需要DirectConsumerOne,來測試第一個隊列Direct_test_queue_1生成和消息接收
  4. 針對消費者,需要DirectConsumerTwo,來測試第二個隊列Direct_test_queue_2生成和消息接收
  5. 針對消費者,需要DirectConsumerThree,來測試第三個隊列Direct_test_queue_3生成和消息接收
  6. 我們還需要一個測試類RabbitmqApplicationTests,用於測試消息的發送和接收

rabbitmq先配置一個DirectExchange

@Bean
DirectExchange directExchange(){
    return new DirectExchange("directTest", true, false);
}

我們可以看到Direct交換器的名稱定義為了directTest,這時候還未綁定任何的隊列。啟動程序,若我們的設想沒錯,則rabbitmq中應該已經生成了directTest的exchange。

Bingo!directTest交換器成功創建。接下來,我們去編寫DirectRabbitSender的代碼

@Component
public class DirectRabbitSender{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String EXCHANGE_NAME = "directTest";
    private final String ROUTING_KEY = "direct_routing_key";

    public void send(Object message) {
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY, message);
    }

}

我們可以看到代碼中,通過rabbitTemplate發送消息到了交換器為directTest,routingkey為direct_routing_key的地方。但這時候我們沒有任何隊列了,自然接不到消息。現在我們去編寫第一個消費者DirectConsumerOne來接受消息。

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_1", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerOne {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_1接到消息" + message);
    }

}

通過代碼可以看到,我們通過@QueueBinding把Direct_test_queue_1隊列綁定到了directTest和direct_routing_key上。Direct_test_queue_1並沒有在rabbitmq創建,這並沒有關係。一般來說,@RabbitListener會自動去創建隊列。啟動程序,我們去看一下rabbitmq里隊列是不是創建了。

Bingo!再次驗證成功。我們去看看綁定關係是不是正確。這時候Direct_test_queue_1應該綁定到了名為directTest的交換器,而綁定的routingkey為direct_routing_key

biubiubiu!綁定關係完全正確。到了這裏,我們進行最後一步,寫了單元測試去發送消息,查看控制台中消費者是否成功收到消息。RabbitmqApplicationTests的代碼如下:

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private DirectRabbitSender directRabbitSender;

    @Test
    void contextLoads() {
    }

    @Test
    public void directSendTest(){
        directRabbitSender.send("direct-sender");
        directRabbitSender.send("direct-sender_test");
    }

}

啟動測試類,然後去查看控制台。

沒錯,這就是我們想要達到的效果!基本可以宣布Direct模式驗證成功。服務端生成exchange,客戶端去生成隊列綁定的方式在direct模式下完全可行。為了保險起見,再驗證一下生成多個消費者綁定到同一個隊列是否可行。

DirectConsumerTwo代碼如下:

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_2", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerTwo {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_2接到消息" + message);
    }

}

DirectConsumerThree代碼如下:

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_3", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerThree {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_3接到消息" + message);
    }

}

啟動測試類,我們去看兩個地方:

  1. rabbitmq是否創建了客戶端綁定的三個隊列Direct_test_queue_1、Direct_test_queue_2、Direct_test_queue_3
  2. 消費者應該各自收到2條消息(Test中發送了兩條,參看上面 RabbitmqApplicationTests 的代碼)。那3個隊列,控制台中應該打印了6條消息。

hohohoho!創建成功,並且綁定關係我看了也全都正確。我們去看控制台

6條!沒有任何毛病,至此,可以宣布Direct模式下,完全支持我們最初的想法:服務端生成exchange,客戶端去生成隊列綁定的方式在direct模式下完全可行。

fanout模式驗證

接下來我們驗證一下fanout的方式,基本操作流程和Direct模式一致。代碼的結構也差不多:

  1. 針對生產者,需要RabbitmqConfig,直接在Direct模式下的rabbitmqConfig里直接添加Fanout的交換器配置
  2. 針對生產者,需要FanoutRabbitSender,用來實現Fanout模式的消息發送
  3. 針對消費者,需要FanoutConsumerOne,來測試第一個隊列Fanout_test_queue_1生成和消息接收
  4. 針對消費者,需要FanoutConsumerTwo,來測試第二個隊列Fanout_test_queue_2生成和消息接收
  5. 針對消費者,需要FanoutConsumerThree,來測試第三個隊列Fanout_test_queue_3生成和消息接收
  6. 測試類RabbitmqApplicationTests也直接復用Direact模式下測試的類

我就不多BB,直接上代碼了。

RabbitmqConfig代碼如下

@Configuration
public class RabbitmqConfig {

    @Bean
    DirectExchange directExchange(){
        return new DirectExchange("directTest", true, false);
    }

    @Bean
    FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanoutTest", true, false);
    }

}

FanoutRabbitSender的代碼如下,此處和direct模式的區別是Fanout中沒有routingkey,所以代碼里也沒定義routingkey:

@Component
public class FanoutRabbitSender{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String EXCHANGE_NAME = "fanoutTest";

    public void send(Object message) {
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, null, message);
    }

}

我們到這裏先啟動程序試試,看看fanoutTest的交換器在沒有綁定隊列的情況下是否生成了。

棒棒棒!和我們想的一樣,那接下來去寫完所有的消費者,這裏和Direct模式最重要的區別是@Exchange中必須要指定type為fanout。direct模式的代碼里沒指定是因為@Exchange的type默認值就是direct。我直接上代碼了:

/**
 * 監聽器主動去聲明queue=fanout_test_queue_1,並綁定到fanoutTest交換器
 */
@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_1", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerOne {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_1接到消息" + message);
    }

}

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_2", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerTwo {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_2接到消息" + message);
    }

}

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_3", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerThree {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_3接到消息" + message);
    }

}

接着去測試類RabbitmqApplicationTests中加上fanout的發送測試,然後註釋掉direct的單元測試,以便一會造成干擾

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private DirectRabbitSender directRabbitSender;

    @Autowired
    private FanoutRabbitSender fanoutRabbitSender;

    @Test
    void contextLoads() {
    }

//    @Test
//    public void directSendTest(){
//        directRabbitSender.send("direct-sender");
//        directRabbitSender.send("direct-sender_test");
//    }

    @Test
    public void fanoutSendTest(){
        fanoutRabbitSender.send("fanout-sender_1");
        fanoutRabbitSender.send("fanout-sender_2");
    }

}

代碼都完成了,現在我們啟動測試類,看看控制台是否正常收到了消息

看圖看圖,fanout模式下也完全認證成功!!!那我們可以宣布,文章開頭的猜想完全可以實現。

總結

服務端只聲明exchange,客戶端來創建queue和綁定exchange的方式完全可行。並且在Direct和Fanout模式下都可行。

那我們可以推測在Header模式的交換器和Topic模式的交換器下應該也大差不差。具體各位可自行驗證,基本流程和上面direct和fanout的流程差不多。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

JSON類庫Jackson優雅序列化Java枚舉類

1. 前言

在Java開發中我們為了避免過多的魔法值,使用枚舉類來封裝一些靜態的狀態代碼。但是在將這些枚舉的意思正確而全面的返回給前端卻並不是那麼順利,我們通常會使用Jackson類庫序列化對象為JSON,今天就來講一個關於使用Jackson序列化枚舉的通用性技巧。

2. 通用枚舉範式

為了便於統一處理和規範統一的風格,建議指定一個統一的抽象接口,例如:

/**
 * The interface Enumerator.
 */
public interface Enumerator {
    /**
     * Code integer.
     *
     * @return the integer
     */
    Integer code();

    /**
     * Description string.
     *
     * @return the string
     */
    String description();
}

我們來寫一個實現來標識性別:

public enum GenderEnum implements Enumerator {
   
    UNKNOWN(0, "未知"),

    MALE(1, "男"),

    FEMALE(2, "女");


    private final Integer code;
    private final String description;

    GenderEnum(Integer code, String description) {
        this.code = code;
        this.description = description;
    }


    @Override
    public Integer code() {
        return code;
    }

    @Override
    public String description() {
        return description;
    }
}

3. 序列化枚舉

如果我們直接使用Jackson對枚舉進行序列化,將只能簡單的輸出枚舉的String名稱:

    @Resource
    private ObjectMapper objectMapper;

    @Test
    void enumTest() {
        try {
            String s = objectMapper.writeValueAsString(GenderEnum.MALE);
            // 輸出字符串 MALE
            System.out.println(s);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

我們期望將GenderEnum.MALE 序列化為 {"code":1,"description":"男"} 。我們可以向ObjectMapper定製化一個Module來實現這種個性化需求:

         // 聲明一個簡單Module 對象
         SimpleModule module = new SimpleModule();
           // 給Module 添加一個序列化器
            module.addSerializer(Enumerator.class, new JsonSerializer<Enumerator>() {
                @Override
                public void serialize(Enumerator value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                   // 開始寫入對象
                    gen.writeStartObject();
                    // 分別指定 k v   code   description 
                    gen.writeNumberField("code",value.code());
                    gen.writeStringField("description",value.description());
                    // 顯式結束操作
                    gen.writeEndObject();
                }
            });

        // 註冊 Module
        objectMapper.registerModule(module);

然後再次執行就會獲取我們期望的結果。然而這並不算合理。

4. Spring Boot 中自動全局配置

在Spring Boot應用中我們希望能全局配置。Spring Boot的自動配置為我們提供了一個個性化定製ObjectMapper的可能性,你只需要聲明一個Jackson2ObjectMapperBuilderCustomizer並注入Spring IoC:

@Bean
public Jackson2ObjectMapperBuilderCustomizer enumCustomizer(){
    return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.serializerByType(Enumerator.class, new JsonSerializer<Enumerator>() {
        @Override
        public void serialize(Enumerator value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                    gen.writeStartObject();
                    gen.writeNumberField("code",value.code());
                    gen.writeStringField("description",value.description());
                    gen.writeEndObject();


        }
    });
}

這樣就實現了全局配置。

5. 總結

這裏我們介紹了如何定製Jackson庫以達到對枚舉進行更加友好的序列化的目的。其實不單單枚舉,你也可以實現其它序列化,反序列化,時間輸出格式的定製。這些特性留給你自己挖掘。多多關注:碼農小胖哥 獲取更多開發技巧。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

從零開始學習Prometheus監控報警系統

Prometheus簡介

Prometheus是一個開源的監控報警系統,它最初由SoundCloud開發。

2016年,Prometheus被納入了由谷歌發起的Linux基金會旗下的雲原生基金會( Cloud Native Computing Foundation),並成為僅次於Kubernetes的第二大開源項目。自此,它成為了一個獨立的開源項目,獨立於任何公司進行維護。

Prometheus擁有非常活躍的開發人員和用戶社區,目前在GitHub上已擁有三萬多的Star。

Prometheus特點

  • 提供多維度數據模型,使用指標名稱和鍵值對標識的時間序列數據
  • 提供靈活的PromQL查詢方式,還提供了HTTP查詢接口,可以很方便地結合Grafana等組件展示數據。
  • 不依賴外部存儲,支持單節點的本地存儲。通過Prometheus自帶的時序數據庫,可以完成每秒百萬及的數據存儲,如果需要存儲大量歷史數據,還可以對接第三方的時序數據庫。
  • 時間序列收集通過HTTP的拉取方式進行,並提供了開放的指標數據標準。
  • 支持向中間網關推送時序數據,可以更加靈活地適用於多種監控場景。
  • 支持通過動態服務發現和靜態文件配置獲取監控對象,目前已支持Kubernetes、Etcd、Consul等多種服務發現機制。
  • 支持多種模式的圖形展示和儀錶盤。
  • 大多數Prometheus的組件都是使用Go語言編寫的,這使得它們很容易以二進制文件的形式構建和部署。

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

Prometheus架構

Prometheus生態圈由多個組件構成,其中許多組件是可選的:

  • Prometheus Server:用於收集、存儲和查詢時間序列數據。通過靜態配置文件管理監控目標,也可以配合使用動態服務發現的方式動態管理監控目標,並從這些監控目標中獲取數據。它將採集到的數據按照時間序列的方式存儲在本地磁盤當中或者外部的時序數據庫中,可通過PromQL語言對數據的查詢以及分析。
  • Client Library:為被監控的應用生成相應的指標(Metric)數據並暴露給Prometheus Server。當Prometheus Server 來拉取時,直接返回實時狀態的指標數據。
  • Push Gateway:主要用於短期存在的Jobs。由於這類Jobs存在時間較短,可能在Prometheus Server來拉取數據之前就消失了。所以,Jobs可以直接向Push Gateway推送它們的指標數據,然後Prometheus Server再從Push Gateway拉取。
  • Exporters:用於暴露已有的第三方服務的指標數據通過HTTP服務的形式暴露給Prometheus Server,比如HAProxy、StatsD、Graphite等等。Prometheus Server通過訪問該Exporter提供的Endpoint,即可獲取到需要採集的監控數據。
  • Alertmanager:從Prometheus Server接收到告警后,會進行去除重複數據,分組,並路由到對收的接受方式,發出報警。Alertmanager的告警方式非常靈活,支持通過郵件、slack或釘釘等多種途徑發出告警。
  • 一些其他的組件。

下面這張圖展示了Prometheus的架構和各個組件是如何交互和協作的:

其大概的工作流程是:

  1. Prometheus Server直接從HTTP接口或者Push Gateway拉取指標(Metric)數據。
  2. Prometheus Server在本地存儲所有採集的指標(Metric)數據,並在這些數據上運行規則,從現有數據中聚合和記錄新的時間序列,或者生成告警。
  3. Alertmanager根據配置文件,對接收到的告警進行處理,發出報警。
  4. 在Grafana或其他API客戶端中,可視化收集的數據。

Prometheus數據模型

Prometheus會將所有採集到的監控數據以時間序列的方式保存在內存數據庫中,並且定時保存到硬盤上。每一條數據由以下三部分組成:

  • 指標(Metric):由指標名稱和描述當前數據特徵的標籤組成。
  • 時間戳(Timestamp):一個精確到毫秒的時間戳。
  • 數據值(Value):一個float64的浮點型數據表示當前數據的值。

其中,指標(Metric)通過如下格式標識:

<指標名稱>{<標籤名稱>=<標籤值>, ...}

指標名稱(Metric Name)可以反映被監控數據的含義。指標名稱只能由ASCII字符、数字、下劃線以及冒號組成並必須符合正則表達式[a-zA-Z_:][a-zA-Z0-9_:]*。
標籤(Label)反映了當前數據的特徵維度,通過這些維度Prometheus可以對數據進行過濾,聚合等操作。標籤的名稱只能由ASCII字符、数字以及下劃線組成並滿足正則表達式[a-zA-Z_][a-zA-Z0-9_]*。

比如:

prometheus_http_requests_total{code="200",handler="/metrics"}

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

指標類型

Prometheus定義了4種不同的指標類型(Metric Type):

  • Counter(計數器)
  • Gauge(儀錶盤)
  • Histogram(直方圖)
  • Summary(摘要)

Counter(計數器)

Counter類型和計數器一樣,只增不減(除非系統發生重置),一般在定義Counter類型指標的名稱時推薦使用_total作為後綴。

比如,Prometheus Server中prometheus_http_requests_total, 表示Prometheus處理的HTTP請求總數:

# HELP prometheus_http_requests_total Counter of HTTP requests.
# TYPE prometheus_http_requests_total counter
prometheus_http_requests_total{code="200",handler="/api/v1/label/:name/values"} 3
prometheus_http_requests_total{code="200",handler="/api/v1/query"} 5
prometheus_http_requests_total{code="200",handler="/api/v1/query_range"} 15
prometheus_http_requests_total{code="200",handler="/graph"} 3
prometheus_http_requests_total{code="200",handler="/metrics"} 23
prometheus_http_requests_total{code="200",handler="/static/*filepath"} 18
prometheus_http_requests_total{code="302",handler="/"} 1

Gauge(儀錶盤)

Gauge類型側重於反應系統的某一個瞬時的值,這類指標的數據可增可減。

比如,Prometheus Server中go_threads, 表示Prometheus當前go線程的數量:

# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 13

Histogram(直方圖)

Histogram類型由 _bucket{le=” “}, _bucket{le=”+Inf”}, _sum, _count 組成,主要用於表示一段時間範圍內對數據進行採樣,並能夠對其指定區間以及總數進行統計,通常它採集的數據展示為直方圖。

比如,Prometheus Server中prometheus_http_response_size_bytes:

# HELP prometheus_http_response_size_bytes Histogram of response size for HTTP requests.
# TYPE prometheus_http_response_size_bytes histogram
prometheus_http_response_size_bytes_bucket{handler="/",le="100"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1000"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="10000"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="100000"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+06"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+07"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+08"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="1e+09"} 1
prometheus_http_response_size_bytes_bucket{handler="/",le="+Inf"} 1
prometheus_http_response_size_bytes_sum{handler="/"} 29
prometheus_http_response_size_bytes_count{handler="/"} 1

Summary(摘要)

Summary類型由 {quantile=”<φ>”}, _sum, _count 組成,主要用於表示一段時間內數據採樣結果,它直接存儲了分位數據,而不是根據統計區間計算出來的。

比如,Prometheus Server中prometheus_target_interval_length_seconds:

# HELP prometheus_target_interval_length_seconds Actual intervals between scrapes.
# TYPE prometheus_target_interval_length_seconds summary
prometheus_target_interval_length_seconds{interval="15s",quantile="0.01"} 14.9986249
prometheus_target_interval_length_seconds{interval="15s",quantile="0.05"} 14.998999
prometheus_target_interval_length_seconds{interval="15s",quantile="0.5"} 15.0000428
prometheus_target_interval_length_seconds{interval="15s",quantile="0.9"} 15.0012009
prometheus_target_interval_length_seconds{interval="15s",quantile="0.99"} 15.0016468
prometheus_target_interval_length_seconds_sum{interval="15s"} 315.0013755
prometheus_target_interval_length_seconds_count{interval="15s"} 21

歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

安裝Prometheus Server

從官方網站(https://prometheus.io/download/)上找到最新版本的Prometheus Sevrer軟件包,如下圖:
根據自己的系統下載對應的壓縮包,這裏以Windows為例,下載prometheus-2.19.0.windows-amd64.tar.gz。

解壓后當前目錄會包含默認的Prometheus配置文件promethes.yml:

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['localhost:9090']

暫且不做修改,雙擊prometheus.exe即可啟動,如下圖:

訪問http://localhost:9090/graph,就可以看到Prometheus自身的監控數據:

尾聲

Prometheus的大致介紹已經告一段落了,但是只是萬里長征的第一步,Prometheus的更多強大功能和使用方法還等待我們去挖掘。

微信公眾號:萬貓學社

微信掃描二維碼

獲得更多Java技術乾貨

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

龍捲風侵襲美國阿肯色州 當局為加速救災實施宵禁

摘錄自2020年3月29日民視報導

美國中部阿肯色州瓊斯伯勒市,昨(28)日遭巨大龍捲風襲擊,多處建築物受損、飛機被吹翻,已知6人受傷。當局事後派出人員搜救又實施宵禁,確保民眾晚上7點之後不會外出,加速救災。

事情發生在美國中部時間28日下午5點左右,突如其來的巨大龍捲風,席捲阿肯色州的瓊斯伯勒市,多棟建築物屋頂瞬間被吹毀、路牌吹歪,甚至傳出當地機場有一架飛機被吹翻,損失慘重。所幸因為武漢肺炎疫情,多數店家早就拉下鐵門,居民也幾乎待在室內,因此只傳出數人受到輕傷。

當地政府第一時間發出警報,要求民眾待在室內避難,事後也派出國民兵進行三次搜救。最後決定實施宵禁,禁止民眾晚上7點後出門,方便有關單位加速清理。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

寮國大壩潰決! 50億噸蓄水傾瀉、數百人失蹤

摘錄自2018年7月24日自由時報報導

寮國東南部一處水力發電水壩驚傳潰決意外,50億噸的蓄水傾瀉而下,造成數百人失蹤。

綜合外媒報導,位於寮國東南部的阿速坡省(Attapeu)週一(23日)深夜驚傳水壩潰壩,一處用來水力發電的水壩不明原因突然潰決,50億噸的水量瞬間傾瀉而下,下游六個村落遭到淹沒,情況一片混亂,寮國官媒僅表示已有人死亡,不過死亡人數不詳,目前還有數百人失蹤。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

雷諾擬與LG Chem合作開發時速400公里的電動車

全球最大可充電電池製造商樂金化學公司(LG Chem),和歐洲電動車先驅雷諾汽車(Renault)聯手,希望未來幾年將電動車的最高時速加快一倍。

據韓國總統朴槿惠的首席經濟幕僚趙源東(Cho Won Dong)透露,這兩家公司正在考慮開發最快時速可達400公里的電動車。目前電動車的最高時速為每小時200公里。

為加強雙邊合作,朴槿惠4日還拜會了雷諾在巴黎南方設立的電動車測試中心。

據悉,兩家公司目前還不會簽署了解備忘錄(MOU),但原則上同意互相合作,正在協商細節上的歧異。由於要開發高速電動車,鋰電池是最重要的環節之一,因此對於雷諾來說,與樂金化學的合作十分重要。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Model S銷量不如預期 Tesla三季度財測不佳

美國電動車製造商特斯拉(Tesla Motors)於美股5日收盤後公佈的上季汽車銷量不如預期、賣給其他車商的碳權抵換交易收入下滑,加上第4季盈餘財測欠佳,導致該公司盤後股價重挫逾12%。

特斯拉表示,Q3期間電動轎車每週已可生產550台、整季出貨量達創記錄的5,500台,其中歐洲地區的出貨量超過了1,000台。不過,Model S的Q3出貨量仍不如市場樂觀的預估,市場曾預期Model S的Q3銷售量有望上升至5,850台。

展望Q4,特斯拉預估Model S的出貨量有望接近6,000台,這會讓2013年一整年的出貨量達到21,500台,而當季的本業毛利率則可望上升至25%。此外,特斯拉已在Q3開始接受來自中國大陸客戶對Model S下達的訂單,預計可在明年Q1首度交貨。

特斯拉去年的Model S出貨量僅約2,650台,不如該公司原本設定的5,000台。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

與特斯拉競爭 BMW、通用等洛杉磯車展推電動車

據華爾街日報報導,美國電動車商特斯拉(Tesla)去年推出Model S後,越來越多的車商參與到豪華電動車市場的競爭。

BMW、通用的凱迪拉克、福斯的保持捷與奧迪,都趁本周揭幕的洛杉磯車展,展出自家新電動車或插電式油電混合車。雖各家車廠的推進技術與Model S不同,但都想爭奪加州消費者的心,此一族群是特斯拉的重要客戶層。

電動車目前在整體車市占有率微不足道。即使日產Leaf電動車需求增加,讓日產擴大Leaf產能,但今年來,電動車僅占美國輕型車銷售約1%。

電動車售價,因鋰離子電池昂貴而難以下降,加上續航力不足,被視為市場接受度不高的主因。但售價7萬美元的特斯拉Model S,因高電池效能讓其充電一次可行駛265英里,因此供不應求。此外政府制定廢氣排放要求趨嚴,也迫使車商生產更多電動車或油電車。

奧迪在洛杉磯車展展出A3 E-Tron插電式油電混合車,準備在2015年上市;但沒有透露何時推出全電動版。通用表示,其ELR油電車因售價7.6萬美元,而會被市場拿來與Model S做比較。BMW在車展上推出13.5萬美元起跳的i8插電式油電跑車,和4萬美元的城市通勤電動小車i3。

而多款豪華插電式新車在明年上市,對特斯拉影響有多大,還要看此一市場規模是否已經變大,抑或大家仍在爭食同一塊大餅。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

.NETCore微服務探尋(三) – 分佈式日誌

前言

一直以來對於.NETCore微服務相關的技術棧都處於一個淺嘗輒止的了解階段,在現實工作中也對於微服務也一直沒有使用的業務環境,所以一直也沒有整合過一個完整的基於.NETCore技術棧的微服務項目。正好由於最近剛好辭職,有了時間可以寫寫自己感興趣的東西,所以在此想把自己了解的微服務相關的概念和技術框架使用實現記錄在一個完整的工程中,由於本人技術有限,所以錯誤的地方希望大家指出。

目錄

  • .NETCore微服務探尋(一) – 網關
  • .NETCore微服務探尋(二) – 認證與授權

項目地址:https://github.com/yingpanwang/fordotnet/tree/dev

為什麼需要分佈式日誌

在項目的運行運行過程中,不可避免的是由於系統原因或者業務原因產生的警告或異常,這時我們需要根據產生的異常或警告信息快速排查出現的問題並修復,但是由於多個服務產生的過於龐雜的信息使那些以往通過直接寫入日誌文件的方式已經無法滿足快速排查的需求了,因為直接寫入日誌文件只能根據事先制定好的規則查看日誌信息,但是由於體量過大導致排查起來異常麻煩,例如,如果問題出現在 6月20日的凌晨1點 日誌文件對應的是 log-2020-06-20 ,那麼導致這個問題產生的原因可能20日之前的前置問題已經產生,如果我們需要排查的話,由於無法宏觀分析問題的出現原因,那麼需要日誌文件逐個查看導致效率低下。

如果採用分佈式日誌的話,首先由於日誌由日誌中心統一存儲,不需要寫入本地文件減少了IO(不包括由於項目與日誌收集中心通訊失敗而導致本地補償產生的日誌),其次搭配其他的可視乎,管理,分析組件,可以有一個良好的日誌管理與可視化,排查時可以通過相關的信息篩選,過濾無關信息,從宏觀信息中精準查詢指定信息,從而提高排查效率。

怎麼給項目接入分佈式日誌系統

  • Exceptionless Asp.Net Core 開源分佈式日誌組件

  • Log組件+ Elasticsearch+ Kibana 這種模式採用的時通過擴展已有日誌組件(Log4Net,Serilog,NLog等),通過Elasticsearch使用或不適用隊列的模式收集日誌,然後通過Kibana可視化管理分析組件 實現日誌的收集分析

    目前我所了解的搭建分佈式日誌系統的方式有兩種,但他們的方式其實底層都差不多 主要依賴Elasticsearch作為日誌的收集,然後搭配可視化的插件,這裏我選擇的時採用的是第二種方式,因為對代碼的侵入性較小,可以比較靈活的根據實際的業務需要添加日誌組件的相關插件。

首先安裝並運行Elasticsearch(es) 和 Kibana

這裏不詳細講 es/kibana 的配置,安裝好jdk以後 直接運行bin目錄下的elasticsearch.bat/kibana.bat 就可以了

注意 :
1.es 運行依賴jdk
2.Kibana 運行需要 對應es 對應的版本

其次擴展已有日誌組件使其通過es支持日誌收集

由於項目中我採用的時Serilog所以下面的代碼都是以Serilog為主,但由於是實現Asp.Net Core中的ILogger,使用實際差距不大\

1.根據需要安裝依賴組件

必須(二選一)

  • Serilog Serilog 基本庫
  • Serilog.AspNetCore AspNetCore框架整合庫,包含Serlog基本庫和控制台日誌實現

可選

  • Serilog.Extensions.Logging 包含了注入Serilog的擴展方法
  • Serilog.Sinks.Async 實現了日誌異步收集
  • Serilog.Sinks.Console 實現了控制台日誌
  • Serilog.Settings.Configuration 如果需要通過json配置文件配置Serilog的話需要安裝此庫
  • Serilog.Sinks.Elasticsearch 實現了Elasticsearch收集

2.初始化Serilog,並添加至AspNetCore的ILoggerFactory

這裏要添加serilog的方式有幾種,常用的是通過硬編碼的情況,另外是通過 xml,json等配置文件的方式,這裏都做一個簡單的示例,更詳細的配置信息可以查看Serilog.Sinks github倉庫中的介紹,非常詳細,這裏不做過多介紹
Serilog.Settings.Configuration項目地址

1.硬編碼的方式


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ForDotNet.Common.Consul.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Sinks.Elasticsearch;

namespace ForDotNet.Web.Api
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;

            //初始化Serilog
            Log.Logger = new LoggerConfiguration()
                    .Enrich.FromLogContext()
                    .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}")
                    .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
                    {
                        AutoRegisterTemplate = true,
                        IndexFormat = "Api1-{0:yyyy-MM-dd}",// es index模板
                    })
                    .CreateLogger();
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // 添加當前項目服務發現
            services.AddConsulServiceDiscovery();

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILoggerFactory loggerFactory,IHostApplicationLifetime life)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // 添加serilog
            loggerFactory.AddSerilog();

            app.UseConsulServiceDiscovery(life);

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}


2.通過配置文件的方式

準備需要配置的信息,這裏我們使用的是appsettings.json文件


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ServiceOptions": {
    "ServiceIP": "localhost",
    "ServiceName": "Auth",
    "Port": 5800,
    "HealthCheckUrl": "/api/health",
    "ConsulOptions": {
      "Scheme": "http",
      "ConsulIP": "localhost",
      "Port": 8500
    }
  },
  "Serilog": {
    "WriteTo": [
      {
        "Name": "Elasticsearch",
        "Args": {
          "nodeUris": "http://localhost:9200;http://remotehost:9200/",
          "indexFormat": "auth-{0:yyyy-MM-dd}",
          "autoRegisterTemplate": true
        }
      }
    ]
  }
}

然後更改Serilog的相關初始化代碼為


public Startup(IConfiguration configuration,IHostEnvironment hostEnvironment)
        {

           // 讀取配置文件
           var builder = new ConfigurationBuilder()
               .SetBasePath(hostEnvironment.ContentRootPath)
               .AddJsonFile("appsettings.json", true, true)
               .AddJsonFile($"appsettings.{hostEnvironment.EnvironmentName}.json", true, true)
               .AddEnvironmentVariables();

            Configuration = builder.Build();

            Log.Logger = new LoggerConfiguration()
                    .ReadFrom.Configuration(Configuration)
                    .CreateLogger();
        }

3.將日誌記錄操作轉為異步

由於serilog.sinks實現大多都是同步的方式實現,所以如果需要以異步的方式收集日誌的話需要引用Serilog.Sinks.Async這個庫,並更改相關代碼。詳細請見Serilog.Sinks.Async官方倉庫,
同樣,異步的方式也可以通過配置文件實現,可以查看官方倉庫,這裏只使用硬編碼的方式。


public Startup(IConfiguration configuration,IHostEnvironment hostEnvironment)
        {

           // 讀取配置文件
           var builder = new ConfigurationBuilder()
               .SetBasePath(hostEnvironment.ContentRootPath)
               .AddJsonFile("appsettings.json", true, true)
               .AddJsonFile($"appsettings.{hostEnvironment.EnvironmentName}.json", true, true)
               .AddEnvironmentVariables();

            Configuration = builder.Build();

            Log.Logger = new LoggerConfiguration()
                    .WriteTo.Async(configure =>
                    {
                        configure
                        .Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code);

                        configure
                        .Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
                        {
                            AutoRegisterTemplate = true,
                            IndexFormat = "auth-{0:yyyy-MM-dd}",

                        });
                    })
                    .CreateLogger();
        }

3.運行並查看

啟動Elasticserach,Kibana,項目后

查看控制台,發現同樣的日誌輸出了兩遍,這是因為AspNetCore默認實現的LoggerProvider 沒有清除所以會導致 打印輸出,我們在啟動時清除默認Provider即可

清除LoggerProvider

然後運行並查看日誌,是不是清爽了很多

然後我們訪問Kiabana查看我們剛剛收集的日誌

訪問 http://localhost:5601 Kibana默認項目地址,不同Kibana版本頁面會有差異

點擊Management建立我們的日誌收集模型

這裏由於我已經建立過其他的模塊的信息,所以可以看到我已經創建的信息,這裏我們點擊創建新的信息

這裏需要輸入 正則表達式 匹配收集的信息,這裏我們輸入我們定義的模板開頭的api1並點擊下一步

這裏選擇@timestamp作為索引模式,也可以選擇不添加,然後創建

創建完成后 去到 Discover 模塊

在左邊選擇需要查看的index

就可以看到我們的日誌已經收集到es中了,可以通過kibana查看了

如果覺得字段過於複雜的話 可以在左邊選擇過濾的字段查看 我這裏已經只選擇了 leve 和 message

好了 ,以上我就我分享的 建立分佈式日誌的內容了,如果有紕漏及錯誤 希望大家指出,謝謝

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準