히스토리 조회 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_name | string | Y | 태그 이름 |
| time_range | TimeRange | Y | 조회 시간 범위 |
| max_points | int32 | N | 최대 포인트 수 (기본: 1000, 최대: 10000) |
TimeRange 구조
| 필드 | 타입 | 설명 |
|---|---|---|
| start | Timestamp | 시작 시각 |
| end | Timestamp | 종료 시각 |
응답
{
"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_name | string | Y | 태그 이름 |
| time_range | TimeRange | Y | 조회 시간 범위 |
| aggregation | AggregationType | Y | 집계 타입 |
| interval | Duration | N | 집계 간격 (예: "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 구조
| 필드 | 타입 | 설명 |
|---|---|---|
| timestamp | Timestamp | 데이터 포인트 시각 |
| value | TagValue | 태그 값 (다형성) |
| quality | string | 품질 ("Good", "Bad", "Uncertain") |
AggregatedPoint 구조
| 필드 | 타입 | 설명 |
|---|---|---|
| timestamp | Timestamp | 집계 간격의 시작 시각 |
| value | double | 집계 값 |
사용 예제
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_EXCEEDED | max_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 연결 실패
- 태그 이름 오타
해결:
GetTagList로 태그 존재 여부 확인ReadTag로 현재 값 확인 (실시간 데이터 수집 중인지)- InfluxDB 연결 상태 확인
응답 속도 느림
원인:
- 과도한 데이터 포인트 요청
- InfluxDB 부하
- 네트워크 지연
해결:
max_points값 줄이기- 집계 데이터 사용
- 조회 기간 단축
- 캐싱 적용
잘못된 시간 범위 (INVALID_TIME_RANGE)
원인:
- start가 end보다 큼
- 미래 시간 지정
- Timezone 혼동
해결:
- UTC 시간 사용 확인
- 시작/종료 시간 순서 확인
- Timestamp 생성 로직 검증
// ❌ 잘못된 예
var start = DateTime.UtcNow;
var end = DateTime.UtcNow.AddDays(-7); // end가 start보다 과거
// ✅ 올바른 예
var start = DateTime.UtcNow.AddDays(-7);
var end = DateTime.UtcNow;