🔧 DevOps

New Relic

애플리케이션 성능 모니터링 플랫폼 - APM, 인프라, 로그를 통합한 클라우드 기반 옵저버빌리티 솔루션

📖 상세 설명

New Relic은 2008년에 설립된 옵저버빌리티(Observability) 분야의 선두 기업으로, 애플리케이션 성능 모니터링(APM)에서 시작하여 현재는 메트릭, 이벤트, 로그, 트레이스(MELT)를 통합 분석하는 풀스택 옵저버빌리티 플랫폼으로 발전했습니다. SaaS 기반으로 제공되어 별도의 인프라 구축 없이 즉시 사용할 수 있으며, 월 100GB의 무료 데이터 수집 티어를 제공합니다.

New Relic의 핵심 기능은 크게 다섯 가지로 나뉩니다. APM은 코드 레벨의 성능 분석과 분산 트레이싱을 제공하고, Infrastructure는 호스트, 컨테이너, 클라우드 서비스의 리소스 모니터링을 담당합니다. Logs는 로그 수집 및 분석을, Browser와 Mobile은 실사용자 경험(RUM)을 추적합니다. 이 모든 데이터는 New Relic Query Language(NRQL)로 통합 분석할 수 있어 상관관계 파악이 용이합니다.

New Relic의 강점은 자동 계측(Auto-instrumentation)과 AI 기반 이상 탐지입니다. Java, Python, Node.js 등 주요 언어에 에이전트를 설치하면 코드 수정 없이 자동으로 트랜잭션, DB 쿼리, 외부 서비스 호출을 추적합니다. Applied Intelligence는 머신러닝으로 비정상 패턴을 감지하고, 연관된 알림을 그룹화하여 알림 피로도를 줄여줍니다. 또한 Service Map으로 마이크로서비스 간 의존성을 시각화합니다.

요금 체계는 사용량 기반(consumption-based)으로, 수집된 데이터 양에 따라 과금됩니다. 데이터 보존 기간, 사용자 유형(Full platform user vs Basic user)에 따라 비용이 달라지므로 사전에 예상 비용을 계산하는 것이 중요합니다. 오픈소스 텔레메트리 표준인 OpenTelemetry와의 호환성도 뛰어나 벤더 종속 없이 계측 데이터를 수집할 수 있습니다.

💻 코드 예제

Python 애플리케이션 APM 설정

# requirements.txt에 추가
# newrelic

# newrelic.ini 설정 파일 생성
# newrelic-admin generate-config YOUR_LICENSE_KEY newrelic.ini

# newrelic.ini 주요 설정
"""
[newrelic]
license_key = YOUR_LICENSE_KEY
app_name = My Python Application
monitor_mode = true
log_level = info
ssl = true
high_security = false
transaction_tracer.enabled = true
transaction_tracer.transaction_threshold = apdex_f
transaction_tracer.record_sql = obfuscated
transaction_tracer.stack_trace_threshold = 0.5
error_collector.enabled = true
browser_monitoring.auto_instrument = true
"""

# Flask 애플리케이션 예제
import newrelic.agent
newrelic.agent.initialize('newrelic.ini')

from flask import Flask, jsonify
import time

app = Flask(__name__)

@app.route('/api/users/')
def get_user(user_id):
    # 자동으로 트랜잭션 추적됨
    # Custom attribute 추가
    newrelic.agent.add_custom_attribute('user_id', user_id)

    # DB 조회 시뮬레이션 (자동으로 DB 쿼리 추적됨)
    user = fetch_user_from_db(user_id)
    return jsonify(user)

@app.route('/api/process')
def process_data():
    # Custom transaction 이름 지정
    newrelic.agent.set_transaction_name('process_heavy_computation')

    # Custom segment로 세부 측정
    with newrelic.agent.FunctionTrace('heavy_computation'):
        result = heavy_computation()

    return jsonify({'result': result})

# 백그라운드 태스크 추적
@newrelic.agent.background_task()
def process_background_job(job_id):
    newrelic.agent.add_custom_attribute('job_id', job_id)
    # 작업 수행
    pass

# 에러 수동 기록
def risky_operation():
    try:
        # 위험한 작업
        pass
    except Exception as e:
        # New Relic에 에러 기록
        newrelic.agent.notice_error()
        raise

if __name__ == '__main__':
    app.run(debug=True)

커스텀 메트릭 및 이벤트 전송

import newrelic.agent
from newrelic.api.application import application_instance
import requests
import time

class NewRelicMetrics:
    def __init__(self, insert_key: str, account_id: str):
        self.insert_key = insert_key
        self.account_id = account_id
        self.metric_api_url = "https://metric-api.newrelic.com/metric/v1"
        self.event_api_url = f"https://insights-collector.newrelic.com/v1/accounts/{account_id}/events"

    def record_custom_metric(self, name: str, value: float):
        """에이전트를 통한 커스텀 메트릭 기록"""
        app = application_instance()
        if app:
            newrelic.agent.record_custom_metric(f'Custom/{name}', value, app)

    def send_metric_api(self, metrics: list):
        """Metric API를 통한 직접 전송"""
        payload = [{
            "metrics": metrics
        }]

        headers = {
            "Content-Type": "application/json",
            "Api-Key": self.insert_key
        }

        response = requests.post(self.metric_api_url, json=payload, headers=headers)
        return response.status_code == 202

    def send_custom_event(self, event_type: str, attributes: dict):
        """커스텀 이벤트 전송"""
        event = {
            "eventType": event_type,
            "timestamp": int(time.time()),
            **attributes
        }

        headers = {
            "Content-Type": "application/json",
            "X-Insert-Key": self.insert_key
        }

        response = requests.post(self.event_api_url, json=[event], headers=headers)
        return response.status_code == 200


# 사용 예시
metrics = NewRelicMetrics(
    insert_key="NRII-xxxxxxxxxxxx",
    account_id="1234567"
)

# 에이전트 메트릭 기록
metrics.record_custom_metric('OrderProcessingTime', 1.5)

# API로 gauge 메트릭 전송
metrics.send_metric_api([
    {
        "name": "custom.queue.depth",
        "type": "gauge",
        "value": 42,
        "timestamp": int(time.time()),
        "attributes": {
            "queue.name": "orders",
            "environment": "production"
        }
    }
])

# 커스텀 비즈니스 이벤트 전송
metrics.send_custom_event("OrderPlaced", {
    "orderId": "ORD-12345",
    "userId": "USR-67890",
    "totalAmount": 99.99,
    "itemCount": 3,
    "paymentMethod": "credit_card"
})

NRQL 쿼리 예제

-- 애플리케이션 응답 시간 추이
SELECT average(duration)
FROM Transaction
WHERE appName = 'My Python Application'
SINCE 1 hour ago
TIMESERIES 5 minutes

-- 느린 트랜잭션 Top 10
SELECT name, average(duration), count(*)
FROM Transaction
WHERE appName = 'My Python Application'
SINCE 1 day ago
FACET name
ORDER BY average(duration) DESC
LIMIT 10

-- 에러율 추이
SELECT percentage(count(*), WHERE error IS true) as 'Error Rate'
FROM Transaction
WHERE appName = 'My Python Application'
SINCE 24 hours ago
TIMESERIES 1 hour

-- DB 쿼리 성능 분석
SELECT average(databaseDuration), count(*)
FROM Transaction
WHERE appName = 'My Python Application'
AND databaseCallCount > 0
FACET databaseCallCount
SINCE 1 hour ago

-- 커스텀 이벤트 분석 (비즈니스 메트릭)
SELECT sum(totalAmount) as 'Total Revenue',
       count(*) as 'Order Count',
       average(totalAmount) as 'Average Order Value'
FROM OrderPlaced
SINCE 7 days ago
TIMESERIES 1 day

-- 인프라: 높은 CPU 사용률 호스트
SELECT average(cpuPercent)
FROM SystemSample
WHERE cpuPercent > 80
FACET hostname
SINCE 1 hour ago

-- 서비스 간 의존성 분석
SELECT count(*)
FROM Span
WHERE appName = 'My Python Application'
FACET externalHost
SINCE 1 hour ago

-- Apdex 점수 계산
SELECT apdex(duration, t: 0.5)
FROM Transaction
WHERE appName = 'My Python Application'
SINCE 1 day ago
TIMESERIES 1 hour

Terraform으로 New Relic 알림 설정

# providers.tf
terraform {
  required_providers {
    newrelic = {
      source  = "newrelic/newrelic"
      version = "~> 3.0"
    }
  }
}

provider "newrelic" {
  account_id = var.newrelic_account_id
  api_key    = var.newrelic_api_key
  region     = "US"
}

# variables.tf
variable "newrelic_account_id" {
  type        = string
  description = "New Relic Account ID"
}

variable "newrelic_api_key" {
  type        = string
  sensitive   = true
  description = "New Relic API Key"
}

variable "app_name" {
  type    = string
  default = "My Python Application"
}

# alert_policy.tf
resource "newrelic_alert_policy" "app_policy" {
  name                = "${var.app_name} Alert Policy"
  incident_preference = "PER_CONDITION_AND_TARGET"
}

# 응답 시간 알림
resource "newrelic_nrql_alert_condition" "high_response_time" {
  policy_id = newrelic_alert_policy.app_policy.id
  name      = "High Response Time"
  type      = "static"

  nrql {
    query = "SELECT average(duration) FROM Transaction WHERE appName = '${var.app_name}'"
  }

  critical {
    operator              = "above"
    threshold             = 2.0
    threshold_duration    = 300
    threshold_occurrences = "all"
  }

  warning {
    operator              = "above"
    threshold             = 1.0
    threshold_duration    = 300
    threshold_occurrences = "all"
  }
}

# 에러율 알림
resource "newrelic_nrql_alert_condition" "error_rate" {
  policy_id = newrelic_alert_policy.app_policy.id
  name      = "High Error Rate"
  type      = "static"

  nrql {
    query = "SELECT percentage(count(*), WHERE error IS true) FROM Transaction WHERE appName = '${var.app_name}'"
  }

  critical {
    operator              = "above"
    threshold             = 5.0
    threshold_duration    = 300
    threshold_occurrences = "at_least_once"
  }
}

# Slack 알림 채널
resource "newrelic_notification_destination" "slack" {
  name = "Slack Alerts"
  type = "SLACK"

  property {
    key   = "url"
    value = var.slack_webhook_url
  }
}

resource "newrelic_notification_channel" "slack_channel" {
  name           = "slack-alerts"
  type           = "SLACK"
  destination_id = newrelic_notification_destination.slack.id
  product        = "IINT"

  property {
    key   = "channelId"
    value = var.slack_channel_id
  }
}

# 알림 워크플로우
resource "newrelic_workflow" "alert_workflow" {
  name                  = "${var.app_name} Alert Workflow"
  muting_rules_handling = "NOTIFY_ALL_ISSUES"

  issues_filter {
    name = "policy-filter"
    type = "FILTER"

    predicate {
      attribute = "labels.policyIds"
      operator  = "EXACTLY_MATCHES"
      values    = [newrelic_alert_policy.app_policy.id]
    }
  }

  destination {
    channel_id = newrelic_notification_channel.slack_channel.id
  }
}

🗣️ 실무에서 이렇게 말해요

  • "New Relic APM 보니까 이 엔드포인트 평균 응답 시간이 2초 넘어요. 트레이스 열어서 어디서 병목인지 확인해볼게요."
  • "NRQL로 지난 주 대비 에러율 추이 뽑아봤는데, 목요일 배포 이후로 증가했네요. 연관된 에러 로그 같이 볼까요?"
  • "Applied Intelligence가 이상 탐지해서 알림 왔어요. DB 커넥션 풀 고갈 패턴이래요."
  • "이번 달 New Relic 비용이 예상보다 높아요. 로그 수집량 줄이거나 샘플링 비율 조정해야 할 것 같아요."
  • "APM 도구 경험 중 New Relic을 어떤 상황에서 활용했고, 어떤 문제를 해결했나요?"
  • "New Relic의 분산 트레이싱이 마이크로서비스 디버깅에 어떻게 도움이 되나요? 실제 사례를 설명해주세요."
  • "NRQL을 활용해서 성능 문제를 분석한 경험이 있나요? 어떤 쿼리를 주로 사용하셨나요?"
  • "옵저버빌리티 도구의 비용 최적화 경험이 있나요? New Relic 사용량을 어떻게 관리하셨나요?"
  • "이 함수 성능이 중요한데 New Relic custom segment로 감싸서 개별 측정하면 좋겠어요."
  • "여기 catch 블록에서 에러 무시하지 말고 newrelic.agent.notice_error() 추가해주세요."
  • "user_id 같은 비즈니스 컨텍스트 custom attribute로 추가하면 디버깅할 때 유용해요."
  • "민감 정보가 로그에 포함되는데, New Relic 수집 전에 마스킹 처리해야 해요."

⚠️ 주의사항

  • 데이터 비용 관리: New Relic은 수집 데이터 양에 따라 과금되므로 예상치 못한 비용 폭증에 주의해야 합니다. 특히 로그 수집, 분산 트레이싱, 커스텀 이벤트의 볼륨을 모니터링하고 필요 시 샘플링 비율을 조정하세요. Drop Filter로 불필요한 데이터를 수집 전에 필터링하는 것도 좋은 방법입니다.
  • 민감 데이터 노출 방지: APM과 로그 수집 시 비밀번호, 신용카드 번호, 개인정보 등이 실수로 전송될 수 있습니다. newrelic.ini의 attributes.exclude 설정으로 민감 파라미터를 제외하고, 로그 파싱 규칙에서 마스킹을 적용하세요. GDPR, HIPAA 등 규정 준수가 필요한 환경에서는 특히 주의가 필요합니다.
  • 에이전트 오버헤드 고려: APM 에이전트는 런타임 오버헤드를 발생시킵니다. 대부분의 경우 1-3% 미만이지만, 트랜잭션 트레이서 임계값이 낮거나 모든 SQL을 기록하면 오버헤드가 증가합니다. 프로덕션 환경에서는 적절한 샘플링과 임계값 설정으로 균형을 맞추세요.

🔗 관련 용어

📚 더 배우기