1. 내용
1) 1편에서 Node JS Application(Frontend)을 Azure Application Insights에 대해서 트랜잭션 추적 모니터링을 진행해보았습니다.
2) 2편에서는 Backend API를 NodeJS로 생성 후 Frontend에서 이미지를 업로드하면 Backend API를 통해 Azure Blob Storage로 이미지를 업로드하는 과정에 대해서 테스트하고 Frontend 및 Backend 트랜잭션 추적 모니터링을 진행해보겠습니다.
2. 순서
1) Backend API Server 코드
2) Frontend Server 코드
3) 이미지 업로드 테스트 결과 확인
4) 트랜잭션 추적 모니터링 테스트 결과 확인
3. 테스트
1) Backend API Server 코드
- Backend API Server 생성 및 Azure Blob Storage, Application Insights 연결을 진행합니다.
- Node JS 코드
## Backend >> backend.js
require('dotenv').config();
const express = require('express');
const multer = require('multer');
const { BlobServiceClient } = require('@azure/storage-blob');
const appInsights = require('applicationinsights');
const cors = require('cors');
// Application Insights 설정
appInsights.setup(process.env.APPINSIGHTS_CONNECTION_STRING)
.setAutoDependencyCorrelation(true)
.setAutoCollectRequests(true)
.setAutoCollectPerformance(true)
.setAutoCollectExceptions(true)
.setAutoCollectDependencies(true)
.setAutoCollectConsole(true)
.setUseDiskRetryCaching(true)
.setSendLiveMetrics(true)
.setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
.start();
const client = appInsights.defaultClient;
const app = express();
const port = 8080;
// CORS 설정
app.use(cors());
// Multer 설정
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
// Azure Blob Storage 클라이언트 설정
const blobServiceClient = BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION_STRING);
const containerName = process.env.AZURE_STORAGE_CONTAINER_NAME || 'images';
// 컨테이너가 없으면 생성하는 함수
async function ensureContainer() {
const containerClient = blobServiceClient.getContainerClient(containerName);
try {
await containerClient.create();
console.log(`Container ${containerName} created`);
} catch (error) {
if (error.code === 'ContainerAlreadyExists') {
console.log(`Container ${containerName} already exists`);
} else {
throw error;
}
}
}
// 서버 시작 시 컨테이너 확인
ensureContainer().catch(console.error);
// 요청 추적 미들웨어
app.use((req, res, next) => {
// 프론트엔드에서 전달된 operation ID 사용
const operationId = req.headers['x-operation-id'] || req.headers['request-id'];
if (operationId) {
req.context = {
operationId: operationId,
startTime: Date.now()
};
// Application Insights에 요청 추적 설정
client.context.tags[client.context.keys.operationId] = operationId;
}
// 디버깅용 로그 출력
console.log("Operation ID:", req.context.operationId);
console.log("Parent ID:", req.context.parentId);
res.on('finish', () => {
if (req.context) {
const duration = Date.now() - req.context.startTime;
client.trackRequest({
name: `${req.method} ${req.url}`,
url: req.url,
duration: duration,
resultCode: res.statusCode,
success: res.statusCode < 400,
properties: {
operationId: req.context.operationId
}
});
}
});
next();
});
// 파일 업로드 엔드포인트
app.post('/upload', upload.single('file'), async (req, res) => {
const startTime = Date.now();
try {
if (!req.file) {
throw new Error('No file uploaded');
}
// 이벤트 추적 - 업로드 시작
client.trackEvent({
name: 'BlobUploadStarted',
properties: {
fileName: req.file.originalname,
operationId: req.context?.operationId
}
});
// Blob 이름 생성 (타임스탬프 + 원본 파일명)
const blobName = `${Date.now()}-${req.file.originalname}`;
const containerClient = blobServiceClient.getContainerClient(containerName);
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
// 파일 타입 확인 및 설정
const contentType = req.file.mimetype || 'application/octet-stream';
// Blob에 업로드
const uploadOptions = {
blobHTTPHeaders: {
blobContentType: contentType
}
};
const uploadStartTime = Date.now();
await blockBlobClient.upload(req.file.buffer, req.file.size, uploadOptions);
// Blob URL 생성
const blobUrl = blockBlobClient.url;
// 성공 이벤트 추적
client.trackEvent({
name: 'BlobUploadCompleted',
properties: {
fileName: req.file.originalname,
blobUrl: blobUrl,
operationId: req.context?.operationId
}
});
// Azure Blob Storage 종속성 추적
client.trackDependency({
target: 'Azure Blob Storage',
name: 'UploadBlob',
data: blobName,
duration: Date.now() - uploadStartTime,
resultCode: 200,
success: true,
properties: {
operationId: req.context?.operationId
}
});
res.json({
success: true,
message: 'File uploaded successfully',
url: blobUrl,
operationId: req.context?.operationId
});
} catch (error) {
console.error('Upload error:', error);
// 예외 추적
client.trackException({
exception: error,
properties: {
fileName: req.file?.originalname,
operationId: req.context?.operationId
}
});
res.status(500).json({
success: false,
message: 'Error uploading file',
error: error.message,
operationId: req.context?.operationId
});
}
});
// 서버 시작
app.listen(port, () => {
console.log(`Backend server running at http://localhost:${port}`);
client.trackEvent({ name: 'BackendServerStarted' });
});
// 예기치 않은 오류 처리
process.on('uncaughtException', (error) => {
client.trackException({ exception: error });
console.error('Uncaught Exception:', error);
});
process.on('unhandledRejection', (reason) => {
client.trackException({ exception: reason });
console.error('Unhandled Rejection:', reason);
});
# Backend >> package.json
{
"name": "nodejs_monitoring_backend",
"version": "1.0.0",
"main": "backend.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@azure/storage-blob": "^12.25.0",
"applicationinsights": "^3.4.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"multer": "^1.4.5-lts.1"
}
}
# Backend >> .env
# Azure Storage 설정
AZURE_STORAGE_CONNECTION_STRING="Azure Storage Account >> Access Key >> 연결 문자열 "
AZURE_STORAGE_CONTAINER_NAME="Blob Storage Container "
# Application Insights 설정
APPINSIGHTS_CONNECTION_STRING="Application Insights 연결 문자열 "
# 서버 설정
PORT=8080
2) Frontend Server 코드
- Frontend에서 Backend 연결 방법 및 이미지 업로드 방법 확인
- 1편에서 진행했던 Frontend app.js 코드를 backend api 서버와 정상적으로 연결하기 위해서 조금 변경을 진행하였습니다. 아래 나오는 내용은 Frontend app.js와 package.json & .env 파일에 대한 내용입니다.
- Frontend는 3001포트에서 실행하고 Backend는 8080에서 실행합니다.
# Frontend >> app.js
require('dotenv').config();
const express = require('express');
const multer = require('multer');
const path = require('path');
const axios = require('axios');
const appInsights = require('applicationinsights');
const { v4: uuidv4 } = require('uuid');
const FormData = require('form-data');
const fs = require('fs');
// Application Insights 설정
appInsights.setup(process.env.APPINSIGHTS_CONNECTION_STRING)
.setAutoDependencyCorrelation(true)
.setAutoCollectRequests(true)
.setAutoCollectPerformance(true)
.setAutoCollectExceptions(true)
.setAutoCollectDependencies(true)
.setAutoCollectConsole(true)
.setUseDiskRetryCaching(true)
.setSendLiveMetrics(true)
.setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
.start();
const client = appInsights.defaultClient;
const app = express();
const port = 3001;
// Multer 설정
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
// EJS 템플릿 엔진 설정
app.set('view engine', 'ejs');
// 정적 파일 제공
app.use(express.static('public'));
// 커스텀 미들웨어: 요청 추적
app.use((req, res, next) => {
if (!req.context) {
req.context = {};
}
req.context.operationId = req.headers['traceparent'] || uuidv4();
req.context.parentId = client.context.tags[client.context.keys.operationParentId];
req.context.startTime = Date.now();
// 디버깅용 로그 출력
console.log("Operation ID:", req.context.operationId);
console.log("Parent ID:", req.context.parentId);
res.on('finish', () => {
const duration = Date.now() - req.context.startTime;
client.trackRequest({
name: `${req.method} ${req.url}`,
url: req.url,
duration: duration,
resultCode: res.statusCode,
success: res.statusCode < 400,
properties: {
operationId: req.context.operationId
}
});
});
next();
});
// 메인 페이지
app.get('/', (req, res) => {
client.trackPageView({
name: 'Upload Page',
url: req.url,
properties: {
operationId: req.context.operationId
}
});
res.render('index');
});
// 파일 업로드 처리
app.post('/upload', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
throw new Error('No file uploaded');
}
client.trackEvent({
name: 'FileUploadStarted',
properties: {
fileName: req.file.originalname,
fileSize: req.file.size,
operationId: req.context.operationId
}
});
client.trackMetric({
name: 'FileSize',
value: req.file.size
});
const form = new FormData();
// Blob 대신 Buffer를 사용하여 파일 추가
form.append('file', req.file.buffer, { filename: req.file.originalname, contentType: req.file.mimetype });
const dependencyStartTime = Date.now();
try {
const backendResponse = await axios.post(
`${process.env.BACKEND_API_URL}/upload`,
form,
{
headers: {
...form.getHeaders(),
'Request-Id': req.context.operationId,
'x-operation-id': req.context.operationId
},
maxContentLength: Infinity,
maxBodyLength: Infinity
}
);
client.trackEvent({
name: 'FileUploadCompleted',
properties: {
fileName: req.file.originalname,
operationId: req.context.operationId,
duration: Date.now() - dependencyStartTime
}
});
client.trackDependency({
target: process.env.BACKEND_API_URL,
name: 'UploadToBackend',
data: 'POST /upload',
duration: Date.now() - dependencyStartTime,
resultCode: backendResponse.status,
success: true,
properties: {
operationId: req.context.operationId
}
});
res.json({
success: true,
message: 'File uploaded successfully',
url: backendResponse.data.url,
operationId: req.context.operationId
});
} catch (error) {
client.trackDependency({
target: process.env.BACKEND_API_URL,
name: 'UploadToBackend',
data: 'POST /upload',
duration: Date.now() - dependencyStartTime,
resultCode: error.response?.status || 500,
success: false,
properties: {
operationId: req.context.operationId
}
});
throw error;
}
} catch (error) {
client.trackException({
exception: error,
properties: {
fileName: req.file?.originalname,
operationId: req.context.operationId
}
});
res.status(500).json({
success: false,
message: 'Error uploading file',
error: error.message,
operationId: req.context.operationId
});
}
});
// 서버 시작
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
client.trackEvent({ name: 'ServerStarted' });
});
// 예기치 않은 오류 처리
process.on('uncaughtException', (error) => {
client.trackException({ exception: error });
console.error('Uncaught Exception:', error);
});
process.on('unhandledRejection', (reason) => {
client.trackException({ exception: reason });
console.error('Unhandled Rejection:', reason);
});
# Frontend >> package.json
{
"name": "testnodejs",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"applicationinsights": "^3.4.0",
"axios": "^1.7.7",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.21.1",
"form-data": "^4.0.1",
"multer": "^1.4.5-lts.1",
"testnodejs": "file:",
"uuid": "^11.0.2"
}
}
# Frontend >> .env
APPINSIGHTS_CONNECTION_STRING="InstrumentationKey=20b67a25-9c83-4340-ad3a-693484d76ff4;IngestionEndpoint=https://koreacentral-0.in.applicationinsights.azure.com/;LiveEndpoint=https://koreacentral.livediagnostics.monitor.azure.com/;ApplicationId=ebc309c9-04f4-4029-bec0-e33014e0b44e"
BACKEND_API_URL="http://localhost:8080"
# Server Port
PORT=3001
# Frontend >> view/index.ejs
<!DOCTYPE html>
<html>
<head>
<title>File Upload</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.upload-container {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
}
.upload-status {
margin-top: 20px;
padding: 10px;
display: none;
}
.success {
background-color: #d4edda;
color: #155724;
}
.error {
background-color: #f8d7da;
color: #721c24;
}
img {
max-width: 300px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="upload-container">
<h2>File Upload</h2>
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" required>
<button type="submit">Upload</button>
</form>
<div id="status" class="upload-status"></div>
<div id="imagePreview"></div>
</div>
<script>
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const statusDiv = document.getElementById('status');
const imagePreviewDiv = document.getElementById('imagePreview');
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
statusDiv.className = 'upload-status success';
statusDiv.textContent = `Upload successful! (Operation ID: ${result.operationId})`;
imagePreviewDiv.innerHTML = `<img src="${result.url}" alt="Uploaded image">`;
} else {
statusDiv.className = 'upload-status error';
statusDiv.textContent = `Upload failed: ${result.message}`;
imagePreviewDiv.innerHTML = '';
}
statusDiv.style.display = 'block';
} catch (error) {
statusDiv.className = 'upload-status error';
statusDiv.textContent = 'Error uploading file';
statusDiv.style.display = 'block';
imagePreviewDiv.innerHTML = '';
}
});
</script>
</body>
</html>
3) 이미지 업로드 테스트 결과 확인
- Frontend와 Backend 모두 실행 시켜놓고 Frontend에서 이미지를 업로드합니다.
- Operation ID를 가지고 트랜잭션 추적 모니터링이 가능합니다.



- 이미지가 Blob Storage에 정상적으로 업로드 됐는지 확인합니다.

4) 트랜잭션 추적 모니터링 테스트 결과 확인
- Azure Application Insights >> 트랜잭션 검색 >> 필터링 : operationID
예) operationID : f73f1248-670f-442f-8f05-ca0484212440


※ Custom Trace (OperationID) 생성하여 추적하는 것이 아닌 Application Insights Trace를 원한다면 trackTrace 메서드를 사용하면 됩니다. 다음은 trackTrace 메서드를 사용한 코드 예시입니다.
require('dotenv').config();
const express = require('express');
const multer = require('multer');
const path = require('path');
const axios = require('axios');
const appInsights = require('applicationinsights');
const { v4: uuidv4 } = require('uuid');
const FormData = require('form-data');
const fs = require('fs');
// Application Insights 설정
appInsights.setup(process.env.APPINSIGHTS_CONNECTION_STRING)
.setAutoDependencyCorrelation(true)
.setAutoCollectRequests(true)
.setAutoCollectPerformance(true)
.setAutoCollectExceptions(true)
.setAutoCollectDependencies(true)
.setAutoCollectConsole(true)
.setUseDiskRetryCaching(true)
.setSendLiveMetrics(true)
.setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
.start();
const client = appInsights.defaultClient;
const app = express();
const port = 3001;
// Multer 설정
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
// EJS 템플릿 엔진 설정
app.set('view engine', 'ejs');
// 정적 파일 제공
app.use(express.static('public'));
// 커스텀 미들웨어: 요청 추적
app.use((req, res, next) => {
if (!req.context) {
req.context = {};
}
req.context.operationId = req.headers['traceparent'] || uuidv4();
req.context.parentId = client.context.tags[client.context.keys.operationParentId];
req.context.startTime = Date.now();
// 디버깅용 트레이스 출력
client.trackTrace({
message: "Received a new request",
properties: {
operationId: req.context.operationId,
method: req.method,
url: req.url
}
});
res.on('finish', () => {
const duration = Date.now() - req.context.startTime;
client.trackRequest({
name: `${req.method} ${req.url}`,
url: req.url,
duration: duration,
resultCode: res.statusCode,
success: res.statusCode < 400,
properties: {
operationId: req.context.operationId
}
});
// 응답 후 디버깅용 트레이스 출력
client.trackTrace({
message: "Response sent",
properties: {
operationId: req.context.operationId,
duration: duration,
resultCode: res.statusCode
}
});
});
next();
});
// 메인 페이지
app.get('/', (req, res) => {
client.trackPageView({
name: 'Upload Page',
url: req.url,
properties: {
operationId: req.context.operationId
}
});
res.render('index');
});
// 파일 업로드 처리
app.post('/upload', upload.single('image'), async (req, res) => {
try {
if (!req.file) {
throw new Error('No file uploaded');
}
client.trackEvent({
name: 'FileUploadStarted',
properties: {
fileName: req.file.originalname,
fileSize: req.file.size,
operationId: req.context.operationId
}
});
client.trackMetric({
name: 'FileSize',
value: req.file.size
});
client.trackTrace({
message: "Preparing file for upload",
properties: {
fileName: req.file.originalname,
fileSize: req.file.size,
operationId: req.context.operationId
}
});
const form = new FormData();
form.append('file', req.file.buffer, { filename: req.file.originalname, contentType: req.file.mimetype });
const dependencyStartTime = Date.now();
try {
const backendResponse = await axios.post(
`${process.env.BACKEND_API_URL}/upload`,
form,
{
headers: {
...form.getHeaders(),
'Request-Id': req.context.operationId,
'x-operation-id': req.context.operationId
},
maxContentLength: Infinity,
maxBodyLength: Infinity
}
);
client.trackEvent({
name: 'FileUploadCompleted',
properties: {
fileName: req.file.originalname,
operationId: req.context.operationId,
duration: Date.now() - dependencyStartTime
}
});
client.trackDependency({
target: process.env.BACKEND_API_URL,
name: 'UploadToBackend',
data: 'POST /upload',
duration: Date.now() - dependencyStartTime,
resultCode: backendResponse.status,
success: true,
properties: {
operationId: req.context.operationId
}
});
client.trackTrace({
message: "File uploaded successfully to backend",
properties: {
fileName: req.file.originalname,
operationId: req.context.operationId,
backendStatus: backendResponse.status
}
});
res.json({
success: true,
message: 'File uploaded successfully',
url: backendResponse.data.url,
operationId: req.context.operationId
});
} catch (error) {
client.trackDependency({
target: process.env.BACKEND_API_URL,
name: 'UploadToBackend',
data: 'POST /upload',
duration: Date.now() - dependencyStartTime,
resultCode: error.response?.status || 500,
success: false,
properties: {
operationId: req.context.operationId
}
});
client.trackTrace({
message: "Error uploading file to backend",
properties: {
operationId: req.context.operationId,
errorMessage: error.message
}
});
throw error;
}
} catch (error) {
client.trackException({
exception: error,
properties: {
fileName: req.file?.originalname,
operationId: req.context.operationId
}
});
client.trackTrace({
message: "Error in file upload process",
properties: {
operationId: req.context.operationId,
errorMessage: error.message
}
});
res.status(500).json({
success: false,
message: 'Error uploading file',
error: error.message,
operationId: req.context.operationId
});
}
});
// 서버 시작
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
client.trackEvent({ name: 'ServerStarted' });
});
// 예기치 않은 오류 처리
process.on('uncaughtException', (error) => {
client.trackException({ exception: error });
client.trackTrace({
message: "Uncaught exception occurred",
properties: {
errorMessage: error.message
}
});
console.error('Uncaught Exception:', error);
});
process.on('unhandledRejection', (reason) => {
client.trackException({ exception: reason });
client.trackTrace({
message: "Unhandled promise rejection occurred",
properties: {
errorMessage: reason.message || 'No message available'
}
});
console.error('Unhandled Rejection:', reason);
});


4.마무리
- Frontend에서 Backend API를 통해 BlobStorage로 이미지를 업로드하였고 정상적으로 Application Insights에서 트랜잭션 추적 모니터링이 가능한 것을 볼 수 있었습니다.
- 본 테스트는 Application Insights 수동 계측 모드를 사용하였습니다.
- 수동 계측으로는 Application Insights SDK 와 Opentelemtery SDK가 있는데, 본 테스트는 Application Insights SDK를 사용하여 분산 추적 모니터링을 진행하였습니다.
5. 의문 및 정리
- Application Insights의 비용은 Log가 저장되는 LogAnalytics 비용으로 산정된다.
- 근데 LogAnalytics에 로그가 쌓이는건 생각보다 비싸다.. 비싸서 고객입장에서 사용하기 힘들 수 있다.
- 그래서 LogAnalytics에 있는 로그를 StorageAccount로 보내면 되지 않나? 라는 생각을 해봤다. 왜냐면 Storage Account 로그 저장 비용은 저렴하기 때문이다.
- 근데 여기서 간과하면 안되는점은 , LogAnalytics에 쌓이는 로그를 StorageAccount로 보낼 순 있지만 보낸다고 한들 LogAnalytics의 로그가 사라지는 것은 아니다. 그러면 Storage Account 비용과 LogAnalytics 비용 둘 다 발생하는 것이니 더 안쓸것이다.
- 그래서 결론적으로 Application Insights에서 실시간 모니터링 및 트랜잭션 분석을 하려면 LogAnalytics를 바라보고 모니터링을 한다. Storage Account를 바라보지는 않는다.
- 그러므로 만약 고객이 2년동안 로그를 보존해야하는 정책이 있다면, LogAnalytics의 로그 보존 기간은 30일 정도로 유지하고 연결된 Storage Account 로그 보존기간을 2년정도 설정하여 비용을 최적화해야한다.
- 아니면 3rd party 제품 쓰던지,, ㅋ (Grafana Tempo 알아봐야겠다.)
끄읏 ~.~
'Azure' 카테고리의 다른 글
| Azure CDN을 이용한 정적 웹 애플리케이션 서비스 (1) | 2024.11.28 |
|---|---|
| Azure Kubernetes Service & Grafana Tempo (0) | 2024.11.01 |
| Azure Application Insights를 이용한 Application Monitoring (nodejs)- 1/2 (0) | 2024.10.31 |
| Azure Application Insights를 이용한 Application Monitoring (java) (0) | 2024.10.29 |
| [Study] Public & Private AKS Cluster (0) | 2024.10.21 |