Log Doctor: Letting AI Take the First Look at My Kubernetes Errors Using AWS Bedrock and AWS Opensearch A developer built Log Doctor, a Kubernetes CronJob that runs every 15 minutes on EKS, pulls aggregated error logs from OpenSearch via Fluentd, sends a bounded, redacted prompt to an LLM on Amazon Bedrock, and posts a short per-service diagnosis to Slack. The design caps prompt size with an OpenSearch terms aggregation (10 services, 5 messages each, 2 samples) and strips AWS keys, bearer tokens and secrets before anything reaches the model. Description: "Every 15 minutes, a small CronJob reads the error logs, asks an LLM on Amazon Bedrock what's going on, and posts a short diagnosis to Slack. Here's how I built it on EKS, Fluentd and OpenSearch." If you've ever been on call, you know how this goes. An alert fires: error rate on orders-api above 5% . You open the log search, filter on level: ERROR , and get back a few hundred lines. You start scrolling. Most of them are the same stack trace repeated over and over. A few are something else. One or two of those might be the real problem, but you won't know until you've read them. By the time you've worked out "okay, the database is out of connections, and the payment timeouts are probably a side effect", ten minutes have gone. That's ten minutes of reading, not fixing. What's missing is the step in between, the first pass a senior engineer does without thinking about it. Group the noise, notice which error is the loud one, form a quick theory, and decide where to look first. That first pass is mostly reading and summarizing repetitive text. That happens to be something large language models are genuinely good at. I didn't want an AI agent with access to my whole cluster. I wanted something much smaller and simpler: Every 15 minutes, look at the errors from the last 15 minutes. If there are any, post a short note to Slack, per service: what's happening, the likely cause, and what to check. If there are none, say nothing. A few rules I set for myself before writing any code: orders-api / payments-api → Fluentd → OpenSearch → Log Doctor CronJob → Amazon Bedrock → Slack Everything runs on a stack that's common in AWS shops, and it's all Terraform plus plain Kubernetes manifests: /simulate-error endpoint that fails on purpose in realistic ways: a database out of connections, a payment call timing out, and the classic 'NoneType' object is not subscriptable . app-logs-YYYY.MM.DD index. 👉 The code is here: github.com/ranjiniganeshan/log-doctor https://github.com/ranjiniganeshan/log-doctor My first thought was to fetch a few hundred log lines and give them to the model. But OpenSearch can already do the grouping, and it gives exact counts. Log Doctor runs a single aggregation: errors per service, then top messages per service, with two recent samples of each. "aggs": { "services": { "terms": {"field": cfg.service field, "size": cfg.max services, "missing": " unknown "}, "aggs": { "messages": { "terms": {"field": cfg.message field, "size": cfg.max messages per service}, "aggs": { "first seen": {"min": {"field": cfg.timestamp field}}, "last seen": {"max": {"field": cfg.timestamp field}}, "samples": { "top hits": { "size": cfg.samples per message, "sort": {cfg.timestamp field: {"order": "desc"}} , " source": {"includes": SAMPLE SOURCE FIELDS}, } }, }, } }, } }, With the defaults, that's at most 10 services, 5 messages per service and 2 samples per message. So I know the maximum size of the prompt, however bad the incident is. The Python code then trims it further: maxTokens caps the reply. There's another benefit: the model gets real counts. It's told "25 of these" instead of having to count lines itself. I spent extra time on this part. Log lines can contain whatever your users typed. If you put them in a prompt and post the answer to Slack, a stranger's text can end up in your on-call channel. So I added three protections: REDACTIONS = re.compile r"AKIA 0-9A-Z {16}" , " REDACTED AWS KEY " , re.compile r" ?i bearer\s+ a-z0-9. \- +" , "Bearer REDACTED " , re.compile r" ?i password|passwd|secret|token|api - ?key \s =: \s \S+" , r"\1= REDACTED " , re.compile r" A-Za-z0-9. %+\- +@ A-Za-z0-9.\- +\. A-Za-z {2,}" , " REDACTED EMAIL " , re.compile r"https://hooks\.slack\.com/\S+" , " REDACTED WEBHOOK " , < channel pushed to your whole team. None of these is perfect. Regex only catches the patterns you thought of, and a model can still be tricked. So the most important protection is the design itself: the model has no tools and no credentials, and all it can do is write text for Slack. If someone does trick it, the worst result is a misleading message. AWS account iam:CreateOpenIDConnectProvider . If your account is in an AWS Organization, check that no service control policy blocks it. Tools envsubst brew install gettext on macOS make and curl Slack Run these checks before you create anything. They don't cost anything, and the EC2 command is only a dry run: export AWS REGION=ap-south-1 Confirm you're in the right account aws sts get-caller-identity Check that you can launch the node type AMI=$ aws ssm get-parameter \ --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86 64 \ --query Parameter.Value --output text aws ec2 run-instances --dry-run --image-id "$AMI" \ --instance-type t3.medium --count 1 The EC2 command should reply with Request would have succeeded, but DryRun flag is set . 💸 This creates billable resources. Estimated cost is in the cost section. Tear it down when you're finished. Log Doctor uses Bedrock's Converse API, so it works with any Bedrock chat model. List the models you can call directly in your region: aws bedrock list-foundation-models --region ap-south-1 --by-output-modality TEXT \ --query "modelSummaries ?contains inferenceTypesSupported,'ON DEMAND' .modelId" I used openai.gpt-oss-20b-1:0 , OpenAI's open-weight model. Some models are only offered through cross-region inference profiles, with IDs that start with global. or apac. . Those can send your request to another region, so a policy that limits your account to one region can block them. Then make a real test call. A model appearing in the list doesn't prove your account can use it. export AWS REGION=ap-south-1 aws bedrock-runtime converse --model-id "openai.gpt-oss-20b-1:0" \ --messages ' {"role":"user","content": {"text":"Reply with OK"} } ' \ --inference-config '{"maxTokens":800}' Third-party models can create an AWS Marketplace subscription on first use. The CronJob's role isn't allowed to do that, which is correct for least privilege, so make this first call with your own admin identity. This model returns reasoning blocks as well as the answer, and it counts them against maxTokens . Don't set the limit too low. Log Doctor keeps only the text blocks. At api.slack.com/apps https://api.slack.com/apps , create an app, turn on Incoming Webhooks , and add one to a channel. Treat the URL like a password, because anyone who has it can post to your channel. export SLACK WEBHOOK URL='https://hooks.slack.com/services/...' It stays in your shell and goes into a Kubernetes Secret later. Nothing gets written to the repo. 📌 ADD BEFORE PUBLISHING: screenshot of the Incoming Webhooks page, with the URL hidden cp infra/terraform.tfvars.example infra/terraform.tfvars These are the settings I used: region = "ap-south-1" bedrock model id = "openai.gpt-oss-20b-1:0" node instance type = "t3.medium" cluster endpoint public access cidrs = "