| name | Logging and Monitoring for Agentic Workflows |
| description | Comprehensive observability patterns for GitHub Agentic Workflows including structured logging, metrics collection, alerting strategies, debugging techniques, and production monitoring best practices for autonomous agent systems. |
| license | Apache-2.0 |
| version | 2.0.0 |
| last_updated | 2026-04-02 |
| tags | ["logging","monitoring","observability","agentic-workflows","metrics","alerting","debugging","structured-logging","telemetry","tracing"] |
📊 Logging and Monitoring for Agentic Workflows
🔴 AI FIRST Quality Principle
Apply the AI FIRST principle: never accept first-pass quality. Minimum 2 iterations. Read all output, improve every section. No shortcuts.
📋 Overview
This skill provides comprehensive patterns for implementing observability in GitHub Agentic Workflows. It covers structured logging architectures, metrics collection, alerting strategies, debugging techniques, and production monitoring best practices for autonomous agent systems.
🎯 Core Concepts
Observability Architecture
graph TB
subgraph "Agent Execution"
A[Agent Start] --> B[Task Execution]
B --> C[MCP Operations]
C --> D[Agent Completion]
end
subgraph "Logging Layer"
B --> E[Structured Logs]
C --> E
D --> E
E --> F[Log Aggregation]
end
subgraph "Metrics Layer"
B --> G[Performance Metrics]
C --> G
D --> G
G --> H[Time Series DB]
end
subgraph "Alerting Layer"
F --> I[Alert Rules]
H --> I
I --> J[Notifications]
end
subgraph "Visualization"
F --> K[Log Explorer]
H --> L[Dashboards]
K --> M[Insights]
L --> M
end
style E fill:#00d9ff
style G fill:#ff006e
style I fill:#ffbe0b
Three Pillars of Observability
- Logs: Detailed event records with context
- Metrics: Quantitative measurements over time
- Traces: Request flow through system components
📝 Structured Logging
1. Logging Architecture
JSON Structured Logging Format
import winston from 'winston';
import { v4 as uuidv4 } from 'uuid';
class AgentLogger {
constructor(options = {}) {
this.agentId = options.agentId || process.env.AGENT_ID || uuidv4();
this.sessionId = options.sessionId || uuidv4();
this.environment = process.env.NODE_ENV || 'development';
this.logger = winston.createLogger({
level: options.level || process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'ISO' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
: {
: .,
: .,
: .,
: ,
: process.. ||
},
: [
winston..({
: winston..(
winston..(),
winston..(..())
)
}),
winston..({
: ,
: ,
: ,
: ,
:
}),
winston..({
: ,
: ,
: ,
:
})
],
:
});
}
() {
{ timestamp, level, message, ...meta } = info;
metaStr = .(meta). ? .(meta, , ) : ;
;
}
() {
..(, {
: ,
: taskType,
...context
});
}
() {
..(, {
: ,
: taskType,
: result.,
: metrics.,
: metrics.,
...result
});
}
() {
..(, {
: ,
: taskType,
: error.,
: error.,
: error.,
...context
});
}
() {
..(, {
: ,
: toolName,
: .(params),
...context
});
}
() {
..(, {
: ,
: toolName,
: duration,
: .(response).,
:
});
}
() {
..(, {
: ,
: toolName,
: duration,
: error.,
: error.
});
}
() {
..(, {
: ,
operation,
: metrics.,
: metrics.,
: metrics.
});
}
() {
..(, {
: ,
: eventType,
...details
});
}
() {
sensitive = [, , , , ];
sanitized = { ...params };
( key .(sanitized)) {
(sensitive.( key.().(s))) {
sanitized[key] = ;
}
}
sanitized;
}
() {
childLogger = .();
childLogger. = ..(metadata);
childLogger;
}
}
;
logger = ({
: ,
: process..
});
logger.(, {
: ,
:
});
Python Structured Logging
import logging
import json
import sys
import os
from datetime import datetime
from typing import Any, Dict, Optional
import uuid
class JSONFormatter(logging.Formatter):
"""
JSON formatter for structured logging
"""
def __init__(self):
super().__init__()
self.agent_id = os.getenv('AGENT_ID', str(uuid.uuid4()))
self.session_id = os.getenv('GITHUB_RUN_ID', str(uuid.uuid4()))
self.environment = os.getenv('ENVIRONMENT', 'development')
def format(self, record: logging.LogRecord) -> str:
"""Format log record as JSON"""
log_data = {
'timestamp': datetime.utcnow().isoformat() + 'Z',
'level': record.levelname,
'message': record.getMessage(),
'agent_id': self.agent_id,
'session_id': self.session_id,
'environment': self.environment,
'service': ,
: record.name,
: record.module,
: record.funcName,
: record.lineno
}
record.exc_info:
log_data[] = {
: record.exc_info[].__name__,
: (record.exc_info[]),
: .formatException(record.exc_info)
}
(record, ):
log_data.update(record.extra_fields)
json.dumps(log_data)
:
():
.logger = logging.getLogger(name)
.logger.setLevel((logging, level.upper()))
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(JSONFormatter())
.logger.addHandler(console_handler)
os.path.exists():
os.makedirs()
error_handler = logging.FileHandler()
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(JSONFormatter())
.logger.addHandler(error_handler)
():
extra = {
: {
: ,
: task_type,
**(context {})
}
}
.logger.info(, extra=extra)
():
extra = {
: {
: ,
: task_type,
: result.get(),
: metrics.get() metrics ,
**result
}
}
.logger.info(, extra=extra)
():
extra = {
: {
: ,
: task_type,
: (error).__name__,
: (error),
**(context {})
}
}
.logger.error(, extra=extra, exc_info=error)
():
extra = {
: {
: ,
: tool_name,
: ._sanitize_params(params),
**(context {})
}
}
.logger.debug(, extra=extra)
() -> [, ]:
sensitive = [, , , , ]
sanitized = params.copy()
key sanitized.keys():
(s key.lower() s sensitive):
sanitized[key] =
sanitized
logger = AgentLogger(, level=)
logger.log_agent_start(, {: })
2. Log Levels and When to Use Them
Log Level Guidelines
logger.error('Failed to connect to MCP server', {
error: error.message,
mcp_server: 'github',
retry_count: 3
});
logger.warn('MCP response slow', {
duration_ms: 5000,
threshold_ms: 3000,
tool_name: 'github-search'
});
logger.info('PR analysis completed', {
pr_number: 123,
issues_found: 5,
duration_ms: 15000
});
logger.debug('Processing file', {
file_path: 'src/index.js',
file_size: 1024,
line_count: 50
});
logger.trace('Token consumed', {
token_count: 1000,
model: 'claude-3-5-sonnet',
prompt_type: 'analysis'
});
3. Log Aggregation Patterns
GitHub Actions Log Groups
steps:
- name: Run Agent Analysis
run: |
echo "::group::Agent Initialization"
node scripts/agents/pr-analyzer.js --phase=init
echo "::endgroup::"
echo "::group::MCP Server Connection"
node scripts/agents/pr-analyzer.js --phase=connect
echo "::endgroup::"
echo "::group::Analysis Execution"
node scripts/agents/pr-analyzer.js --phase=analyze
echo "::endgroup::"
echo "::group::Report Generation"
node scripts/agents/pr-analyzer.js --phase=report
echo "::endgroup::"
Centralized Log Collection
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { gunzipSync, gzipSync } from 'zlib';
class LogCollector {
constructor(options = {}) {
this.s3Client = new S3Client({
region: options.region || 'us-east-1'
});
this.bucket = options.bucket || 'agentic-workflow-logs';
this.compression = options.compression !== false;
}
async uploadLogs(logFile, metadata = {}) {
const content = await fs.readFile(logFile, 'utf8');
const logs = content.split('\n').filter(Boolean).map(JSON.parse);
const key = this.generateLogKey(metadata);
const body = . ? (.(logs)) : .(logs);
..( ({
: .,
: key,
: body,
: ,
: . ? : ,
: {
: metadata.,
: metadata.,
: ().()
}
}));
.();
}
() {
date = ();
year = date.();
month = (date.() + ).(, );
day = (date.()).(, );
[
,
,
,
,
,
].();
}
() {
params = {
: .,
: filters.,
: ,
: .(filters),
: {
: { : },
: . ? :
},
: {
: { : }
}
};
}
}
;
📈 Metrics Collection
1. Performance Metrics
Agent Execution Metrics
import { performance } from 'perf_hooks';
import os from 'os';
class MetricsCollector {
constructor() {
this.metrics = new Map();
this.startTime = performance.now();
}
recordDuration(operation, duration) {
this.recordMetric('duration_ms', operation, duration, 'histogram');
}
recordCounter(name, value = 1, labels = {}) {
this.recordMetric(name, JSON.stringify(labels), value, 'counter');
}
recordGauge(name, value, labels = {}) {
this.recordMetric(name, JSON.stringify(labels), value, 'gauge');
}
recordMetric(name, key, value, type) {
metricKey = ;
(!..(metricKey)) {
..(metricKey, {
name,
type,
: [],
: key !== ? .(key) : {}
});
}
..(metricKey)..({
value,
: .()
});
}
() {
used = process.();
cpus = os.();
{
: {
: .(used. / / ),
: .(used. / / ),
: .(used. / / ),
: .(used. / / )
},
: {
: cpus.,
: cpus[].,
: os.()
},
: .(process.())
};
}
() {
duration = performance.() - .;
{
: .(duration),
: .(),
: .(),
: .()
};
}
() {
lines = [];
( [key, metric] ..()) {
{ name, type, values, labels } = metric;
lines.();
lines.();
labelStr = .(labels)
.( )
.();
(type === ) {
sorted = values.( v.).( a - b);
lines.();
lines.();
lines.();
lines.();
lines.();
lines.();
} (type === ) {
sum = values.( acc + v., );
lines.();
} (type === ) {
latest = values[values. - ];
lines.();
}
}
lines.();
}
() {
metrics = {
: ().(),
: .(),
: .(),
: {}
};
( [key, metric] ..()) {
metrics.[metric.] = {
: metric.,
: metric.,
: metric.
};
}
metrics;
}
}
;
metrics = ();
start = performance.();
();
metrics.(, performance.() - start);
metrics.(, , { : });
metrics.(, );
.(metrics.());
2. Custom Metrics for Agents
class AgentMetrics extends MetricsCollector {
recordLLMCall(model, tokens, duration) {
this.recordCounter('llm_calls_total', 1, { model });
this.recordCounter('llm_tokens_total', tokens, { model });
this.recordDuration('llm_duration', duration);
}
recordMCPTool(toolName, duration, success) {
this.recordCounter('mcp_calls_total', 1, { tool: toolName, success: success.toString() });
this.recordDuration('mcp_duration', duration);
}
recordDecision(decisionType, confidence) {
this.recordCounter('agent_decisions_total', 1, { type: decisionType });
this.recordGauge('agent_confidence', confidence, { type: decisionType });
}
recordCodeChanges() {
.(, filesModified);
.(, linesAdded);
.(, linesRemoved);
}
() {
costs = {
: ,
: ,
:
};
costPerToken = costs[model] || ;
cost = (tokens / ) * costPerToken;
.(, cost, { model });
cost;
}
}
3. Metrics Visualization
GitHub Actions Job Summary
async function generateMetricsSummary(metrics) {
const agentMetrics = metrics.getAgentMetrics();
const systemMetrics = metrics.getSystemMetrics();
await core.summary
.addHeading('📊 Agent Execution Metrics')
.addTable([
[{data: 'Metric', header: true}, {data: 'Value', header: true}],
['Total Duration', `${agentMetrics.total_duration_ms}ms`],
['Operations', agentMetrics.operations_count.toString()],
['Errors', agentMetrics.errors_count.toString()],
['Success Rate', `${agentMetrics.success_rate}%`]
])
.addHeading('💻 System Metrics', 3)
.addTable([
[{data: 'Resource', header: true}, {data: 'Usage', header: true}],
['Memory (Heap)', `${systemMetrics.memory.heap_used_mb}MB / MB`],
[, ],
[, systemMetrics...()],
[, ]
])
.();
}
🚨 Alerting Strategies
1. Alert Rules Configuration
name: Agent Monitoring & Alerts
on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
check-metrics:
name: Check Agent Metrics
runs-on: ubuntu-latest
steps:
- name: Fetch Recent Workflow Runs
id: fetch-runs
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
status: 'completed',
per_page: 50
});
// Filter agent workflows
const
{
}
{
,
,
,
,
}
{
{
,
,
{} ,
,
}
}
{
{
,
,
{} ,
,
}
}
{
{
,
,
{} ,
,
}
}
{
{ } {
,
,
,
,
}
{}
{
{
,
,
{},
{}
{}
{}
{}
{ },
[, , {}]
}
}
}
2. Real-Time Alerting
import { WebClient } from '@slack/web-api';
import nodemailer from 'nodemailer';
class AlertManager {
constructor(options = {}) {
this.slackClient = options.slackWebhook ? new WebClient(options.slackToken) : null;
this.slackChannel = options.slackChannel;
this.emailTransporter = options.emailConfig ? nodemailer.createTransporter(options.emailConfig) : null;
this.thresholds = options.thresholds || this.getDefaultThresholds();
}
getDefaultThresholds() {
return {
duration: {
warning: 300000,
critical: 600000
},
errorRate: {
warning: 0.05,
critical:
},
: {
: ,
:
}
};
}
() {
threshold = .[metric];
(!threshold) ;
(value >= threshold.) {
{ : , : threshold. };
} (value >= threshold.) {
{ : , : threshold. };
}
;
}
() {
promises = [];
(.) {
promises.(.(alert));
}
(.) {
promises.(.(alert));
}
.(promises);
}
() {
emoji = alert. === ? : ;
color = alert. === ? : ;
...({
: .,
: ,
: [
{
: ,
: {
: ,
:
}
},
{
: ,
: [
{
: ,
:
},
{
: ,
:
},
{
: ,
:
},
{
: ,
:
}
]
},
{
: ,
: {
: ,
: alert.
}
},
{
: ,
: [
{
: ,
:
}
]
}
],
: [
{
: color,
:
}
]
});
}
() {
..({
: ,
: ,
: ,
:
});
}
}
;
🐛 Debugging Techniques
1. Debug Mode
process.env.DEBUG = 'agent:*,mcp:*';
process.env.LOG_LEVEL = 'debug';
import createDebug from 'debug';
const debug = createDebug('agent:pr-analyzer');
const debugMCP = createDebug('mcp:github');
debug('Starting PR analysis for #%d', prNumber);
debugMCP('Calling tool %s with params %o', toolName, params);
2. Interactive Debugging
steps:
- name: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
timeout-minutes: 30
3. Trace Analysis
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
class AgentTracer {
constructor(serviceName = 'agentic-workflow') {
this.tracer = trace.getTracer(serviceName);
}
async traceOperation(name, fn, attributes = {}) {
const span = this.tracer.startSpan(name, {
attributes: {
'agent.service': 'agentic-workflow',
...attributes
}
});
try {
const result = await context.with(
trace.setSpan(context.active(), span),
fn
);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
span.recordException(error);
throw error;
} {
span.();
}
}
() {
.(
,
fn,
{
: toolName,
: .(params)
}
);
}
}
tracer = ();
tracer.(, () => {
tracer.(, { prNumber }, () => {
});
});
📊 Observability Best Practices
1. Correlation IDs
import { v4 as uuidv4 } from 'uuid';
class CorrelationContext {
constructor() {
this.correlationId = uuidv4();
this.parentId = null;
}
createChild() {
const child = new CorrelationContext();
child.parentId = this.correlationId;
return child;
}
getHeaders() {
return {
'X-Correlation-ID': this.correlationId,
'X-Parent-ID': this.parentId
};
}
}
const ctx = new CorrelationContext();
logger.info('Starting operation', {
correlation_id: ctx.correlationId,
parent_id: ctx.parentId
});
2. Health Checks
export async function checkHealth() {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
checks: {}
};
try {
await fetch('http://localhost:3000/health');
health.checks.mcp_server = { status: 'up' };
} catch (error) {
health.checks.mcp_server = { status: 'down', error: error.message };
health.status = 'unhealthy';
}
const used = process.memoryUsage();
const memoryPercent = (used.heapUsed / used.heapTotal) * 100;
health.checks.memory = {
status: memoryPercent < 90 ? 'healthy' : 'warning',
heap_used_mb: Math.round(used.heapUsed / 1024 / 1024),
: .(used. / / ),
: .(memoryPercent)
};
health;
}
3. Dashboards
export function generateDashboard(metrics, logs) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Agent Metrics Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<h1>📊 Agentic Workflow Metrics</h1>
<div class="metrics">
<div class="metric-card">
<h3>Success Rate</h3>
<div class="value">${metrics.successRate}%</div>
</div>
<div class="metric-card">
<h3>Avg Duration</h3>
<div class="value">${metrics.avgDuration}ms</div>
</div>
<div class="metric-card">
<h3>Error Count</h3>
<div class="value">${metrics.errorCount}</div>
</div>
</div>
<canvas id="durationChart"></canvas>
<script>
const ctx = document.getElementById('durationChart');
new Chart(ctx, {
type: 'line',
data: {
labels: ${JSON.stringify(metrics.timestamps)},
datasets: [{
label: 'Duration (ms)',
data: ${JSON.stringify(metrics.durations)},
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
}
});
</script>
</body>
</html>
`;
}
📚 Related Skills
🔗 References
Logging & Monitoring
GitHub Actions
Observability Tools
✅ Remember Checklist
When implementing logging and monitoring for agentic workflows:
License: Apache-2.0
Version: 2.0.0
Last Updated: 2026-04-02
Maintained by: Hack23 Organization
🔗 Integration with Riksdagsmonitor agentic workflows
This gh-aw skill is applied by the 11 agentic news workflows in .github/workflows/news-*.md. Their domain contract (analysis-artifact product, gate, article contract) lives in:
Upstream gh-aw docs (v0.69.3): abridged · complete · agentic-workflows blog series · source repo · GitHub CLI manual.