คำนาม สมุดจดรายการต่าง /lɑɡ bʊk/: หนังสือที่บันทึกการวัดจากบันทึกของเรือ พร้อมด้วยรายละเอียดสำคัญอื่นๆ ของการเดินทาง
Logbook เป็นไลบรารี Java ที่ขยายได้เพื่อเปิดใช้งานการบันทึกคำขอและการตอบกลับที่สมบูรณ์สำหรับเทคโนโลยีฝั่งไคลเอ็นต์และเซิร์ฟเวอร์ที่แตกต่างกัน ตอบสนองความต้องการพิเศษโดย ก) ช่วยให้นักพัฒนาแอปพลิเคชันเว็บบันทึกการรับส่งข้อมูล HTTP ใดๆ ที่แอปพลิเคชันได้รับหรือส่ง ข) ในลักษณะที่ทำให้ง่ายต่อการคงอยู่และวิเคราะห์ในภายหลัง สิ่งนี้มีประโยชน์สำหรับการวิเคราะห์บันทึกแบบดั้งเดิม การปฏิบัติตามข้อกำหนดการตรวจสอบ หรือการตรวจสอบปัญหาการรับส่งข้อมูลในอดีตแต่ละรายการ
Logbook พร้อมใช้งานทันทีเมื่อนำออกจากกล่องสำหรับการตั้งค่าทั่วไปส่วนใหญ่ แม้แต่แอปพลิเคชันและเทคโนโลยีที่ไม่ธรรมดา ก็ควรปรับใช้อินเทอร์เฟซที่จำเป็นเพื่อเชื่อมต่อไลบรารี/เฟรมเวิร์ก/อื่นๆ ได้อย่างง่ายดาย ถึงมัน
เพิ่มการพึ่งพาต่อไปนี้ให้กับโครงการของคุณ:
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-core</ artifactId >
< version >${logbook.version}</ version >
</ dependency >
สำหรับความเข้ากันได้แบบย้อนหลังของ Spring 5 / Spring Boot 2 โปรดเพิ่มการนำเข้าต่อไปนี้:
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-servlet</ artifactId >
< version >${logbook.version}</ version >
< classifier >javax</ classifier >
</ dependency >
โมดูล/สิ่งประดิษฐ์เพิ่มเติมของ Logbook จะใช้หมายเลขเวอร์ชันเดียวกันเสมอ
หรือคุณสามารถนำเข้า รายการวัสดุ ของเรา ...
< dependencyManagement >
< dependencies >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-bom</ artifactId >
< version >${logbook.version}</ version >
< type >pom</ type >
< scope >import</ scope >
</ dependency >
</ dependencies >
</ dependencyManagement >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-core</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-httpclient</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-jaxrs</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-json</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-netty</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-okhttp</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-okhttp2</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-servlet</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-spring-boot-starter</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-ktor-common</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-ktor-client</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-ktor-server</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-ktor</ artifactId >
</ dependency >
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-logstash</ artifactId >
</ dependency >
ตัวบันทึกสมุดบันทึกต้องได้รับการกำหนดค่าให้ติดตามระดับเพื่อบันทึกคำขอและการตอบกลับ ด้วย Spring Boot 2 (โดยใช้ Logback) สามารถทำได้โดยเพิ่มบรรทัดต่อไปนี้ใน application.properties
ของคุณ
logging.level.org.zalando.logbook: TRACE
การบูรณาการทั้งหมดจำเป็นต้องมี Logbook
ซึ่งเก็บการกำหนดค่าทั้งหมดและเชื่อมต่อส่วนที่จำเป็นทั้งหมดไว้ด้วยกัน คุณสามารถสร้างได้โดยใช้ค่าเริ่มต้นทั้งหมด:
Logbook logbook = Logbook . create ();
หรือสร้างเวอร์ชันที่กำหนดเองโดยใช้ LogbookBuilder
:
Logbook logbook = Logbook . builder ()
. condition ( new CustomCondition ())
. queryFilter ( new CustomQueryFilter ())
. pathFilter ( new CustomPathFilter ())
. headerFilter ( new CustomHeaderFilter ())
. bodyFilter ( new CustomBodyFilter ())
. requestFilter ( new CustomRequestFilter ())
. responseFilter ( new CustomResponseFilter ())
. sink ( new DefaultSink (
new CustomHttpLogFormatter (),
new CustomHttpLogWriter ()
))
. build ();
สมุดจดรายการต่างเคยมีกลยุทธ์ที่เข้มงวดมากในการดำเนินการบันทึกคำขอ/ตอบกลับ:
ข้อจำกัดบางประการสามารถบรรเทาลงได้ด้วยการใช้งาน HttpLogWriter
แบบกำหนดเอง แต่ก็ไม่เหมาะเลย
ตั้งแต่เวอร์ชัน 2.0 Logbook มาพร้อมกับรูปแบบกลยุทธ์ที่เป็นแกนหลัก ตรวจสอบให้แน่ใจว่าคุณได้อ่านเอกสารประกอบของอินเทอร์เฟซ Strategy
เพื่อทำความเข้าใจผลกระทบ
Logbook มาพร้อมกับกลยุทธ์ในตัว:
BodyOnlyIfStatusAtLeastStrategy
StatusAtLeastStrategy
WithoutBodyStrategy
ตั้งแต่เวอร์ชัน 3.4.0 เป็นต้นไป Logbook มีคุณลักษณะที่เรียกว่า Attribute Extractor โดยพื้นฐานแล้วแอตทริบิวต์คือรายการคู่คีย์/ค่าที่สามารถดึงมาจากคำขอและ/หรือการตอบกลับ และบันทึกไว้ด้วย แนวคิดนี้เกิดขึ้นจากฉบับที่ 381 ซึ่งมีการร้องขอคุณลักษณะเพื่อแยกการอ้างสิทธิ์เรื่องจากโทเค็น JWT ในส่วนหัวของการอนุญาต
อินเทอร์เฟซ AttributeExtractor
มีสองวิธี extract
: วิธีหนึ่งสามารถแยกคุณลักษณะจากคำขอเท่านั้น และอีกวิธีหนึ่งที่มีทั้งคำขอและการตอบสนองตามประโยชน์ ทั้งสองส่งคืนอินสแตนซ์ของคลาส HttpAttributes
ซึ่งโดยพื้นฐานแล้วจะเป็น Map<String, Object>
แฟนซี โปรดสังเกตว่าเนื่องจากค่าแผนที่เป็นประเภท Object
จึงควรมีเมธอด toString()
ที่เหมาะสมเพื่อให้ปรากฏในบันทึกในลักษณะที่มีความหมาย หรืออีกทางหนึ่ง ตัวจัดรูปแบบบันทึกสามารถแก้ไขปัญหานี้ได้โดยใช้ตรรกะการทำให้เป็นอนุกรมของตนเอง ตัวอย่างเช่น ตัวจัดรูปแบบบันทึกในตัว JsonHttpLogFormatter
ใช้ ObjectMapper
เพื่อทำให้ค่าเป็นอนุกรม
นี่คือตัวอย่าง:
final class OriginExtractor implements AttributeExtractor {
@ Override
public HttpAttributes extract ( final HttpRequest request ) {
return HttpAttributes . of ( "origin" , request . getOrigin ());
}
}
สมุดบันทึกจะต้องถูกสร้างขึ้นโดยการลงทะเบียนตัวแยกคุณลักษณะนี้:
final Logbook logbook = Logbook . builder ()
. attributeExtractor ( new OriginExtractor ())
. build ();
ซึ่งจะส่งผลให้บันทึกคำขอรวมสิ่งที่ต้องการ:
"attributes":{"origin":"LOCAL"}
สำหรับตัวอย่างขั้นสูงเพิ่มเติม โปรดดูที่คลาส JwtFirstMatchingClaimExtractor
และ JwtAllMatchingClaimsExtractor
แบบแรกแยกการอ้างสิทธิ์แรกที่ตรงกับรายการชื่อการอ้างสิทธิ์จากโทเค็นคำขอ JWT ส่วนหลังแยกการอ้างสิทธิ์ทั้งหมดที่ตรงกับรายการชื่อการอ้างสิทธิ์จากโทเค็นคำขอ JWT
หากคุณต้องการรวม AttributeExtractor
หลายรายการ คุณสามารถใช้คลาส CompositeAttributeExtractor
:
final List < AttributeExtractor > extractors = List . of (
extractor1 ,
extractor2 ,
extractor3
);
final Logbook logbook = Logbook . builder ()
. attributeExtractor ( new CompositeAttributeExtractor ( extractors ))
. build ();
สมุดจดรายการต่างทำงานในหลายขั้นตอน:
แต่ละเฟสจะแสดงด้วยอินเทอร์เฟซตั้งแต่หนึ่งรายการขึ้นไปที่สามารถใช้เพื่อการปรับแต่งได้ ทุกเฟสมีค่าเริ่มต้นที่สมเหตุสมผล
การบันทึกข้อความ HTTP และการรวมเนื้อความนั้นเป็นงานที่ค่อนข้างแพง ดังนั้นจึงสมเหตุสมผลมากที่จะปิดใช้งานการบันทึกสำหรับคำขอบางรายการ กรณีการใช้งานทั่วไปคือการเพิกเฉยต่อคำขอ ตรวจสอบสภาพ จากโหลดบาลานเซอร์ หรือคำขอใดๆ ไปยังปลายทางการจัดการที่โดยปกติแล้วออกโดยนักพัฒนา
การกำหนดเงื่อนไขนั้นง่ายพอๆ กับการเขียน Predicate
พิเศษที่จะตัดสินใจว่าคำขอ (และการตอบกลับที่เกี่ยวข้อง) ควรถูกบันทึกไว้หรือไม่ หรือคุณสามารถใช้และรวมเพรดิเคตที่กำหนดไว้ล่วงหน้า:
Logbook logbook = Logbook . builder ()
. condition ( exclude (
requestTo ( "/health" ),
requestTo ( "/admin/**" ),
contentType ( "application/octet-stream" ),
header ( "X-Secret" , newHashSet ( "1" , "true" ):: contains )))
. build ();
รูปแบบการยกเว้น เช่น /admin/**
เป็นไปตามรูปแบบเส้นทางของ Ant อย่างหลวมๆ โดยไม่คำนึงถึงสตริงการสืบค้นของ URL
เป้าหมายของ การกรอง คือการป้องกันการบันทึกส่วนที่ละเอียดอ่อนของคำขอและการตอบกลับ HTTP ซึ่งโดยปกติจะรวมถึงส่วนหัว การอนุญาต ด้วย แต่ก็สามารถนำไปใช้กับการสืบค้นข้อความธรรมดาหรือพารามิเตอร์แบบฟอร์มบางอย่างได้ เช่น รหัสผ่าน
Logbook รองรับตัวกรองประเภทต่างๆ:
พิมพ์ | ดำเนินการบน | นำไปใช้กับ | ค่าเริ่มต้น |
---|---|---|---|
QueryFilter | สตริงการสืบค้น | ขอ | access_token |
PathFilter | เส้นทาง | ขอ | ไม่มี |
HeaderFilter | ส่วนหัว (คู่คีย์-ค่าเดียว) | ทั้งคู่ | Authorization |
BodyFilter | ประเภทเนื้อหาและเนื้อหา | ทั้งคู่ | json: access_token และ refresh_token แบบฟอร์ม: client_secret password และ refresh_token |
RequestFilter | HttpRequest | ขอ | แทนที่ไบนารี หลายส่วน และสตรีมเนื้อหา |
ResponseFilter | HttpResponse | การตอบสนอง | แทนที่ไบนารี หลายส่วน และสตรีมเนื้อหา |
QueryFilter
, PathFilter
, HeaderFilter
และ BodyFilter
อยู่ในระดับค่อนข้างสูงและควรครอบคลุมความต้องการทั้งหมดประมาณ 90% ของทุกกรณี สำหรับการตั้งค่าที่ซับซ้อนมากขึ้น ควรถอยกลับไปใช้เวอร์ชันระดับต่ำ เช่น RequestFilter
และ ResponseFilter
ตามลำดับ (ร่วมกับ ForwardingHttpRequest
/ ForwardingHttpResponse
)
คุณสามารถกำหนดค่าตัวกรองดังนี้:
import static org . zalando . logbook . core . HeaderFilters . authorization ;
import static org . zalando . logbook . core . HeaderFilters . eachHeader ;
import static org . zalando . logbook . core . QueryFilters . accessToken ;
import static org . zalando . logbook . core . QueryFilters . replaceQuery ;
Logbook logbook = Logbook . builder ()
. requestFilter ( RequestFilters . replaceBody ( message -> contentType ( "audio/*" ). test ( message ) ? "mmh mmh mmh mmh" : null ))
. responseFilter ( ResponseFilters . replaceBody ( message -> contentType ( "*/*-stream" ). test ( message ) ? "It just keeps going and going..." : null ))
. queryFilter ( accessToken ())
. queryFilter ( replaceQuery ( "password" , "<secret>" ))
. headerFilter ( authorization ())
. headerFilter ( eachHeader ( "X-Secret" :: equalsIgnoreCase , "<secret>" ))
. build ();
คุณสามารถกำหนดค่าตัวกรองได้มากเท่าที่คุณต้องการ โดยตัวกรองจะทำงานติดต่อกัน
คุณสามารถใช้การกรองเส้นทาง JSON กับเนื้อหา JSON ได้ นี่คือตัวอย่างบางส่วน:
import static org . zalando . logbook . json . JsonPathBodyFilters . jsonPath ;
import static java . util . regex . Pattern . compile ;
Logbook logbook = Logbook . builder ()
. bodyFilter ( jsonPath ( "$.password" ). delete ())
. bodyFilter ( jsonPath ( "$.active" ). replace ( "unknown" ))
. bodyFilter ( jsonPath ( "$.address" ). replace ( "X" ))
. bodyFilter ( jsonPath ( "$.name" ). replace ( compile ( "^( \ w).+" ), "$1." ))
. bodyFilter ( jsonPath ( "$.friends.*.name" ). replace ( compile ( "^( \ w).+" ), "$1." ))
. bodyFilter ( jsonPath ( "$.grades.*" ). replace ( 1.0 ))
. build ();
ดูตัวอย่างต่อไปนี้ ก่อนและหลังใช้การกรอง:
{
"id" : 1 ,
"name" : " Alice " ,
"password" : " s3cr3t " ,
"active" : true ,
"address" : " Anhalter Straße 17 13, 67278 Bockenheim an der Weinstraße " ,
"friends" : [
{
"id" : 2 ,
"name" : " Bob "
},
{
"id" : 3 ,
"name" : " Charlie "
}
],
"grades" : {
"Math" : 1.0 ,
"English" : 2.2 ,
"Science" : 1.9 ,
"PE" : 4.0
}
}
{
"id" : 1 ,
"name" : " Alice " ,
"active" : " unknown " ,
"address" : " XXX " ,
"friends" : [
{
"id" : 2 ,
"name" : " B. "
},
{
"id" : 3 ,
"name" : " C. "
}
],
"grades" : {
"Math" : 1.0 ,
"English" : 1.0 ,
"Science" : 1.0 ,
"PE" : 1.0
}
}
Logbook ใช้ รหัสความสัมพันธ์ เพื่อเชื่อมโยงคำขอและการตอบกลับ ซึ่งอนุญาตคำขอและการตอบกลับที่เกี่ยวข้องกับการจับคู่ซึ่งโดยปกติจะอยู่ในตำแหน่งที่แตกต่างกันในไฟล์บันทึก
หากการใช้งานเริ่มต้นของรหัสความสัมพันธ์ไม่เพียงพอสำหรับกรณีการใช้งานของคุณ คุณอาจจัดให้มีการใช้งานแบบกำหนดเอง:
Logbook logbook = Logbook . builder ()
. correlationId ( new CustomCorrelationId ())
. build ();
การจัดรูปแบบ กำหนดวิธีการแปลงคำขอและการตอบกลับเป็นสตริงโดยทั่วไป ตัวจัดรูปแบบ ไม่ ได้ระบุว่าคำขอและการตอบกลับถูกบันทึกไปที่ใด - ผู้เขียนทำหน้าที่นั้น
Logbook มาพร้อมกับตัวจัดรูปแบบเริ่มต้นที่แตกต่างกันสองแบบ: HTTP และ JSON
HTTP คือรูปแบบการจัดรูปแบบเริ่มต้น ซึ่งระบุโดย DefaultHttpLogFormatter
โดยหลักแล้วออกแบบมาเพื่อใช้สำหรับการพัฒนาและการดีบักภายในเครื่อง ไม่ใช่สำหรับการใช้งานจริง เนื่องจากไม่สามารถอ่านด้วยเครื่องได้ง่ายเท่ากับ JSON
Incoming Request: 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b
GET http://example.org/test HTTP/1.1
Accept: application/json
Host: localhost
Content-Type: text/plain
Hello world!
Outgoing Response: 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b
Duration: 25 ms
HTTP/1.1 200
Content-Type: application/json
{ "value" : " Hello world! " }
JSON เป็นรูปแบบการจัดรูปแบบทางเลือกอื่น ซึ่งจัดทำโดย JsonHttpLogFormatter
ต่างจาก HTTP ตรงที่ได้รับการออกแบบมาเพื่อการใช้งานจริงเป็นหลัก — ผู้แยกวิเคราะห์และผู้บริโภคบันทึกสามารถใช้งานได้ง่าย
ต้องการการพึ่งพาดังต่อไปนี้:
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-json</ artifactId >
</ dependency >
{
"origin" : " remote " ,
"type" : " request " ,
"correlation" : " 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b " ,
"protocol" : " HTTP/1.1 " ,
"sender" : " 127.0.0.1 " ,
"method" : " GET " ,
"uri" : " http://example.org/test " ,
"host" : " example.org " ,
"path" : " /test " ,
"scheme" : " http " ,
"port" : null ,
"headers" : {
"Accept" : [ " application/json " ],
"Content-Type" : [ " text/plain " ]
},
"body" : " Hello world! "
}
{
"origin" : " local " ,
"type" : " response " ,
"correlation" : " 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b " ,
"duration" : 25 ,
"protocol" : " HTTP/1.1 " ,
"status" : 200 ,
"headers" : {
"Content-Type" : [ " text/plain " ]
},
"body" : " Hello world! "
}
หมายเหตุ: เนื้อความของประเภท application/json
(และ application/*+json
) จะถูก แทรก เข้าไปในแผนผัง JSON ที่เป็นผลลัพธ์ กล่าวคือ เนื้อหาการตอบสนอง JSON จะ ไม่ ถูกหลีกเลี่ยงและแสดงเป็นสตริง:
{
"origin" : " local " ,
"type" : " response " ,
"correlation" : " 2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b " ,
"duration" : 25 ,
"protocol" : " HTTP/1.1 " ,
"status" : 200 ,
"headers" : {
"Content-Type" : [ " application/json " ]
},
"body" : {
"greeting" : " Hello, world! "
}
}
Common Log Format (CLF) เป็นรูปแบบไฟล์ข้อความมาตรฐานที่ใช้โดยเว็บเซิร์ฟเวอร์เมื่อสร้างไฟล์บันทึกของเซิร์ฟเวอร์ รองรับรูปแบบผ่าน CommonsLogFormatSink
:
185.85.220.253 - - [02/Aug/2019:08:16:41 0000] "GET /search?q=zalando HTTP/1.1" 200 -
Extended Log Format (ELF) เป็นรูปแบบไฟล์ข้อความมาตรฐาน เช่น Common Log Format (CLF) ที่เว็บเซิร์ฟเวอร์ใช้เมื่อสร้างไฟล์บันทึก แต่ไฟล์ ELF จะให้ข้อมูลและความยืดหยุ่นมากกว่า รองรับรูปแบบนี้ผ่าน ExtendedLogFormatSink
ดูเอกสาร W3C ด้วย
ฟิลด์เริ่มต้น:
date time c-ip s-dns cs-method cs-uri-stem cs-uri-query sc-status sc-bytes cs-bytes time-taken cs-protocol cs(User-Agent) cs(Cookie) cs(Referrer)
ตัวอย่างเอาต์พุตบันทึกเริ่มต้น:
2019-08-02 08:16:41 185.85.220.253 localhost POST /search ?q=zalando 200 21 20 0.125 HTTP/1.1 "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" "name=value" "https://example.com/page?q=123"
ผู้ใช้สามารถแทนที่ฟิลด์เริ่มต้นด้วยฟิลด์ที่กำหนดเองผ่านตัวสร้างของ ExtendedLogFormatSink
:
new ExtendedLogFormatSink ( new DefaultHttpLogWriter (), "date time cs(Custom-Request-Header) sc(Custom-Response-Header)" )
สำหรับฟิลด์ส่วนหัว Http: cs(Any-Header)
และ sc(Any-Header)
ผู้ใช้สามารถระบุส่วนหัวใด ๆ ที่พวกเขาต้องการแยกออกจากคำขอ
ช่องอื่นๆ ที่รองรับจะแสดงรายการอยู่ในค่าของ ExtendedLogFormatSink.Field
ซึ่งสามารถใส่ลงในนิพจน์ช่องแบบกำหนดเองได้
cURL เป็นรูปแบบการจัดรูปแบบทางเลือก ซึ่งจัดทำโดย CurlHttpLogFormatter
ซึ่งจะแสดงคำขอเป็นคำสั่ง cURL
ที่ปฏิบัติการได้ ต่างจาก JSON ตรงที่ออกแบบมาเพื่อมนุษย์เป็นหลัก
curl -v -X GET ' http://localhost/test ' -H ' Accept: application/json '
ดู HTTP หรือระบุทางเลือกของตัวเองสำหรับการตอบกลับ:
new CurlHttpLogFormatter ( new JsonHttpLogFormatter ());
Splunk เป็นรูปแบบการจัดรูปแบบทางเลือก ซึ่งจัดทำโดย SplunkHttpLogFormatter
ซึ่งจะแสดงคำขอและการตอบกลับเป็นคู่คีย์-ค่า
origin=remote type=request correlation=2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b protocol=HTTP/1.1 sender=127.0.0.1 method=POST uri=http://example.org/test host=example.org scheme=http port=null path=/test headers={Accept=[application/json], Content-Type=[text/plain]} body=Hello world!
origin=local type=response correlation=2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b duration=25 protocol=HTTP/1.1 status=200 headers={Content-Type=[text/plain]} body=Hello world!
การเขียนจะกำหนดตำแหน่งที่เขียนคำขอและการตอบกลับตามรูปแบบ Logbook มาพร้อมกับการใช้งานสามแบบ: Logger, Stream และ Chunking
ตามค่าเริ่มต้น คำขอและการตอบกลับจะถูกบันทึกด้วยตัวบันทึก slf4j ที่ใช้หมวดหมู่ org.zalando.logbook.Logbook
และ trace
ระดับบันทึก สิ่งนี้สามารถปรับแต่งได้:
Logbook logbook = Logbook . builder ()
. sink ( new DefaultSink (
new DefaultHttpLogFormatter (),
new DefaultHttpLogWriter ()
))
. build ();
การใช้งานทางเลือกอื่นคือบันทึกคำขอและการตอบกลับไปยัง PrintStream
เช่น System.out
หรือ System.err
โดยปกติแล้วจะเป็นตัวเลือกที่ไม่ดีสำหรับการรันในการผลิต แต่บางครั้งอาจมีประโยชน์สำหรับการพัฒนาและ/หรือการสอบสวนในท้องถิ่นในระยะสั้น
Logbook logbook = Logbook . builder ()
. sink ( new DefaultSink (
new DefaultHttpLogFormatter (),
new StreamHttpLogWriter ( System . err )
))
. build ();
ChunkingSink
จะแบ่งข้อความยาวๆ ออกเป็นส่วนเล็กๆ และจะเขียนทีละข้อความในขณะที่มอบหมายไปยังซิงก์อื่น:
Logbook logbook = Logbook . builder ()
. sink ( new ChunkingSink ( sink , 1000 ))
. build ();
การผสมผสานระหว่าง HttpLogFormatter
และ HttpLogWriter
เหมาะกับกรณีการใช้งานส่วนใหญ่ แต่ก็มีข้อจำกัด การใช้อินเทอร์เฟซ Sink
โดยตรงจะทำให้มีกรณีการใช้งานที่ซับซ้อนมากขึ้น เช่น การเขียนคำขอ/การตอบกลับไปยังพื้นที่จัดเก็บข้อมูลถาวรที่มีโครงสร้าง เช่น ฐานข้อมูล
ซิงก์หลายตัวสามารถรวมเป็นอันเดียวได้โดยใช้ CompositeSink
คุณจะต้องลงทะเบียน LogbookFilter
เป็น Filter
ในกลุ่มตัวกรองของคุณ — ทั้งในไฟล์ web.xml
ของคุณ (โปรดทราบว่าวิธี xml จะใช้ค่าเริ่มต้นทั้งหมดและไม่สามารถกำหนดค่าได้):
< filter >
< filter-name >LogbookFilter</ filter-name >
< filter-class >org.zalando.logbook.servlet.LogbookFilter</ filter-class >
</ filter >
< filter-mapping >
< filter-name >LogbookFilter</ filter-name >
< url-pattern >/*</ url-pattern >
< dispatcher >REQUEST</ dispatcher >
< dispatcher >ASYNC</ dispatcher >
</ filter-mapping >
หรือโดยทางโปรแกรมผ่าน ServletContext
:
context . addFilter ( "LogbookFilter" , new LogbookFilter ( logbook ))
. addMappingForUrlPatterns ( EnumSet . of ( REQUEST , ASYNC ), true , "/*" );
ระวัง : ไม่รองรับการจัดส่ง ERROR
ขอแนะนำอย่างยิ่งให้สร้างการตอบกลับข้อผิดพลาดภายในการส่ง REQUEST
หรือ ASNYC
ตามค่าเริ่มต้น LogbookFilter
จะปฏิบัติต่อคำขอที่มี application/x-www-form-urlencoded
body ซึ่งไม่แตกต่างจากคำขออื่นๆ กล่าวคือ คุณจะเห็นเนื้อหาคำขอในบันทึก ข้อเสียของวิธีนี้คือ คุณจะไม่สามารถใช้เมธอด HttpServletRequest.getParameter*(..)
ใดๆ ได้ ดูปัญหา #94 สำหรับรายละเอียดเพิ่มเติม
ตั้งแต่ Logbook 1.5.0 ตอนนี้คุณสามารถระบุหนึ่งในสามกลยุทธ์ที่กำหนดวิธีที่ Logbook จัดการกับสถานการณ์นี้โดยใช้คุณสมบัติระบบ logbook.servlet.form-request
:
ค่า | ข้อดี | ข้อเสีย |
---|---|---|
body (ค่าเริ่มต้น) | ร่างกายถูกบันทึกไว้ | รหัสดาวน์สตรีม ไม่สามารถใช้ getParameter*() |
parameter | เนื้อความถูกบันทึกไว้ (แต่ถูกสร้างใหม่จากพารามิเตอร์) | รหัสดาวน์สตรีม ไม่สามารถใช้ getInputStream() |
off | โค้ดดาวน์สตรีมสามารถตัดสินใจได้ว่าจะใช้ getInputStream() หรือ getParameter*() | ร่างกาย ไม่ได้ถูกบันทึกไว้ |
แอปพลิเคชันที่ปลอดภัยมักต้องมีการตั้งค่าที่แตกต่างออกไปเล็กน้อย โดยทั่วไปคุณควรหลีกเลี่ยงการบันทึกคำขอที่ไม่ได้รับอนุญาต โดยเฉพาะอย่างยิ่งเนื้อหา เนื่องจากจะทำให้ผู้โจมตีสามารถท่วมไฟล์บันทึกของคุณได้อย่างรวดเร็ว และทำให้พื้นที่ดิสก์อันมีค่าของคุณลดลงด้วย สมมติว่าแอปพลิเคชันของคุณจัดการการอนุญาตภายในตัวกรองอื่น คุณมีสองทางเลือก:
คุณสามารถบรรลุการตั้งค่าเดิมได้อย่างง่ายดายโดยวาง LogbookFilter
ไว้หลังตัวกรองความปลอดภัยของคุณ อย่างหลังซับซ้อนกว่าเล็กน้อย คุณจะต้องมีอินสแตนซ์ LogbookFilter
ซ์ — หนึ่งอินสแตนซ์ก่อนตัวกรองความปลอดภัยของคุณ และอีกหนึ่งอินสแตนซ์หลังจากนั้น:
context . addFilter ( "SecureLogbookFilter" , new SecureLogbookFilter ( logbook ))
. addMappingForUrlPatterns ( EnumSet . of ( REQUEST , ASYNC ), true , "/*" );
context . addFilter ( "securityFilter" , new SecurityFilter ())
. addMappingForUrlPatterns ( EnumSet . of ( REQUEST ), true , "/*" );
context . addFilter ( "LogbookFilter" , new LogbookFilter ( logbook ))
. addMappingForUrlPatterns ( EnumSet . of ( REQUEST , ASYNC ), true , "/*" );
ตัวกรองสมุดจดรายการแรกจะบันทึกคำขอที่ไม่ได้รับอนุญาต เท่านั้น ตัวกรองที่สองจะบันทึกคำขอที่ได้รับอนุญาตเช่นเคย
โมดูล logbook-httpclient
มีทั้ง HttpRequestInterceptor
และ HttpResponseInterceptor
เพื่อใช้กับ HttpClient
:
CloseableHttpClient client = HttpClientBuilder . create ()
. addInterceptorFirst ( new LogbookHttpRequestInterceptor ( logbook ))
. addInterceptorFirst ( new LogbookHttpResponseInterceptor ())
. build ();
เนื่องจาก LogbookHttpResponseInterceptor
เข้ากันไม่ได้กับ HttpAsyncClient
จึงมีวิธีอื่นในการบันทึกการตอบกลับ:
CloseableHttpAsyncClient client = HttpAsyncClientBuilder . create ()
. addInterceptorFirst ( new LogbookHttpRequestInterceptor ( logbook ))
. build ();
// and then wrap your response consumer
client . execute ( producer , new LogbookHttpAsyncResponseConsumer <>( consumer ), callback )
โมดูล logbook-httpclient5
มี ExecHandler
เพื่อใช้กับ HttpClient
:
CloseableHttpClient client = HttpClientBuilder . create ()
. addExecInterceptorFirst ( "Logbook" , new LogbookHttpExecHandler ( logbook ))
. build ();
ควรเพิ่มตัวจัดการก่อน เพื่อให้มีการดำเนินการบีบอัดหลังจากการบันทึก และดำเนินการคลายการบีบอัดก่อนการบันทึก
เพื่อหลีกเลี่ยงการเปลี่ยนแปลงที่เสียหาย ยังมี HttpRequestInterceptor
และ HttpResponseInterceptor
เพื่อใช้กับ HttpClient
ซึ่งทำงานได้ดีตราบใดที่ไม่ได้ใช้การบีบอัด (หรือ ExecHandlers อื่น ๆ )
CloseableHttpClient client = HttpClientBuilder . create ()
. addRequestInterceptorFirst ( new LogbookHttpRequestInterceptor ( logbook ))
. addResponseInterceptorFirst ( new LogbookHttpResponseInterceptor ())
. build ();
เนื่องจาก LogbookHttpResponseInterceptor
เข้ากันไม่ได้กับ HttpAsyncClient
จึงมีวิธีอื่นในการบันทึกการตอบกลับ:
CloseableHttpAsyncClient client = HttpAsyncClientBuilder . create ()
. addRequestInterceptorFirst ( new LogbookHttpRequestInterceptor ( logbook ))
. build ();
// and then wrap your response consumer
client . execute ( producer , new LogbookHttpAsyncResponseConsumer <>( consumer ), callback )
บันทึก
รองรับ JAX-RS 2.x
การสนับสนุน JAX-RS 2.x (ดั้งเดิม) ลดลงใน Logbook 3.0 เป็น 3.6
ตั้งแต่ Logbook 3.7 การรองรับ JAX-RS 2.x กลับมาแล้ว
อย่างไรก็ตาม คุณต้องเพิ่ม ตัวแยกประเภท javax
เพื่อใช้โมดูล Logbook ที่เหมาะสม:
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-jaxrs</ artifactId >
< version >${logbook.version}</ version >
< classifier >javax</ classifier >
</ dependency >
คุณควรตรวจสอบให้แน่ใจด้วยว่าการขึ้นต่อกันต่อไปนี้อยู่บนคลาสพาธของคุณ ตามค่าเริ่มต้น logbook-jaxrs
นำเข้า jersey-client 3.x
ซึ่งเข้ากันไม่ได้กับ JAX-RS 2.x:
โมดูล logbook-jaxrs
ประกอบด้วย:
LogbookClientFilter
ที่จะใช้สำหรับแอปพลิเคชันที่ส่งคำขอ HTTP
client . register ( new LogbookClientFilter ( logbook ));
LogbookServerFilter
สำหรับใช้กับเซิร์ฟเวอร์ HTTP
resourceConfig . register ( new LogbookServerFilter ( logbook ));
โมดูล logbook-jdkserver
ให้การสนับสนุนเซิร์ฟเวอร์ JDK HTTP และมี:
LogbookFilter
ที่จะใช้กับเซิร์ฟเวอร์ในตัว
httpServer . createContext ( path , handler ). getFilters (). add ( new LogbookFilter ( logbook ))
โมดูล logbook-netty
ประกอบด้วย:
LogbookClientHandler
ที่จะใช้กับ HttpClient
:
HttpClient httpClient =
HttpClient . create ()
. doOnConnected (
( connection -> connection . addHandlerLast ( new LogbookClientHandler ( logbook )))
);
LogbookServerHandler
สำหรับใช้กับ HttpServer
:
HttpServer httpServer =
HttpServer . create ()
. doOnConnection (
connection -> connection . addHandlerLast ( new LogbookServerHandler ( logbook ))
);
ผู้ใช้ Spring WebFlux สามารถเลือกตัวเลือกใดก็ได้ต่อไปนี้:
NettyWebServer
โดยทางโปรแกรม (ผ่าน HttpServer
)NettyServerCustomizer
แบบกำหนดเองReactorClientHttpConnector
โดยทางโปรแกรม (ผ่าน HttpClient
)WebClientCustomizer
แบบกำหนดเองlogbook-spring-webflux
ผู้ใช้ Micronaut สามารถติดตามเอกสารอย่างเป็นทางการเกี่ยวกับวิธีการรวม Logbook เข้ากับ Micronaut
โมดูล logbook-okhttp2
มี Interceptor
เพื่อใช้กับเวอร์ชัน 2.x ของ OkHttpClient
:
OkHttpClient client = new OkHttpClient ();
client . networkInterceptors (). add ( new LogbookInterceptor ( logbook ));
หากคุณคาดหวังว่าจะได้รับคำตอบที่บีบอัดด้วย gzip คุณจะต้องลงทะเบียน GzipInterceptor
ของเราเพิ่มเติม การสนับสนุน gzip แบบโปร่งใสที่สร้างใน OkHttp จะทำงานหลังจากตัวสกัดกั้นเครือข่ายใดๆ ซึ่งบังคับให้สมุดจดรายการบันทึกบันทึกการตอบสนองแบบไบนารีที่ถูกบีบอัด
OkHttpClient client = new OkHttpClient ();
client . networkInterceptors (). add ( new LogbookInterceptor ( logbook ));
client . networkInterceptors (). add ( new GzipInterceptor ());
โมดูล logbook-okhttp
มี Interceptor
เพื่อใช้กับเวอร์ชัน 3.x ของ OkHttpClient
:
OkHttpClient client = new OkHttpClient . Builder ()
. addNetworkInterceptor ( new LogbookInterceptor ( logbook ))
. build ();
หากคุณคาดหวังว่าจะได้รับคำตอบที่บีบอัดด้วย gzip คุณจะต้องลงทะเบียน GzipInterceptor
ของเราเพิ่มเติม การสนับสนุน gzip แบบโปร่งใสที่สร้างใน OkHttp จะทำงานหลังจากตัวสกัดกั้นเครือข่ายใดๆ ซึ่งบังคับให้สมุดจดรายการบันทึกบันทึกการตอบสนองแบบไบนารีที่ถูกบีบอัด
OkHttpClient client = new OkHttpClient . Builder ()
. addNetworkInterceptor ( new LogbookInterceptor ( logbook ))
. addNetworkInterceptor ( new GzipInterceptor ())
. build ();
โมดูล logbook-ktor-client
ประกอบด้วย:
LogbookClient
ที่จะใช้กับ HttpClient
:
private val client = HttpClient ( CIO ) {
install( LogbookClient ) {
logbook = logbook
}
}
โมดูล logbook-ktor-server
ประกอบด้วย:
LogbookServer
ที่จะใช้กับ Application
:
private val server = embeddedServer( CIO ) {
install( LogbookServer ) {
logbook = logbook
}
}
หรือคุณสามารถใช้ logbook-ktor
ซึ่งจัดส่งทั้งโมดูล logbook-ktor-client
และ logbook-ktor-server
โมดูล logbook-spring
มี ClientHttpRequestInterceptor
เพื่อใช้กับ RestTemplate
:
LogbookClientHttpRequestInterceptor interceptor = new LogbookClientHttpRequestInterceptor ( logbook );
RestTemplate restTemplate = new RestTemplate ();
restTemplate . getInterceptors (). add ( interceptor );
Logbook มาพร้อมกับการกำหนดค่าอัตโนมัติที่สะดวกสบายสำหรับผู้ใช้ Spring Boot โดยจะตั้งค่าส่วนต่างๆ ต่อไปนี้ทั้งหมดโดยอัตโนมัติด้วยค่าเริ่มต้นที่สมเหตุสมผล:
แทนที่จะประกาศการพึ่งพา logbook-core
ให้ประกาศหนึ่งรายการกับ Spring Boot Starter:
< dependency >
< groupId >org.zalando</ groupId >
< artifactId >logbook-spring-boot-starter</ artifactId >
< version >${logbook.version}</ version >
</ dependency >
ทุก bean สามารถแทนที่และปรับแต่งได้หากจำเป็น เช่น:
@ Bean
public BodyFilter bodyFilter () {
return merge (
defaultValue (),
replaceJsonStringProperty ( singleton ( "secret" ), "XXX" ));
}
โปรดดู LogbookAutoConfiguration
หรือตารางต่อไปนี้เพื่อดูรายการจุดรวมที่เป็นไปได้:
พิมพ์ | ชื่อ | ค่าเริ่มต้น |
---|---|---|
FilterRegistrationBean | secureLogbookFilter | ขึ้นอยู่กับ LogbookFilter |
FilterRegistrationBean | logbookFilter | ขึ้นอยู่กับ LogbookFilter |
Logbook | ตามเงื่อนไข ฟิลเตอร์ ฟอร์แมตเตอร์ และตัวเขียน | |
Predicate<HttpRequest> | requestCondition | ไม่มีตัวกรอง จะถูกรวมเข้ากับ logbook.exclude และ logbook.exclude ในภายหลัง |
HeaderFilter | อ้างอิงจาก logbook.obfuscate.headers | |
PathFilter | อิงตาม logbook.obfuscate.paths | |
QueryFilter | อิงตาม logbook.obfuscate.parameters | |
BodyFilter | BodyFilters.defaultValue() โปรดดูการกรอง | |
RequestFilter | RequestFilters.defaultValue() โปรดดูการกรอง | |
ResponseFilter | ResponseFilters.defaultValue() โปรดดูการกรอง | |
Strategy | DefaultStrategy | |
AttributeExtractor | NoOpAttributeExtractor | |
Sink | DefaultSink | |
HttpLogFormatter | JsonHttpLogFormatter | |
HttpLogWriter | DefaultHttpLogWriter |
ตัวกรองหลายตัวถูกรวมเข้าเป็นหนึ่งเดียว
logbook-spring
บางคลาสจาก logbook-spring
จะรวมอยู่ในการกำหนดค่าอัตโนมัติ
คุณสามารถเชื่อมต่อ LogbookClientHttpRequestInterceptor
อัตโนมัติด้วยรหัสเช่น:
private final RestTemplate restTemplate ;
MyClient ( RestTemplateBuilder builder , LogbookClientHttpRequestInterceptor interceptor ){
this . restTemplate = builder
. additionalInterceptors ( interceptor )
. build ();
}
ตารางต่อไปนี้แสดงการกำหนดค่าที่ใช้ได้ (เรียงตามตัวอักษร):
การกำหนดค่า | คำอธิบาย | ค่าเริ่มต้น |
---|---|---|
logbook.attribute-extractors | รายการ AttributeExtractors รวมถึงการกำหนดค่า เช่น type (ปัจจุบันคือ JwtFirstMatchingClaimExtractor หรือ JwtAllMatchingClaimsExtractor ) claim-names และ claim-key | [] |
logbook.filter.enabled | เปิดใช้งาน LogbookFilter | true |
logbook.filter.form-request-mode | กำหนดวิธีจัดการคำขอแบบฟอร์ม | body |
logbook.filters.body.default-enabled | เปิดใช้งาน/ปิดใช้งานตัวกรองเนื้อหาเริ่มต้นที่รวบรวมโดย java.util.ServiceLoader | true |
logbook.format.style | รูปแบบการจัดรูปแบบ ( http , json , curl หรือ splunk ) | json |
logbook.httpclient.decompress-response | เปิดใช้งาน/ปิดใช้งานกระบวนการคลายการบีบอัดเพิ่มเติมสำหรับ HttpClient ด้วยเนื้อหาที่เข้ารหัส gzip (เพื่อวัตถุประสงค์ในการบันทึกเท่านั้น) นี่หมายถึงการบีบอัดเพิ่มเติมและผลกระทบต่อประสิทธิภาพที่เป็นไปได้ | false (ปิดการใช้งาน) |
logbook.minimum-status | สถานะขั้นต่ำในการเปิดใช้งานการบันทึก ( status-at-least และ body-only-if-status-at-least ) | 400 |
logbook.obfuscate.headers | รายชื่อส่วนหัวที่ต้องทำให้สับสน | [Authorization] |
logbook.obfuscate.json-body-fields | รายการช่องเนื้อหา JSON ที่จะสร้างความสับสน | [] |
logbook.obfuscate.parameters | รายการชื่อพารามิเตอร์ที่ต้องทำให้สับสน | [access_token] |
logbook.obfuscate.paths | รายการเส้นทางที่ต้องทำให้สับสน ตรวจสอบการกรองไวยากรณ์ | [] |
logbook.obfuscate.replacement | ค่าที่จะใช้แทนค่าที่สับสน | XXX |
logbook.predicate.include | รวมเฉพาะเส้นทางและวิธีการบางอย่าง (หากกำหนดไว้) | [] |
logbook.predicate.exclude | ยกเว้นเส้นทางและวิธีการบางอย่าง (แทนที่ logbook.predicate.include ) | [] |
logbook.secure-filter.enabled | เปิดใช้งาน SecureLogbookFilter | true |
logbook.strategy | กลยุทธ์ ( default , status-at-least , body-only-if-status-at-least , without-body ) | default |
logbook.write.chunk-size | แยกบรรทัดบันทึกออกเป็นชิ้นเล็กๆ จนถึง chunk-size | 0 (ปิดการใช้งาน) |
logbook.write.max-body-size | ตัดทอนเนื้อหาให้เหลืออักขระ max-body-size และต่อท้าย ... logbook.write.max-body-size | -1 (ปิดการใช้งาน) |
logbook :
predicate :
include :
- path : /api/**
methods :
- GET
- POST
- path : /actuator/**
exclude :
- path : /actuator/health
- path : /api/admin/**
methods :
- POST
filter.enabled : true
secure-filter.enabled : true
format.style : http
strategy : body-only-if-status-at-least
minimum-status : 400
obfuscate :
headers :
- Authorization
- X-Secret
parameters :
- access_token
- password
write :
chunk-size : 1000
attribute-extractors :
- type : JwtFirstMatchingClaimExtractor
claim-names : [ "sub", "subject" ]
claim-key : Principal
- type : JwtAllMatchingClaimsExtractor
claim-names : [ "sub", "iat" ]
สำหรับการกำหนดค่า Logback ขั้นพื้นฐาน
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
กำหนดค่า Logbook ด้วย LogstashLogbackSink
HttpLogFormatter formatter = new JsonHttpLogFormatter();
LogstashLogbackSink sink = new LogstashLogbackSink(formatter);
สำหรับเอาต์พุตเช่น
{
"@timestamp" : "2019-03-08T09:37:46.239+01:00",
"@version" : "1",
"message" : "GET http://localhost/test?limit=1",
"logger_name" : "org.zalando.logbook.Logbook",
"thread_name" : "main",
"level" : "TRACE",
"level_value" : 5000,
"http" : {
// logbook request/response contents
}
}
คุณมีความยืดหยุ่นในการปรับแต่งระดับการบันทึกเริ่มต้นโดยการเริ่มต้น LogstashLogbackSink
ด้วยระดับเฉพาะ ตัวอย่างเช่น:
LogstashLogbackSink sink = new LogstashLogbackSink(formatter, Level.INFO);
getWriter
และ/หรือ getParameter*()
ดูเซิร์ฟเล็ตสำหรับรายละเอียดเพิ่มเติมERROR
ขอแนะนำอย่างยิ่งว่าอย่าใช้เพื่อสร้างการตอบกลับข้อผิดพลาด หากคุณมีคำถาม ข้อกังวล รายงานข้อผิดพลาด ฯลฯ โปรดแจ้งปัญหาในตัวติดตามปัญหาของพื้นที่เก็บข้อมูลนี้
หากต้องการมีส่วนร่วม เพียงส่งคำขอและเพิ่มคำอธิบายสั้นๆ (1-2 ประโยค) ของการเพิ่มเติมหรือการเปลี่ยนแปลงของคุณ สำหรับรายละเอียดเพิ่มเติม ตรวจสอบหลักเกณฑ์การบริจาค
Grand Turk ซึ่งเป็นแบบจำลองของเรือฟริเกตอัตรา 6 สามเสากระโดงจากสมัยของเนลสัน - สมุดบันทึกและแผนภูมิ โดย JoJan ได้รับอนุญาตภายใต้ Creative Commons (Attribution-Share Alike 3.0 Unported)