1. Spring Boot Actuator 활성화
Spring Boot 는 애플리케이션을 운영환경으로 전환할 때 애플리케이션을 모니터링하고 관리하는 데 도움이 되는 여러 가지 추가 기능이 포함되어 있다. 이들 기능은 Spring Boot Actuator 모듈에 대한 의존성을 추가하여 활성화 할 수 있다. 의존성은 spring-boot-starter-actuator 스타터를 사용한다.① 의존성 추가
spring-boot-starter-actuator 의존성을 프로젝트의 pom.xml 또는 build.gradle 파일에 추가한다. 이렇게 하면 Spring Actuator 관련 기능을 프로젝트에 포함시킬 수 있다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dependencies { | |
// actuator | |
implementation 'org.springframework.boot:spring-boot-starter-actuator' | |
} |
Spring Boot Actuator가 제공하는 주요 기능은 다음과 같다:
- 엔드포인트(Endpoints): Spring Boot Actuator는 다양한 HTTP 엔드포인트를 제공하여 애플리케이션의 상태 정보를 노출한다. 예를 들어, /actuator/health, /actuator/info, /actuator/metrics, /actuator/beans, /actuator/env 등의 엔드포인트를 통해 건강 상태, 애플리케이션 정보, 메트릭 데이터, 빈 정보, 환경 속성 정보 등을 확인할 수 있다.
- 메트릭 수집: Spring Boot Actuator는 애플리케이션의 성능과 동작에 관한 다양한 메트릭 데이터를 수집하고 노출한다. 이러한 메트릭 데이터는 모니터링 도구나 시각화 도구와 연동하여 성능 향상 및 문제 해결에 도움을 줄 수 있다.
- 응용 프로그램 정보: /actuator/info 엔드포인트를 통해 사용자 정의 애플리케이션 정보를 노출.
- 애플리케이션의 건강 검사: /actuator/health 엔드포인트를 사용하여 애플리케이션의 건강 상태를 확인하고 이에 따른 조치를 취할 수 있다.
- 환경 속성 관리: /actuator/env 엔드포인트를 통해 애플리케이션 환경 속성을 확인하고 조작할 수 있다.
❉ HTTP 엔드포인트는 일반적으로 URL 형식을 가지며, 웹 주소의 일부다. 예를 들어, "https://example.com/api/data"와 같은 형태가 될 수 있다.
2. 설정 및 보안 구성
Spring Boot Actuator 사용을 위해서 의존성을 추가하는 것으로는 충분하지 않으며 몇 가지 설정과 보안을 추가적으로 구성해야 한다.② 구성 파일 설정
Spring Actuator 의 기능 중 일부는 구성 파일(application.properties 또는 application.yml)에서 활성화하거나 구성해야 한다. 예를 들어, management.endpoints.web.exposure.include 속성을 사용하여 공개할 엔드포인트를 구성하거나, 엔드포인트의 경로를 사용자 정의할 수 있다.
③ 보안 설정
Spring Actuator 엔드포인트에 접근하는 데 보안을 구성해야 할 수 있습니다. 기본적으로 모든 엔드포인트는 보안이 적용되지 않았으며, 이를 구성하여 액세스 제한, 인증 또는 권한 부여를 설정할 수 있다.
④ 엔드포인트 사용
Spring Actuator의 엔드포인트를 사용하려면 브라우저 또는 HTTP 클라이언트를 통해 엔드포인트에 요청을 보낼 수 있다. 엔드포인트는 /actuator 경로 아래에 위치하며, 예를 들어 /actuator/health 엔드포인트는 애플리케이션의 건강 상태 정보를 제공한다. 다음은 health,beans,metrics 기능들을 웹 서비스 엔트포인트로 사용하고 jmx 는 사용하지 않는 경우 설정이다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
management: | |
# 2. Enable specific endpoints | |
endpoint: | |
health: | |
enabled: true | |
show-details: always | |
beans: | |
enabled: true | |
threaddump: | |
enabled: true | |
metrics: | |
enabled: true | |
endpoints: | |
# 1. Endpoint all disable | |
enabled-by-default: false | |
# 3. Exclude all endpoint for JMX and Expose specific endpoints | |
jmx: | |
exposure: | |
exclude: "*" | |
web: | |
# 5. Change Actuator Default path | |
# base-path: "/manage" | |
exposure: | |
include: health,beans,metrics | |
# 4. Use other port for Actuator | |
# server: | |
# port: 9999 | |
3. 모니터링 구현하기
모니터링은 Vue 기술을 사용하여 구현했다. 모니터링 포트는 별도로 지정하는 것이 보안 및 모니터링 행위가 주는 영향을 최소화하기위하여 좋을 것 같다.
◼︎ 환경
웹과 모니터링 포트를 같이 사용하는 경우 모니터링 API 호출로 인하여 http.server.requests 값이 상승하는 것을 확인할 수 있었다.
관련 코드는 아래와 같다.
- Computing Power
- Model : MacBook Pro (14-inch, 2021)
- CPU : Apple M1 Pro
- MENORY : 16GB
- DISK : 512 GB SSD
- OS : macOS 13.2.4 (22F66)
- TOOLS : Visual Studio Code, Java 11, Gradle, Docker
- Version Control : GitHub
- Back-End
- Programming Language : Java
- Framework : Spring Boot 2.7.12
- DBMS : MySql 8.0.33
- Front-End
- Vue3 , Vuetify3, pinia, axios, apexcharts
![]() |
<웹 프로그램과 모니터링 포트를 같이 사용하는 경우> |
웹과 모니터링 포트를 같이 사용하는 경우 모니터링 API 호출로 인하여 http.server.requests 값이 상승하는 것을 확인할 수 있었다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script setup lang="ts"> | |
import { computed, onMounted, ref } from 'vue'; | |
import { useTheme } from 'vuetify'; | |
import { useMgmtActuatorHealthStore } from "../../store/studio/mgmt/mgmt.actuator.health.store"; | |
const theme = useTheme(); | |
const primary = theme.current.value.colors.primary; | |
const lightprimary = theme.current.value.colors.lightprimary; | |
const mgmtActuatorHealthStore = useMgmtActuatorHealthStore(); | |
const loader = ref(false); | |
const chartOptions = computed(() => { | |
return { | |
labels: ['Used', 'Free'], | |
chart: { | |
type: 'donut', | |
fontFamily: `inherit`, | |
foreColor: '#a1aab2', | |
toolbar: { | |
show: false | |
} | |
}, | |
colors: [primary, lightprimary, '#F9F9FD'], | |
plotOptions: { | |
pie: { | |
startAngle: -90, | |
endAngle: 90, | |
offsetY: 10, | |
donut: { | |
size: '75%', | |
background: 'transparent' | |
} | |
} | |
}, | |
stroke: { | |
show: false | |
}, | |
dataLabels: { | |
enabled: false | |
}, | |
legend: { | |
show: false | |
}, | |
tooltip: { theme: "light", fillSeriesColor: false }, | |
}; | |
}); | |
const series = computed(() => { | |
return [ | |
(diskSpace.value.details.total - diskSpace.value.details.free) / diskSpace.value.details.total * 100, | |
(diskSpace.value.details.free / diskSpace.value.details.total * 100)]; | |
}) | |
const isDanger = computed(()=>{ | |
if( ( diskSpace.value.details.free / diskSpace.value.details.total * 100 ) < 10 ) | |
return true; | |
else | |
return false; | |
}); | |
const status = ref(); | |
const diskSpace = ref({ | |
status: "", | |
details: { | |
total: 0, | |
free: 0, | |
threshold: 0, | |
exists: false, | |
}, | |
}); | |
function bytes(data, to) { | |
const const_term = 1024; | |
// Convert the values and concatenate | |
// the appropriate unit | |
if (to === "KB") { | |
return (data / const_term).toFixed(3) + "KB"; | |
} else if (to === "MB") { | |
return (data / const_term ** 2).toFixed(3) + "MB"; | |
} else if (to === "GB") { | |
return (data / const_term ** 3).toFixed(3) + "GB"; | |
} else if (to === "TB") { | |
return (data / const_term ** 4).toFixed(3) + "TB"; | |
} else { | |
return "Please pass valid option"; | |
} | |
} | |
async function getData() { | |
loader.value = true; | |
if (!mgmtActuatorHealthStore.isLoaded) { | |
await mgmtActuatorHealthStore.fetch(); | |
} | |
status.value = mgmtActuatorHealthStore.dataItem.status; | |
diskSpace.value = mgmtActuatorHealthStore.dataItem.components.diskSpace; | |
loader.value = false; | |
} | |
onMounted(async () => { | |
getData(); | |
}) | |
</script> | |
<template> | |
<v-card elevation="10" class="withbg"> | |
<v-card-item> | |
<div class="d-sm-flex align-center justify-space-between pt-sm-2"> | |
<v-card-title class="text-h5">Disk Space</v-card-title> | |
</div> | |
<v-row> | |
<v-col cols="7" sm="7"> | |
<div class="mt-6"> | |
<h3 class="text-h3">Total : {{ bytes(diskSpace.details.total , 'GB') }}</h3> | |
<div class="mt-1"> | |
<v-avatar :class="isDanger ? 'bg-red-lighten-5 text-red-accent-4' : 'bg-lightsuccess text-success'" size="25"> | |
<!-- <CloudIcon size="20" /> --> | |
<v-icon :icon="isDanger ? 'mdi-folder-check' : 'mdi-folder-alert'"></v-icon> | |
</v-avatar> | |
<span class="text-subtitle-1 ml-2 font-weight-bold">Used : {{ bytes( diskSpace.details.total - diskSpace.details.free , 'GB')}}</span> | |
<span class="text-subtitle-1 text-muted ml-2">Free : {{ bytes( diskSpace.details.free , 'GB')}}</span> | |
</div> | |
<div class="d-flex align-center mt-sm-10 mt-8"> | |
<h6 class="text-subtitle-1 text-muted"> | |
<v-icon icon="mdi mdi-checkbox-blank-circle" class="mr-1" size="10" | |
color="primary"></v-icon> Used | |
</h6> | |
<h6 class="text-subtitle-1 text-muted pl-5"> | |
<v-icon icon="mdi mdi-checkbox-blank-circle" class="mr-1" size="10" | |
color="lightprimary"></v-icon> Free | |
</h6> | |
</div> | |
</div> | |
</v-col> | |
<v-col cols="5" sm="5" class="pl-lg-0"> | |
<div class="d-flex align-center flex-shrink-0"> | |
<apexchart class="pt-6" type="donut" height="145" :options="chartOptions" :series="series"> | |
</apexchart> | |
</div> | |
</v-col> | |
</v-row> | |
</v-card-item> | |
</v-card></template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<v-row> | |
<v-col cols="12"> | |
<v-row> | |
<v-col cols="12" lg="8"> | |
<v-row> | |
<v-col cols="12" lg="6"> | |
<Metrics :metric="'system.load.average.1m'" :name="'averate load'" | |
:title="'Average load value for the one minute'"> | |
</Metrics> | |
</v-col> | |
<v-col cols="12" lg="6"> | |
<Metrics :metric="'http.server.requests'" :name="'request'" :title="'Request Handling'" :color="'#E91E63'"></Metrics> | |
</v-col> | |
<v-col cols="12" lg="6"> | |
<Metrics :metric="'process.cpu.usage'" :name="'cpu usage'" :title="'Process CPU Usage'" :color="'#546E7A'"></Metrics> | |
</v-col> | |
<v-col cols="12" lg="6"> | |
<Metrics :metric="'jvm.memory.used'" :name="'memory used(GB)'" :title="'JVM Memory Usage'" :max="1000" :color="'#66DA26'"> | |
</Metrics> | |
</v-col> | |
</v-row> | |
</v-col> | |
<v-col cols="12" lg="4"> | |
<health></health> | |
<ActuatorBottomSheet></ActuatorBottomSheet> | |
</v-col> | |
</v-row> | |
</v-col> | |
</v-row> | |
</template> | |
<script setup lang="ts"> | |
import { onMounted, ref } from 'vue'; | |
import { useMgmtActuatorHealthStore } from '../../store/studio/mgmt/mgmt.actuator.health.store'; | |
import Metrics from '../../components/actuator/Metrics.vue'; | |
import Health from '../../components/actuator/Health.vue'; | |
import ActuatorBottomSheet from '@/components/actuator/ActuatorBottomSheet.vue'; | |
const mgmtActuatorHealthStore = useMgmtActuatorHealthStore(); | |
const loader = ref(false); | |
const dataItem = ref(); | |
const diskSpace = ref({ | |
status: "", | |
details: { | |
total: 0, | |
free: 0, | |
threshold: 0, | |
exists: false, | |
}, | |
}); | |
const dialogs = ref({ | |
env: { visible: false }, | |
}); | |
async function getData() { | |
loader.value = true; | |
if (!mgmtActuatorHealthStore.isLoaded) { | |
await mgmtActuatorHealthStore.fetch(); | |
} | |
dataItem.value = mgmtActuatorHealthStore.dataItem; | |
diskSpace.value = mgmtActuatorHealthStore.dataItem.components.diskSpace; | |
loader.value = false; | |
} | |
onMounted(async () => { | |
getData(); | |
}) | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<v-card elevation="10" class="withbg"> | |
<v-card-item> | |
<div class="d-sm-flex align-center justify-space-between pt-sm-2"> | |
<v-card-title class="text-h5">{{ title }}</v-card-title> | |
</div> | |
<v-row> | |
<v-col cols="12" sm="12" class="pl-lg-0"> | |
<div class="d-flex align-center flex-shrink-0"> | |
<apexchart class="pt-6" type="line" height="350" width="100%" :options="chartOptions" | |
:series="series"> | |
</apexchart> | |
</div> | |
</v-col> | |
</v-row> | |
</v-card-item> | |
</v-card> | |
</template> | |
<script setup lang="ts"> | |
import { computed, onMounted, onUnmounted, ref } from 'vue'; | |
import { useMgmtActuatorMetricsStore } from '../../store/studio/mgmt/mgmt.actuator.metrics.store' | |
// define props with default values. | |
const props = withDefaults(defineProps<{ | |
metric?: string, | |
name?: string, | |
title?: string, | |
max?: number, | |
color?: string, | |
}>(), { | |
metric: "jvm.memory.used", | |
title: "VM Memory Used", | |
name: "Memory Used", | |
max: 100, | |
color: '#2E93fA', | |
}); | |
const chartOptions = computed(() => { | |
return { | |
chart: { | |
height: 350, | |
width: '100%', | |
type: 'line', | |
zoom: { | |
enabled: false | |
}, | |
toolbar: { | |
show: false | |
}, | |
animations: { | |
enabled: true, | |
easing: 'linear', | |
dynamicAnimation: { | |
speed: 1000 | |
} | |
}, | |
}, | |
colors: [props.color], //, '#66DA26', '#546E7A', '#E91E63', '#FF9800'], | |
stroke: { | |
curve: 'smooth' | |
}, | |
dataLabels: { | |
enabled: false | |
}, | |
title: { | |
text: description.value, | |
align: 'left' | |
}, | |
markers: { | |
size: 0 | |
}, | |
xaxis: { | |
type: 'datetime', | |
labels: { | |
format: 'HH:mm ss', | |
formatter: function (value, timestamp) { | |
let date = new Date(timestamp); | |
const options = { hour: '2-digit', minute: '2-digit', second: '2-digit' , hour12: false }; | |
const formattedDate = new Intl.DateTimeFormat('en-US', options).format(date); | |
return formattedDate // new Date(timestamp) // The formatter function overrides format property | |
}, | |
} | |
}, | |
yaxis: { | |
max: props.max, | |
tooltip: { | |
enabled: false | |
} | |
}, | |
legend: { | |
show: false | |
}, | |
} | |
}); | |
const loader = ref(false); | |
const dataItems = ref([]); | |
const series = computed(() => { | |
return [{ | |
name: props.name, | |
data: dataItems.value | |
}]; | |
}); | |
const actuatorMetricsStore = useMgmtActuatorMetricsStore(); | |
const description = ref('Missing data (null values)'); | |
async function collectData() { | |
if (dataItems.value.length > 30) | |
dataItems.value.shift(); | |
let currentTimestamp = new Date().getTime(); | |
let data = await actuatorMetricsStore.getMetrixByName(props.metric); | |
if (data.description && data.description != null) | |
description.value = data.description; | |
let baseUnit = data.baseUnit; | |
let floatToInt = Math.floor(data.measurements[0].value); | |
if (baseUnit === 'bytes') { | |
floatToInt = ((floatToInt || 0 / 1024) / 1024) / 1024; | |
} | |
floatToInt = Number((floatToInt).toFixed(1)) | |
dataItems.value.push({ | |
x: currentTimestamp, | |
y: floatToInt | |
}); | |
} | |
const intervalTime: number = 2000; | |
var intervalId: number; | |
onMounted(async () => { | |
intervalId = setInterval(collectData, intervalTime); | |
}) | |
onUnmounted(() => { | |
clearInterval(intervalId) | |
}) | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { API_HEADERS, authHeader } from "@/util/helpers"; | |
import axios from "axios"; | |
import { defineStore } from "pinia"; | |
import { computed, ref } from "vue"; | |
import { useAlertStore } from "@/store/alert.store"; | |
export const useMgmtActuatorHealthStore = defineStore("mgmt-actuator-health-store", () => { | |
const baseUrl = `${import.meta.env.VITE_ACTUATOR_API_URL}/actuator/health`; | |
const alertStore = useAlertStore(); | |
// state | |
const isLoaded = ref<boolean>(false); | |
const dataItem = ref(); | |
// actions | |
async function fetch() { | |
const headers = { ...API_HEADERS, ...authHeader() }; | |
await axios | |
.get(baseUrl, { | |
params: {}, | |
headers: headers, | |
}) | |
.then((response) => { | |
const data = response.data; | |
dataItem.value = data; | |
isLoaded.value = true; | |
}) | |
.catch((err) => { | |
alertStore.error(err); | |
}); | |
} | |
return { | |
isLoaded, | |
dataItem, | |
fetch, | |
}; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { API_HEADERS, authHeader } from "@/util/helpers"; | |
import axios from "axios"; | |
import { defineStore } from "pinia"; | |
import { computed, ref } from "vue"; | |
import { useAlertStore } from "@/store/alert.store"; | |
export const useMgmtActuatorMetricsStore = defineStore("mgmt-actuator-metrics-store", () => { | |
const baseUrl = `${import.meta.env.VITE_ACTUATOR_API_URL}/actuator/metrics`; | |
const alertStore = useAlertStore(); | |
// state | |
const isLoaded = ref<boolean>(false); | |
const dataItem = ref(); | |
// actions | |
async function getMetrixByName(name: String) { | |
const headers = { ...API_HEADERS, ...authHeader() }; | |
const item = await axios | |
.get( | |
`${ | |
import.meta.env.VITE_ACTUATOR_API_URL | |
}/actuator/metrics/${name}`, | |
{ headers: headers } | |
) | |
.then((response) => { | |
const data = response.data; | |
return data; | |
}) | |
.catch((err) => { | |
alertStore.error(err); | |
}); | |
return item; | |
} | |
async function fetch() { | |
const headers = { ...API_HEADERS, ...authHeader() }; | |
await axios | |
.get(baseUrl, { | |
params: {}, | |
headers: headers, | |
}) | |
.then((response) => { | |
const data = response.data; | |
dataItem.value = data; | |
isLoaded.value = true; | |
}) | |
.catch((err) => { | |
alertStore.error(err); | |
}); | |
} | |
return { | |
isLoaded, | |
dataItem, | |
fetch, | |
getMetrixByName, | |
}; | |
}); |
댓글 없음:
댓글 쓰기