1) Zipkin을 설치하고자하는 폴더를 생성한 뒤 아래 명령어를 입력한다.
curl -sSL https://zipkin.io/quickstart.sh | bash -s
2) 아래 명령어를 입력하여 Zipkin을 실행한다.
java -jar zipkin.jar
3) http://localhost:9411/ 으로 접속해보자.
스프링 부트 환경에서 사용할 것이므로 아래 종속을 추가하였다.
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-zipkin', version: '2.2.4.RELEASE'
zipkin도 가장 최신 버전을 받았기 때문에 위 스타터팩도 가장 최신 버전을 사용하였다.
아래와 같이 잘 설치되었는지 확인하자
UserController를 생성하고 간단한 GET 메소드를 만들어보자.
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@GetMapping("/{name}")
public String callName(@PathVariable String name){
log.info(" >>> GET ");
return "call name "+name;
}
}
포스트맨을 이용해 api를 호출하자
중요한 것은 반환되는 결과값이 아니라, 로그이다.
위 컨트롤러를 보면 알 수 있듯이 >>> GET
이라는 짧은 로그를 남겼었는데, 서버에 찍혀있는 것을 보도록하자.
2020-08-10 20:19:15.499 INFO [,08e0e2fbe4c1eb6b,08e0e2fbe4c1eb6b,true] 15941 --- [nio-8080-exec-1] c.e.sleuth.controller.UserController : >>> GET
🤔 그냥 평범한 로그 같은데.. 싶으면 sleuth와 zipkin 라이브러리가 포함되지 않은 어플리케이션에서 찍힌 아래의 로그를 보자
2020-08-10 20:30:18.760 INFO 16362 --- [nio-8080-exec-2] c.e.e.apiConfroller.UserController : >>> GET
sleuth를 추가한 프로젝트의 로그에는 "INFO" 다음에 알 수 없는 문자열이 추가됐음을 알 수 있다.
application.properties에 다음과 같이 어플리케이션 이름을 추가한 뒤 재시작해보자.
2020-08-10 20:35:33.374 INFO [sleuth_example,,,] 16408 --- [ main] com.example.sleuth.SleuthApplication : Started SleuthApplication in 2.221 seconds (JVM running for 2.578)
INFO 뒤에 방금 추가한 이름이 나오는 것을 볼 수 있다.
뒤에는 , (콤마)
가 3개 찍혀있는데 각 콤마 사이는 아무래도 비워져있는 듯하다.
위에서 api를 요청했을 때 찍힌 로그의 일부를 다시 보도록하자.
[,08e0e2fbe4c1eb6b,08e0e2fbe4c1eb6b,true]
자, 여기에도 콤마가 3개가 찍혀있지만 첫 부분만 비워져있음을 알 수 있다.
그럼 이 상태에서 api 요청을 보내면 어떤 로그가 찍히게 될지 예상이 가는가 🤔
2020-08-10 20:47:17.662 INFO [sleuth_example,654228cc354b4b60,654228cc354b4b60,true] 16408 --- [nio-8080-exec-2] c.e.sleuth.controller.UserController : >>> GET
예상한대로 각 콤마를 기준으로 모든 부분이 채워져있다.
[sleuth_example,654228cc354b4b60,654228cc354b4b60,true]
해당 문자열이 의미하는 바는 다음과 같다.
sleuth_example
: 어플리케이션 이름654228cc354b4b60
: trace ID654228cc354b4b60
: span IDtrue
: exportable (true or false) - zipkin 등 외부로 이 값을 전송하는지 여부❗️ NOTE
zipkin과 연동을 위한 라이브러리 종속(group: 'org.springframework.cloud', name: 'spring-cloud-starter-zipkin'
)을 추가했기때문에 exportable의 디폴트값이true
로 설정되어있다.
application.properties에 아래 설정을 추가해 주었다.
spring.sleuth.sampler.probability
: 어플리케이션으로 오늘 요청 중 초당 몇 퍼센트나 트렌잭션 정보를 외부로 전달할지 설정한다. 0.0 ~ 1.0 값을 사용할 수 있으며 디폴트는 0.1 (10%)이다.spring.zipkin.base-url
: 데이터를 전송할 zipkin 서버 urlrestTemplate을 이용하여 다른 host로의 요청을 추적해야하기 때문에 단일프로젝트를 멀티프로젝트로 확장할 것이다.
🔎 settings.gradle
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'sleuth'
include 'common'
include 'module-first'
include 'module-second'
module-one과 module-two라는 이름을 가지는 모듈로 분리할 것이다.
🔎 build.gradle
buildscript {
ext {
springBootVersion = '2.3.2.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath "io.spring.gradle:dependency-management-plugin:1.0.9.RELEASE"
}
}
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
test {
useJUnitPlatform()
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-zipkin', version: '2.2.4.RELEASE'
}
task initSourceFolders {
sourceSets*.java.srcDirs*.each {
if (!it.exists()) {
it.mkdirs()
}
}
sourceSets*.resources.srcDirs*.each {
if (!it.exists()) {
it.mkdirs()
}
}
}
}
project(':common') {
dependencies {
}
}
project(':module-first') {
dependencies {
compile project(':common')
implementation 'org.springframework.boot:spring-boot-starter-web'
}
}
project(':module-second') {
dependencies {
compile project(':common')
implementation 'org.springframework.boot:spring-boot-starter-web'
}
}
자동으로 폴더가 생성되도록 구성하였는데 너무 시간이 오래걸려서 중간에 중단하고 직접 구성하였다. 🤔
module-first | module-second |
---|---|
module-first의 application.properties
server.port=8080
spring.application.name= first-point
spring.sleuth.sampler.probability=1.0
spring.zipkin.base-url= http://localhost:9411
module-second의 application.properties
server.port=8081
spring.application.name= second-point
spring.sleuth.sampler.probability=1.0
spring.zipkin.base-url= http://localhost:9411
두 서버를 각각 실행시켜야하므로 port를 다르게 지정해준다.
따로 gradlew
스크립트를 구성하지 않았으므로 그레들의 bootRun을 직접 실행시켜주자.
module-first
와 module-second
를 각각 실행한 뒤 로그는 다음과 같다.
// module-first
2020-08-10 22:41:20.369 INFO [first-point,,,] 17770 --- [ main] com.example.FirstModuleApplication : Started FirstModuleApplication in 2.181 seconds (JVM running for 2.516)
// module-second
2020-08-10 22:41:43.483 INFO [second-point,,,] 17776 --- [ main] com.example.SecondModuleApplication : Started SecondModuleApplication in 2.059 seconds (JVM running for 2.41)
application.properties에서 지정한 어플리케이션 이름으로 출력되는 것을 확인할 수 있다.
처음으로 요청을 보낼 곳이다. 이 모듈에서 restTemplate을 사용해 두번째 모듈로 요청을 또다시 보내게된다.
🔎 config.CommonConfig
이 부분을 common 모듈에 넣어서 사용하고 싶었는데 잘 안돼서 위치를 옮겨주었다.. 😂
@Configuration
public class CommonConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
🔎 controller.FirstController
@RestController
@RequestMapping("/first")
@RequiredArgsConstructor
@Slf4j
public class FirstController {
@Autowired
private final FirstService firstService;
@GetMapping("/start")
public String start() {
log.info(">>> start .. first controller ... ");
String result = firstService.sendSecond();
log.info(">>> first controller ... {}",result);
return result;
}
}
🔎 service.FirstService
@RequiredArgsConstructor
@Component
@Slf4j
public class FirstService {
private static final String secondUri = "http://localhost:8081/second";
private final RestTemplate restTemplate;
public String sendSecond() {
log.info(">>> first service");
String response = restTemplate.getForObject(secondUri+"/ping", String.class);
log.info(">>> from second-point .... response : {}", response);
return "finish";
}
}
"http://localhost:8080/first/start"로 요청을 보내면 "http://localhost:8081/second/ping"으로 또 한 번 요청을 보내게 되어있다. 이를 참고하여 나머지 코드도 작성해보자.
🔎 controller.SecondController
@RestController
@RequestMapping("/second")
@RequiredArgsConstructor
@Slf4j
public class SecondController {
private final SecondService secondService;
@GetMapping("/ping")
public String ping() {
log.info(">>> second-point .... ");
String result = secondService.ping();
log.info(">>> second-point .... {} ", result);
return result;
}
}
🔎 service.SecondService
@RequiredArgsConstructor
@Component
@Slf4j
public class SecondService {
public String ping(){
log.info(">>> second service ... end");
return "request ping success!!";
}
}
Zipkin 서버를 실행시킨 후에, 각 모듈을 bootRun으로 실행한 뒤 포스트맨으로 요청을 보내보자
module-first의 로그
module-second의 로그
이를 흐름에 따라 정리하면 다음과 같다.
application-name | location | trace ID | span ID | log |
---|---|---|---|---|
first-point | FirstController | d83616d1e009db9c | d83616d1e009db9c | >>> start .. first controller ... |
first-point | FirstService | d83616d1e009db9c | d83616d1e009db9c | >>> first service |
second-point | SecondController | d83616d1e009db9c | fadc35d0ff59d7fc | >>> second-point .... |
second-point | SecondService | d83616d1e009db9c | fadc35d0ff59d7fc | >>> second service ... end |
second-point | SecondController | d83616d1e009db9c | fadc35d0ff59d7fc | >>> second-point .... request ping success!! |
first-point | FirstService | d83616d1e009db9c | d83616d1e009db9c | >>> from second-point .... response : request ping success!! |
first-point | FirstController | d83616d1e009db9c | d83616d1e009db9c | >>> first controller ... finish |
값이 변경되는 지점을 굵게 표시해보았다.
trace ID(d83616d1e009db9c
)는 처음 요청이 끝날 때까지 변하지 않는다. 즉, 서로 다른 두 서버를 지나면서도 유지되었다.
반면에 span ID는 동일한 서버에서만 동일한 값을 가지고 있음을 알 수 있다.
d83616d1e009db9c
fadc35d0ff59d7fc
http://localhost:9411/ 로 접속하여 좀전에 보낸 요청으로 인해 발생한 trace를 찾아본다.
spring.application.name
값검색버튼을 눌러도 아무것도 나오지 않는다면 추적한 시각을 늘려보자 (검색버튼 바로 옆의
2HOURS
부분)
검색 결과를 클릭하면 다음과 같은 화면으로 전환된다.
SHOW ALL ANNOTATIONS를 눌러보면 다음과 같이 span에 대한 자세한 내용을 확인할 수 있다.
FIRST-POINT | SECOND-POINT |
---|---|
전체 코드는 github에서 확인할 수 있습니다.
감사합니다.