CampaignMetricsService β gRPC
Servicio de mΓ©tricas de campaΓ±as en tiempo real para DataFlow AI β arquitectura de microservicios
SerializaciΓ³n
10Γ
mΓ‘s pequeΓ±o que JSON
Parsing speed
5-10Γ
mΓ‘s rΓ‘pido que REST+JSON
Lenguajes
12+
generaciΓ³n de stubs
RPCs definidos
5
unary + streaming
βΉοΈ
Skill aplicada: gRPC: Servicios RPC de Alto Rendimiento β Define el contrato en .proto, genera stubs con protoc, implementa handlers con patrones de errores, deadlines e interceptores. Adecuado para el pipeline de eventos de DataFlow AI donde la latencia baja y el streaming son crΓticos.
Inventario de RPCs
Contrato de API| RPC | Tipo | Input β Output | DescripciΓ³n | Estado |
|---|---|---|---|---|
| RecordMetric | UNARY | CampaignMetric β RecordResponse |
Registra una mΓ©trica puntual desde un ad network | β Listo |
| GetCampaignSummary | UNARY | SummaryRequest β CampaignSummary |
Agrega KPIs consolidados de una campaΓ±a (ROAS, CTR) | β Listo |
| BatchIngestMetrics | CLIENT STREAM | stream CampaignMetric β IngestSummary |
Ingesta masiva desde conectores de Meta/Google/TikTok | β Listo |
| StreamAlerts | SERVER STREAM | AlertSubscription β stream Alert |
Push de alertas de anomalΓas al dashboard en tiempo real | β Listo |
| LiveMetricsChat | BIDI STREAM | stream MetricQuery β stream MetricResult |
Canal interactivo para drill-down y filtros en vivo | β‘ Beta |
DefiniciΓ³n Protocol Buffers
proto/campaign_metrics.protoproto/campaign_metrics.proto β protobuf
// DataFlow AI β CampaignMetricsService // Contrato gRPC para el pipeline de mΓ©tricas de campaΓ±as syntax = "proto3"; package dataflowai.metrics.v1; import "google/protobuf/timestamp.proto"; import "google/protobuf/empty.proto"; // βββ Servicio principal βββββββββββββββββββββββββββββββββββββββββββββββββββ service CampaignMetricsService { // Unary: registra una mΓ©trica puntual rpc RecordMetric(CampaignMetric) returns (RecordResponse); // Unary: devuelve el resumen consolidado de una campaΓ±a rpc GetCampaignSummary(SummaryRequest) returns (CampaignSummary); // Client streaming: ingesta masiva desde ad networks rpc BatchIngestMetrics(stream CampaignMetric) returns (IngestSummary); // Server streaming: push de alertas de anomalΓas al dashboard rpc StreamAlerts(AlertSubscription) returns (stream Alert); // Bidireccional: drill-down interactivo en vivo rpc LiveMetricsChat(stream MetricQuery) returns (stream MetricResult); } // βββ Mensajes principales βββββββββββββββββββββββββββββββββββββββββββββββββ message CampaignMetric { string campaign_id = 1; Channel channel = 2; // META | GOOGLE | TIKTOK int64 impressions = 3; int64 clicks = 4; double spend_eur = 5; int32 conversions = 6; google.protobuf.Timestamp timestamp = 7; } message CampaignSummary { string campaign_id = 1; string name = 2; double total_spend_eur = 3; double roas = 4; // return on ad spend double ctr_pct = 5; int32 conversions = 6; CampaignStatus status = 7; } message Alert { string campaign_id = 1; AlertSeverity severity = 2; string message = 3; string metric_name = 4; double value = 5; double threshold = 6; google.protobuf.Timestamp fired_at = 7; } message MetricQuery { string campaign_id = 1; Channel channel = 2; string metric = 3; } message MetricResult { string campaign_id = 1; string metric = 2; double value = 3; string unit = 4; } message SummaryRequest { string campaign_id = 1; } message RecordResponse { string metric_id = 1; bool accepted = 2; } message IngestSummary { int32 ingested_count = 1; int32 failed_count = 2; repeated string errors = 3; } message AlertSubscription { string workspace_id = 1; AlertSeverity min_severity = 2; } // βββ Enums ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ enum Channel { CHANNEL_UNSPECIFIED = 0; CHANNEL_META = 1; CHANNEL_GOOGLE = 2; CHANNEL_TIKTOK = 3; } enum AlertSeverity { SEVERITY_UNSPECIFIED = 0; SEVERITY_LOW = 1; SEVERITY_MEDIUM = 2; SEVERITY_HIGH = 3; SEVERITY_CRITICAL = 4; } enum CampaignStatus { STATUS_UNSPECIFIED = 0; STATUS_ACTIVE = 1; STATUS_PAUSED = 2; STATUS_COMPLETED = 3; }
ImplementaciΓ³n del servidor
server.js β Node.jsserver.js β JavaScript (Node.js Β· @grpc/grpc-js)
const grpc = require('@grpc/grpc-js'); const protoLoader = require('@grpc/proto-loader'); const crypto = require('crypto'); const reflection = require('@grpc/reflection'); // ββ Carga dinΓ‘mica del proto (sin compilar) βββββββββββββββββββββββββββββββ const pkgDef = protoLoader.loadSync('proto/campaign_metrics.proto', { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, }); const proto = grpc.loadPackageDefinition(pkgDef).dataflowai.metrics.v1; // ββ In-memory store (producciΓ³n β PostgreSQL + Redis) βββββββββββββββββββββ const metricsStore = new Map(); // campaign_id β [CampaignMetric] const alertListeners = new Map(); // workspace_id β Set(call) // ββ Interceptor de logging ββββββββββββββββββββββββββββββββββββββββββββββββ function loggingInterceptor(methodDef, next) { return function(call, callback) { const start = Date.now(); console.log(`β [${methodDef.path}] llamada iniciada`); next(call, (...args) => { console.log(`β [${methodDef.path}] ${Date.now() - start}ms`); callback(...args); }); }; } // ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ function computeSummary(campaignId) { const metrics = metricsStore.get(campaignId) || []; const totalSpend = metrics.reduce((s, m) => s + m.spend_eur, 0); const totalClicks = metrics.reduce((s, m) => s + m.clicks, 0); const totalImpr = metrics.reduce((s, m) => s + m.impressions, 0); const totalConv = metrics.reduce((s, m) => s + m.conversions, 0); const revenue = totalConv * 47.5; // AOV estimado return { campaign_id: campaignId, name: `CampaΓ±a ${campaignId.slice(0,8)}`, total_spend_eur: totalSpend, roas: totalSpend > 0 ? revenue / totalSpend : 0, ctr_pct: totalImpr > 0 ? (totalClicks / totalImpr) * 100 : 0, conversions: totalConv, status: 'STATUS_ACTIVE', }; } // ββ Handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ const service = { // UNARY β guardar una mΓ©trica RecordMetric(call, callback) { const { campaign_id } = call.request; if (!campaign_id) return callback({ code: grpc.status.INVALID_ARGUMENT, message: 'campaign_id es obligatorio' }); if (!metricsStore.has(campaign_id)) metricsStore.set(campaign_id, []); metricsStore.get(campaign_id).push(call.request); callback(null, { metric_id: crypto.randomUUID(), accepted: true }); }, // UNARY β resumen de campaΓ±a GetCampaignSummary(call, callback) { const { campaign_id } = call.request; if (!metricsStore.has(campaign_id)) return callback({ code: grpc.status.NOT_FOUND, message: `CampaΓ±a ${campaign_id} no encontrada` }); callback(null, computeSummary(campaign_id)); }, // CLIENT STREAMING β ingesta masiva BatchIngestMetrics(call, callback) { let ingested = 0; const errors = []; call.on('data', (metric) => { if (!metric.campaign_id) { errors.push('metric sin campaign_id omitida'); return; } if (!metricsStore.has(metric.campaign_id)) metricsStore.set(metric.campaign_id, []); metricsStore.get(metric.campaign_id).push(metric); ingested++; }); call.on('end', () => callback(null, { ingested_count: ingested, failed_count: errors.length, errors })); call.on('error', (e) => console.error('BatchIngestMetrics error:', e)); }, // SERVER STREAMING β push alertas al dashboard StreamAlerts(call) { const { workspace_id, min_severity } = call.request; if (!alertListeners.has(workspace_id)) alertListeners.set(workspace_id, new Set()); const listeners = alertListeners.get(workspace_id); listeners.add(call); // SimulaciΓ³n de detecciΓ³n de anomalΓa (producciΓ³n β ML anomaly detector) const timer = setInterval(() => { call.write({ campaign_id: 'camp_summer_2025', severity: 'SEVERITY_HIGH', message: 'CTR caΓdo 40% en TikTok β posible fatiga creativa', metric_name: 'ctr_pct', value: 0.8, threshold: 1.5, fired_at: { seconds: Math.floor(Date.now() / 1000) }, }); }, 10000); call.on('cancelled', () => { clearInterval(timer); listeners.delete(call); }); }, // BIDIRECCIONAL β drill-down interactivo LiveMetricsChat(call) { call.on('data', ({ campaign_id, metric }) => { const summary = computeSummary(campaign_id); const value = summary[metric] ?? 0; call.write({ campaign_id, metric, value, unit: unitOf(metric) }); }); call.on('end', () => call.end()); }, }; // ββ Arranque del servidor βββββββββββββββββββββββββββββββββββββββββββββββββ const server = new grpc.Server({ interceptors: [loggingInterceptor] }); server.addService(proto.CampaignMetricsService.service, service); reflection.addToServer(server); // habilita grpcurl en desarrollo server.bindAsync('0.0.0.0:50055', grpc.ServerCredentials.createInsecure(), (err, port) => { if (err) { console.error('Error arrancando servidor:', err); return; } console.log(`β CampaignMetricsService escuchando en 0.0.0.0:${port}`); });
Cliente Python β Pipeline de ingesta
ingest_pipeline.py β Pythoningest_pipeline.py β Python 3 (grpcio)
"""DataFlow AI β Pipeline de ingesta masiva de mΓ©tricas via gRPC client streaming.""" import grpc, csv, datetime from google.protobuf import timestamp_pb2 import campaign_metrics_pb2 as pb import campaign_metrics_pb2_grpc as stub CHANNEL_MAP = { 'META': pb.CHANNEL_META, 'GOOGLE': pb.CHANNEL_GOOGLE, 'TIKTOK': pb.CHANNEL_TIKTOK } def parse_csv_row(row): ts = timestamp_pb2.Timestamp() ts.FromDatetime(datetime.datetime.fromisoformat(row['timestamp'])) return pb.CampaignMetric( campaign_id = row['campaign_id'], channel = CHANNEL_MAP.get(row['channel'], pb.CHANNEL_UNSPECIFIED), impressions = int(row['impressions']), clicks = int(row['clicks']), spend_eur = float(row['spend_eur']), conversions = int(row['conversions']), timestamp = ts, ) def metric_generator(csv_path): with open(csv_path) as f: for row in csv.DictReader(f): yield parse_csv_row(row) def run_batch_ingest(csv_path: str): # Deadline de 60s para batch grandes with grpc.insecure_channel('localhost:50055') as channel: client = stub.CampaignMetricsServiceStub(channel) response = client.BatchIngestMetrics( metric_generator(csv_path), timeout=60, ) print(f"β Ingestadas: {response.ingested_count} | Fallidas: {response.failed_count}") if response.errors: print("Errores:", response.errors) if __name__ == '__main__': run_batch_ingest('data/meta_export_2025-06-14.csv') # Ejemplo CSV esperado: # campaign_id,channel,impressions,clicks,spend_eur,conversions,timestamp # camp_summer_2025,META,150000,2100,840.50,38,2025-06-14T08:00:00 # camp_summer_2025,TIKTOK,95000,760,310.00,12,2025-06-14T08:00:00 # camp_brandawareness,GOOGLE,200000,4500,1200.00,55,2025-06-14T09:00:00
Testing con grpcurl
CLI testing β sin cΓ³digotest_commands.sh β shell
# Listar servicios disponibles (reflection activa) grpcurl -plaintext localhost:50055 list # β dataflowai.metrics.v1.CampaignMetricsService # Inspeccionar RPC grpcurl -plaintext localhost:50055 describe dataflowai.metrics.v1.CampaignMetricsService # Registrar una mΓ©trica (unary) grpcurl -plaintext -d '{ "campaign_id": "camp_summer_2025", "channel": "CHANNEL_META", "impressions": 150000, "clicks": 2100, "spend_eur": 840.50, "conversions": 38 }' localhost:50055 dataflowai.metrics.v1.CampaignMetricsService/RecordMetric # β { "metric_id": "3f7a1c2e-...", "accepted": true } # Consultar resumen de campaΓ±a grpcurl -plaintext -d '{"campaign_id": "camp_summer_2025"}' \ localhost:50055 dataflowai.metrics.v1.CampaignMetricsService/GetCampaignSummary # β { "campaign_id": "camp_summer_2025", "total_spend_eur": 840.5, # "roas": 2.15, "ctr_pct": 1.40, "conversions": 38, "status": "STATUS_ACTIVE" } # Suscribirse a alertas en tiempo real (server streaming) grpcurl -plaintext -d '{ "workspace_id": "ws_dataflowai_001", "min_severity": "SEVERITY_MEDIUM" }' localhost:50055 dataflowai.metrics.v1.CampaignMetricsService/StreamAlerts
Rendimiento estimado vs REST/JSON
Payload size (batch 1k mΓ©tricas)
~18 KB
REST/JSON equivalente
~135 KB
Parse time gRPC/protobuf
0.4 ms
Parse time REST/JSON
2.8 ms
Latencia p99 unary RPC
1.2 ms
Latencia p99 REST
8.5 ms
DataFlow AI Β· CampaignMetricsService v1.0 Β· gRPC skill demo β CULTIVA IA
@grpc/grpc-js ^1.10
grpcio ^1.63
proto3