콘텐츠로 이동

OpenLLMetry

OpenLLMetry는 Traceloop 팀이 Apache 2.0 라이선스로 개발하고 유지 관리하는 도구로, OpenTelemetry의 기능을 확장해 LLM 애플리케이션을 위한 전용 모니터링 및 디버깅 도구를 제공합니다. OpenTelemetry의 표준화된 텔레메트리 데이터 형식을 활용해 LLM 애플리케이션의 핵심 성능 지표와 추적 정보를 표준화된 형태로 출력합니다.

설정

OTEL을 사용해 추적 데이터를 DataKit으로 전송하기 전에 Collector가 설정되어 있는지 확인하고, 다음과 같이 구성 파일의 customer_tags = ["llm.request.type","traceloop.entity.path","llm.is_streaming","gen_ai.openai.api_base","gen_ai.prompt.1.content","gen_ai.response.model","gen_ai.completion.0.content","gen_ai.request.model","gen_ai.request.temperature","gen_ai.system","traceloop.workflow.name"]를 조정해야 합니다.

[[inputs.opentelemetry]]
  ## customer_tags는 데이터 센터로 태그가 전송되는 것을 방지하는 화이트리스트로 작동합니다.
  ## 모든 . 는 _ 로 대체됩니다. 예:
    customer_tags = ["llm.request.type","traceloop.entity.path","llm.is_streaming","gen_ai.openai.api_base","gen_ai.prompt.1.content","gen_ai.response.model","gen_ai.completion.0.content","gen_ai.request.model","gen_ai.request.temperature","gen_ai.system","traceloop.workflow.name"]
  ...

조정이 끝나면 DataKit을 재시작합니다

OpenTelemetry SDK 설치

pip install opentelemetry-api opentelemetry-instrumentation pip install opentelemetry-instrumentation-flask

OpenLLMetry SDK 설치

pip install traceloop-sdk

애플리케이션에서 OpenLLMetry 초기화

from traceloop.sdk import Traceloop

# OpenLit 초기화
# Traceloop.init()

Traceloop.init(app_name="kimi_openllmetry_stream_flask")

OpenLLMetry 예제 코드

import os
import httpx
from flask import Flask, request, Response,jsonify,stream_with_context
from traceloop.sdk import Traceloop
from traceloop.sdk.decorators import workflow,task
from openai import OpenAI

from opentelemetry.instrumentation.flask import FlaskInstrumentor


app = Flask(__name__)
# FlaskInstrumentor로 Flask 애플리케이션 자동 계측
FlaskInstrumentor().instrument_app(app)

# OpenLit 초기화
Traceloop.init(app_name="kimi_openllmetry_stream_flask")

# 환경 변수에서 API Key 가져오기
api_key = os.getenv("MOONSHOT_API_KEY")
if not api_key:
    raise ValueError("MOONSHOT_API_KEY 환경 변수를 설정하세요")

client = OpenAI(
    api_key=api_key,
    base_url="https://api.moonshot.cn/v1",
)

def estimate_token_count(input_messages) -> int:
    """
    입력 메시지의 토큰 수를 계산합니다.
    """
    try:
        header = {
            "Authorization": f"Bearer {api_key}",
        }
        data = {
            "model": "moonshot-v1-128k",
            "messages": input_messages,
        }
        with httpx.Client() as client:
            print("API 요청")
            r = client.post("https://api.moonshot.cn/v1/tokenizers/estimate-token-count", headers=header, json=data)
            r.raise_for_status()
            response_data = r.json()
            print(response_data["data"]["total_tokens"])
            return response_data["data"]["total_tokens"]
    except httpx.RequestError as e:
        print(f"요청 실패: {e}")
        raise
    except (KeyError, ValueError) as e:
        print(f"응답 파싱 실패: {e}")
        raise

def select_model(input_messages, max_tokens=1024) -> str:
    """
    입력 컨텍스트 메시지와 예상 max_tokens 값을 기준으로 적절한 모델을 선택합니다.
    """
    if not isinstance(max_tokens, int) or max_tokens <= 0:
        raise ValueError("max_tokens는 양의 정수여야 합니다")

    prompt_tokens = estimate_token_count(input_messages)
    total_tokens = prompt_tokens + max_tokens

    if total_tokens <= 8 * 1024:
        return "moonshot-v1-8k"
    elif total_tokens <= 32 * 1024:
        return "moonshot-v1-32k"
    elif total_tokens <= 128 * 1024:
        return "moonshot-v1-128k"
    else:
        raise ValueError("토큰 수가 제한을 초과했습니다 😢")

@app.route('/ask', methods=['POST'])
@workflow(name="ask_workflow")
def ask():
    data = request.json
    messages = data.get('messages')
    max_tokens = data.get('max_tokens', 2048)

    if not messages:
        return jsonify({"error": "messages 필드는 비워둘 수 없습니다"}), 400

    try:
        model = select_model(messages, max_tokens)

        completion = client.chat.completions.create(
            model=model,
            messages=messages,
            max_tokens=max_tokens,
            temperature=0.3,
            stream=True  # 스트리밍 생성을 활성화
        )

        def generate():
            for chunk in completion:
                # chunk.choices[0].delta.content 또는 ''를 반환
                delta = chunk.choices[0].delta
                if delta.content:
                    print(delta.content, end="")
                    yield delta.content or ''

        return Response(stream_with_context(generate()), content_type='text/event-stream')
    except Exception as e:
        return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
    app.run(debug=True,port=5001)

env를 설정하여 데이터를 OpenTelemetry를 통해 Datakit으로 전송합니다

export TRACELOOP_BASE_URL=http://localhost:9529/otel

지표 설명

지표 이름 설명 단위
gen_ai.client.generation.choices 클라이언트가 생성한 선택 항목 수
gen_ai.client.operation.duration_bucket 클라이언트 작업 지속 시간의 히스토그램 버킷 밀리초
gen_ai.client.operation.duration_count 클라이언트 작업의 총 횟수
gen_ai.client.operation.duration_max 클라이언트 작업의 최대 지속 시간 밀리초
gen_ai.client.operation.duration_min 클라이언트 작업의 최소 지속 시간 밀리초
gen_ai.client.operation.duration_sum 클라이언트 작업의 총 지속 시간 밀리초
llm.openai.chat_completions.streaming_time_to_first_token_bucket OpenAI 채팅 완성 기능의 스트리밍 중 첫 토큰 생성 시간 히스토그램 버킷 밀리초
llm.openai.chat_completions.streaming_time_to_first_token_count OpenAI 채팅 완성 기능의 스트리밍 중 첫 토큰 생성 총 횟수
llm.openai.chat_completions.streaming_time_to_first_token_max OpenAI 채팅 완성 기능의 스트리밍 중 첫 토큰 생성 최대 시간 밀리초
llm.openai.chat_completions.streaming_time_to_first_token_min OpenAI 채팅 완성 기능의 스트리밍 중 첫 토큰 생성 최소 시간 밀리초
llm.openai.chat_completions.streaming_time_to_first_token_sum OpenAI 채팅 완성 기능의 스트리밍 중 첫 토큰 생성 총 시간 밀리초
llm.openai.chat_completions.streaming_time_to_generate_bucket OpenAI 채팅 완성 기능의 스트리밍 중 생성 내용 총 시간 히스토그램 버킷 밀리초
llm.openai.chat_completions.streaming_time_to_generate_count OpenAI 채팅 완성 기능의 스트리밍 중 생성 내용 총 횟수
llm.openai.chat_completions.streaming_time_to_generate_max OpenAI 채팅 완성 기능의 스트리밍 중 생성 내용 최대 시간 밀리초
llm.openai.chat_completions.streaming_time_to_generate_min OpenAI 채팅 완성 기능의 스트리밍 중 생성 내용 최소 시간 밀리초
llm.openai.chat_completions.streaming_time_to_generate_sum OpenAI 채팅 완성 기능의 스트리밍 중 생성 내용 총 시간 밀리초

참고 자료

문서 평가

이 페이지가 도움이 되었나요?