본문으로 건너뛰기

히스토리 조회 API

개요

히스토리 조회 API는 태그의 시계열 데이터를 조회하고, 특정 기간의 집계 데이터(평균, 최소, 최대 등)를 제공합니다. InfluxDB에 저장된 OT 데이터를 분석하는 데 사용됩니다.

기본 정보

항목
서비스CaffeineApi
엔드포인트Engine gRPC 포트 (기본: 5001)
인증JWT 토큰 필요
버전v1
데이터 소스InfluxDB

엔드포인트 요약

메서드경로설명
GET/api/v1/tags/{tag_name}/history태그 히스토리 조회
GET/api/v1/tags/{tag_name}/aggregated집계 데이터 조회

상세 엔드포인트

GetTagHistory

태그의 시계열 히스토리 데이터를 조회합니다.

gRPC 메서드

rpc GetTagHistory (GetTagHistoryRequest) returns (GetTagHistoryResponse)

요청

파라미터타입필수설명
tag_namestringY태그 이름
time_rangeTimeRangeY조회 시간 범위
max_pointsint32N최대 포인트 수 (기본: 1000, 최대: 10000)

TimeRange 구조

필드타입설명
startTimestamp시작 시각
endTimestamp종료 시각

응답

{
"points": [
{
"timestamp": "2026-02-17T10:00:00Z",
"value": {
"doubleValue": 123.45
},
"quality": "Good"
},
{
"timestamp": "2026-02-17T10:01:00Z",
"value": {
"doubleValue": 125.67
},
"quality": "Good"
}
]
}

응답 코드

코드설명
OK성공
NOT_FOUND태그를 찾을 수 없음
INVALID_ARGUMENT잘못된 시간 범위 또는 max_points
UNAUTHENTICATED인증 필요

REST 경로

GET /api/v1/tags/{tag_name}/history?start=2026-02-17T00:00:00Z&end=2026-02-17T23:59:59Z&maxPoints=5000

GetAggregatedData

태그의 집계 데이터를 조회합니다.

gRPC 메서드

rpc GetAggregatedData (GetAggregatedDataRequest) returns (GetAggregatedDataResponse)

요청

파라미터타입필수설명
tag_namestringY태그 이름
time_rangeTimeRangeY조회 시간 범위
aggregationAggregationTypeY집계 타입
intervalDurationN집계 간격 (예: "1h", "5m")

AggregationType 열거형

설명
NONE집계 없음
AVG평균
MIN최소값
MAX최대값
SUM합계
COUNT개수

Duration 예시

설명
"60s"60초 (1분)
"5m"5분
"1h"1시간
"24h"24시간 (1일)

응답

{
"points": [
{
"timestamp": "2026-02-17T10:00:00Z",
"value": 125.5
},
{
"timestamp": "2026-02-17T11:00:00Z",
"value": 130.2
}
]
}

응답 코드

코드설명
OK성공
NOT_FOUND태그를 찾을 수 없음
INVALID_ARGUMENT잘못된 파라미터
UNAUTHENTICATED인증 필요

REST 경로

GET /api/v1/tags/{tag_name}/aggregated?start=2026-02-17T00:00:00Z&end=2026-02-17T23:59:59Z&aggregation=AVG&interval=1h

데이터 타입

HistoryPoint 구조

필드타입설명
timestampTimestamp데이터 포인트 시각
valueTagValue태그 값 (다형성)
qualitystring품질 ("Good", "Bad", "Uncertain")

AggregatedPoint 구조

필드타입설명
timestampTimestamp집계 간격의 시작 시각
valuedouble집계 값

사용 예제

C# (CaffeineClient)

using Caffeine.Client;
using Google.Protobuf.WellKnownTypes;

var client = new CaffeineClient();
await client.ConnectAsync("https://localhost:5001");

// 태그 히스토리 조회 (최근 24시간)
var historyResponse = await client.GetTagHistoryAsync(
tagName: "PLC01.D100",
startTime: DateTime.UtcNow.AddDays(-1),
endTime: DateTime.UtcNow,
maxPoints: 5000
);

Console.WriteLine($"Total Points: {historyResponse.Points.Count}");
foreach (var point in historyResponse.Points.Take(10))
{
Console.WriteLine($"{point.Timestamp}: {point.Value} ({point.Quality})");
}

// 집계 데이터 조회 (시간별 평균, 최근 7일)
var aggregatedResponse = await client.GetAggregatedDataAsync(
tagName: "PLC01.D100",
startTime: DateTime.UtcNow.AddDays(-7),
endTime: DateTime.UtcNow,
aggregation: AggregationType.Avg,
interval: Duration.FromTimeSpan(TimeSpan.FromHours(1))
);

Console.WriteLine($"Total Points: {aggregatedResponse.Points.Count}");
foreach (var point in aggregatedResponse.Points)
{
Console.WriteLine($"{point.Timestamp}: {point.Value:F2}");
}

await client.DisposeAsync();

C# (직접 gRPC 호출)

using Grpc.Net.Client;
using Caffeine.IPC.Grpc;
using Google.Protobuf.WellKnownTypes;

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var grpcClient = new CaffeineApi.CaffeineApiClient(channel);

// 태그 히스토리 조회
var historyResponse = await grpcClient.GetTagHistoryAsync(new GetTagHistoryRequest
{
TagName = "PLC01.D100",
TimeRange = new TimeRange
{
Start = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-1)),
End = Timestamp.FromDateTime(DateTime.UtcNow)
},
MaxPoints = 5000
});

foreach (var point in historyResponse.Points)
{
Console.WriteLine($"{point.Timestamp.ToDateTime()}: {point.Value.DoubleValue}");
}

// 집계 데이터 조회 (일별 최대값)
var aggregatedResponse = await grpcClient.GetAggregatedDataAsync(new GetAggregatedDataRequest
{
TagName = "PLC01.D100",
TimeRange = new TimeRange
{
Start = Timestamp.FromDateTime(DateTime.UtcNow.AddMonths(-1)),
End = Timestamp.FromDateTime(DateTime.UtcNow)
},
Aggregation = AggregationType.Max,
Interval = Duration.FromTimeSpan(TimeSpan.FromDays(1))
});

foreach (var point in aggregatedResponse.Points)
{
Console.WriteLine($"{point.Timestamp.ToDateTime():yyyy-MM-dd}: {point.Value:F2}");
}

curl (REST API)

태그 히스토리 조회

curl -X GET "https://localhost:5001/api/v1/tags/PLC01.D100/history?start=2026-02-17T00:00:00Z&end=2026-02-17T23:59:59Z&maxPoints=5000" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

응답

{
"points": [
{
"timestamp": "2026-02-17T00:00:00.000Z",
"value": {
"doubleValue": 123.45
},
"quality": "Good"
},
{
"timestamp": "2026-02-17T00:01:00.000Z",
"value": {
"doubleValue": 124.12
},
"quality": "Good"
}
]
}

집계 데이터 조회 (시간별 평균)

curl -X GET "https://localhost:5001/api/v1/tags/PLC01.D100/aggregated?start=2026-02-17T00:00:00Z&end=2026-02-17T23:59:59Z&aggregation=AVG&interval=1h" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

응답

{
"points": [
{
"timestamp": "2026-02-17T00:00:00.000Z",
"value": 125.5
},
{
"timestamp": "2026-02-17T01:00:00.000Z",
"value": 127.3
}
]
}

집계 데이터 조회 (일별 최대값)

curl -X GET "https://localhost:5001/api/v1/tags/PLC01.D100/aggregated?start=2026-02-01T00:00:00Z&end=2026-02-28T23:59:59Z&aggregation=MAX&interval=24h" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

에러 코드

코드설명HTTP 상태
TAG_NOT_FOUND태그를 찾을 수 없음404
INVALID_TIME_RANGE잘못된 시간 범위 (start > end)400
MAX_POINTS_EXCEEDEDmax_points가 10000 초과400
INVALID_INTERVAL잘못된 집계 간격400
NO_DATA조회 기간에 데이터 없음404

성능 최적화

적절한 max_points 설정

데이터 포인트 수를 제한하여 성능을 최적화합니다.

// ❌ 비효율적: 과도한 포인트 요청
var history = await client.GetTagHistoryAsync(
tagName: "PLC01.D100",
startTime: DateTime.UtcNow.AddYears(-1),
endTime: DateTime.UtcNow,
maxPoints: 10000 // 1년치 데이터를 10000 포인트로 샘플링
);

// ✅ 효율적: 집계 데이터 사용
var aggregated = await client.GetAggregatedDataAsync(
tagName: "PLC01.D100",
startTime: DateTime.UtcNow.AddYears(-1),
endTime: DateTime.UtcNow,
aggregation: AggregationType.Avg,
interval: Duration.FromTimeSpan(TimeSpan.FromDays(1)) // 일별 평균
);

집계 데이터 활용

장기간 데이터는 집계 데이터를 사용하여 성능을 향상시킵니다.

// 대시보드 차트용 데이터
var chartData = await client.GetAggregatedDataAsync(
tagName: "PLC01.D100",
startTime: DateTime.UtcNow.AddDays(-30),
endTime: DateTime.UtcNow,
aggregation: AggregationType.Avg,
interval: Duration.FromTimeSpan(TimeSpan.FromHours(6)) // 6시간 간격
);

캐싱 전략

자주 조회되는 히스토리 데이터를 캐싱합니다.

using Microsoft.Extensions.Caching.Memory;

private readonly IMemoryCache _cache;

public async Task<GetTagHistoryResponse> GetHistoryWithCacheAsync(
string tagName,
DateTime startTime,
DateTime endTime)
{
var cacheKey = $"history_{tagName}_{startTime:yyyyMMddHHmm}_{endTime:yyyyMMddHHmm}";

if (_cache.TryGetValue(cacheKey, out GetTagHistoryResponse cachedResponse))
{
return cachedResponse;
}

var response = await client.GetTagHistoryAsync(tagName, startTime, endTime);
_cache.Set(cacheKey, response, TimeSpan.FromMinutes(10));
return response;
}

실전 활용 예제

온도 추세 분석

// 최근 7일 온도 추세 (시간별 평균)
var temperatureData = await client.GetAggregatedDataAsync(
tagName: "PLC01.Temperature",
startTime: DateTime.UtcNow.AddDays(-7),
endTime: DateTime.UtcNow,
aggregation: AggregationType.Avg,
interval: Duration.FromTimeSpan(TimeSpan.FromHours(1))
);

// 최고 온도와 최저 온도 찾기
var maxTemp = temperatureData.Points.Max(p => p.Value);
var minTemp = temperatureData.Points.Min(p => p.Value);
var avgTemp = temperatureData.Points.Average(p => p.Value);

Console.WriteLine($"Max: {maxTemp:F2}°C, Min: {minTemp:F2}°C, Avg: {avgTemp:F2}°C");

일일 생산량 집계

// 최근 30일 일일 생산량 합계
var productionData = await client.GetAggregatedDataAsync(
tagName: "PLC01.ProductionCount",
startTime: DateTime.UtcNow.AddDays(-30),
endTime: DateTime.UtcNow,
aggregation: AggregationType.Sum,
interval: Duration.FromTimeSpan(TimeSpan.FromDays(1))
);

var totalProduction = productionData.Points.Sum(p => p.Value);
var avgDailyProduction = productionData.Points.Average(p => p.Value);

Console.WriteLine($"Total: {totalProduction:N0}, Daily Avg: {avgDailyProduction:N0}");

이상치 탐지

// 최근 1시간 데이터로 이상치 탐지
var recentData = await client.GetTagHistoryAsync(
tagName: "PLC01.Vibration",
startTime: DateTime.UtcNow.AddHours(-1),
endTime: DateTime.UtcNow,
maxPoints: 3600 // 1초당 1포인트
);

var values = recentData.Points.Select(p => p.Value.DoubleValue).ToList();
var mean = values.Average();
var stdDev = Math.Sqrt(values.Average(v => Math.Pow(v - mean, 2)));

// 3σ 기준 이상치
var outliers = recentData.Points
.Where(p => Math.Abs(p.Value.DoubleValue - mean) > 3 * stdDev)
.ToList();

Console.WriteLine($"Outliers detected: {outliers.Count}");
foreach (var outlier in outliers)
{
Console.WriteLine($" {outlier.Timestamp}: {outlier.Value.DoubleValue:F2}");
}

대시보드 차트 데이터

public async Task<ChartData> GetDashboardChartDataAsync(string tagName, TimeSpan period)
{
AggregationType aggregation;
Duration interval;

// 기간에 따라 자동 집계 간격 조정
if (period <= TimeSpan.FromHours(24))
{
// 1일 이하: 1분 간격
aggregation = AggregationType.Avg;
interval = Duration.FromTimeSpan(TimeSpan.FromMinutes(1));
}
else if (period <= TimeSpan.FromDays(7))
{
// 7일 이하: 10분 간격
aggregation = AggregationType.Avg;
interval = Duration.FromTimeSpan(TimeSpan.FromMinutes(10));
}
else if (period <= TimeSpan.FromDays(30))
{
// 30일 이하: 1시간 간격
aggregation = AggregationType.Avg;
interval = Duration.FromTimeSpan(TimeSpan.FromHours(1));
}
else
{
// 30일 초과: 1일 간격
aggregation = AggregationType.Avg;
interval = Duration.FromTimeSpan(TimeSpan.FromDays(1));
}

var data = await client.GetAggregatedDataAsync(
tagName,
DateTime.UtcNow - period,
DateTime.UtcNow,
aggregation,
interval
);

return new ChartData
{
Labels = data.Points.Select(p => p.Timestamp.ToDateTime()).ToArray(),
Values = data.Points.Select(p => p.Value).ToArray()
};
}

문제 해결

데이터가 없음 (NO_DATA)

원인:

  • 조회 기간에 실제 데이터가 없음
  • InfluxDB 연결 실패
  • 태그 이름 오타

해결:

  1. GetTagList로 태그 존재 여부 확인
  2. ReadTag로 현재 값 확인 (실시간 데이터 수집 중인지)
  3. InfluxDB 연결 상태 확인

응답 속도 느림

원인:

  • 과도한 데이터 포인트 요청
  • InfluxDB 부하
  • 네트워크 지연

해결:

  1. max_points 값 줄이기
  2. 집계 데이터 사용
  3. 조회 기간 단축
  4. 캐싱 적용

잘못된 시간 범위 (INVALID_TIME_RANGE)

원인:

  • start가 end보다 큼
  • 미래 시간 지정
  • Timezone 혼동

해결:

  1. UTC 시간 사용 확인
  2. 시작/종료 시간 순서 확인
  3. Timestamp 생성 로직 검증
// ❌ 잘못된 예
var start = DateTime.UtcNow;
var end = DateTime.UtcNow.AddDays(-7); // end가 start보다 과거

// ✅ 올바른 예
var start = DateTime.UtcNow.AddDays(-7);
var end = DateTime.UtcNow;

관련 문서