<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Muhammetberdi Jepbarov on Medium]]></title>
        <description><![CDATA[Stories by Muhammetberdi Jepbarov on Medium]]></description>
        <link>https://medium.com/@mecreate?source=rss-8bc0f2c771a------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*sYQrcSw2TcjprUM-ivn8Rg.png</url>
            <title>Stories by Muhammetberdi Jepbarov on Medium</title>
            <link>https://medium.com/@mecreate?source=rss-8bc0f2c771a------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 19:42:24 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mecreate/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[I Moved to Germany and Learned the Language with AI. Here Is My Exact Toolchain.]]></title>
            <link>https://medium.com/@mecreate/i-moved-to-germany-and-learned-the-language-with-ai-here-is-my-exact-toolchain-9676186f275d?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/9676186f275d</guid>
            <category><![CDATA[notebooklm]]></category>
            <category><![CDATA[cloud-services]]></category>
            <category><![CDATA[ai-tools]]></category>
            <category><![CDATA[google]]></category>
            <category><![CDATA[gemini]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Fri, 09 Oct 2026 22:23:50 GMT</pubDate>
            <atom:updated>2026-10-09T22:38:00.425Z</atom:updated>
            <content:encoded><![CDATA[<h4>Gemini, NotebookLM, Google Cloud and the everyday Google apps, used as a real system. No theory, just the steps you can copy today.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Lf_pAkwfWAi371UzMEI0QQ.jpeg" /></figure><p>I landed in Göttingen with a suitcase and a phone. My German was close to zero. I had to do very real things in German: activate a SIM card, ask for a seat on the train, show my ticket to an inspector, and sit across from a landlord and tell about myself.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FIcjrr2zEEaU%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIcjrr2zEEaU&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FIcjrr2zEEaU%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/159f87b0e23640eaddfe93b6734d9c56/href">https://medium.com/media/159f87b0e23640eaddfe93b6734d9c56/href</a></iframe><p>I reached several tools, especially Google’s tools. It is built from three pillars, <strong>Gemini</strong>, <strong>NotebookLM</strong>, and <strong>Google Cloud. </strong>It works not only for a language learning but for any subject. And you can set it up tonight.</p><blockquote><em>“AI is my sparring partner. The street is the real match.”</em></blockquote><h3>The one rule that makes AI actually work</h3><p>Most people use AI and still do not learn. They read a perfect answer, nod, and forget it. The fix is a loop:</p><pre>PREPARE with AI  -&gt;  PRACTICE in the real world  -&gt;  FEEDBACK  -&gt;  (repeat)</pre><p>Here is the story that taught me this. <br><strong>Activating a SIM card.</strong><br>I could not explain my problem to the worker. First try, fail. Second, fail. By the fifth try I finally said it clearly and it worked. No grammar book corrected me. The situation did. Every confused look and every “wie bitte?” was a signal telling my brain what to change.</p><p>In my field we call that <strong>reinforcement learning</strong>: you act, you get a reward signal, you update. Your brain does the same thing with a real conversation. So the job of AI is not to replace the conversation. It is to get you ready for it, and to help you dissect it afterwards.</p><blockquote><em>“Every ‘wie bitte?’ is training data.”</em></blockquote><p>Now the toolchain.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ddntwQmr4-CHGKutJr6apQ.png" /></figure><h3>Pillar 1: Capture reality, transcription with Google tools</h3><p>You learn fastest from your own words and your own life. That means turning speech and the real world into text you can work with. Google gives you three tiers, from zero effort to full power.</p><h3>Tier 1, zero setup: Google Docs Voice Typing and the Recorder app</h3><p><strong>Google Docs Voice Typing</strong> is free and instant. Open a Doc, go to <em>Tools &gt; Voice typing</em>, pick <strong>German (Deutschland)</strong>, and speak. It writes what it hears. Speak German for a minute and read back what actually came out. Where the text is wrong, either you mispronounced it or you used the wrong word. That is your first free feedback.</p><blockquote>OpenAI’s ChatGPT also could be used normally.</blockquote><p>On mobile phone the <strong>Recorder</strong> app transcribes as you record and lets you search inside your recordings. Record yourself or a real conversation you are allowed to record, and you get text you can paste into Gemini.</p><h3>Tier 2, upload and ask: Gemini transcribes audio directly</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/785/0*YHZux8k3Sr1U7mO8" /></figure><p>Gemini is multimodal. In the Gemini app or in Google AI Studio you can upload an audio file and simply say: <em>“Transcribe this German audio, then list every grammar mistake I made.”</em> One step, transcript plus correction.</p><blockquote>Other options: turboscribe.ai and elevenlabs.</blockquote><h3>Tier 3, power user: Google Cloud Speech-to-Text with Chirp</h3><p>When you want accuracy and volume, use <strong>Cloud Speech-to-Text</strong>. Its universal <strong>Chirp</strong> model is trained on millions of hours across 120+ languages, German included, and Google gives you <strong>60 minutes of transcription free every month</strong>, with no expiry. Async transcription handles files up to 480 minutes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RLChE3TYyuFl08e6.png" /></figure><p>A minimal Python call looks like this:</p><pre># pip install google-cloud-speech<br>from google.cloud import speech_v2 as speech<br>from google.cloud.speech_v2.types import cloud_speech as t<br>client = speech.SpeechClient()<br>with open(&quot;meine_aufnahme.wav&quot;, &quot;rb&quot;) as f:<br>    audio_bytes = f.read()<br>config = t.RecognitionConfig(<br>    auto_decoding_config=t.AutoDetectDecodingConfig(),<br>    language_codes=[&quot;de-DE&quot;],<br>    model=&quot;chirp_2&quot;,<br>)<br>req = t.RecognizeRequest(<br>    # use a regional recognizer, e.g. europe-west4, see the docs<br>    recognizer=&quot;projects/PROJECT_ID/locations/europe-west4/recognizers/_&quot;,<br>    config=config, content=audio_bytes,<br>)<br>for result in client.recognize(request=req).results:<br>    print(result.alternatives[0].transcript)</pre><p>Not a coder? You never touch this. Tier 1 and Tier 2 are enough for 95% of learners. The point is: your voice becomes text, and text is something AI can teach you from.</p><h3>Pillar 2: Gemini, your always-on tutor</h3><p>Gemini is the daily driver. Five features carry the whole workflow.</p><h3>1. Gemini Live: talk out loud, hands free</h3><p>Open the Gemini app, tap <strong>Live</strong>, and just have a conversation in German. This is your zero-stakes rehearsal before a real one. My go-to opener:</p><blockquote>“Let’s talk in German at B1 level about apartment hunting. Ask me questions like a landlord would. Correct me gently as we go, and keep it slow.”</blockquote><h3>2. Gems: a saved tutor that knows you</h3><p>A <strong>Gem</strong> is a custom Gemini you configure once and reuse forever. Open Gemini, expand the left menu, click <strong>Gems &gt; New Gem</strong>, paste instructions, and (the killer part) <strong>connect it to a Google Drive folder</strong> so it reads your notes live. Update a Doc, and your tutor sees the change instantly.</p><p>Here is the instruction I use. Copy it into a Gem:</p><pre>You are &quot;Mein Deutschcoach&quot;, my personal German tutor. My level is B1.2, goal C1.<br>I am an English-fluent software engineer living in Germany.<br>Every session:<br>- Explain in clear English, keep all German examples correct (a o u umlauts and ss).<br>- Correct every German mistake I make, briefly, and keep a running error list.<br>- Teach new words in small batches of 5, then quiz me before moving on.<br>- Prefer real, everyday phrasing over textbook German.<br>- When I paste a transcript of my own speech, give me: a corrected version, a better<br>  B2 version, a table of my mistakes, and 5 drills targeting exactly those mistakes.<br>Read my linked Drive folder for my current vocabulary and my error list.<br>Start by asking what we work on today and how many minutes I have.</pre><h3>3. Gemini inside Google Docs: fix your writing in place</h3><p>Every real German text I must write, an email to a housing platform, a message to a landlord, I draft myself first, then in the Doc I use <strong>Help me write</strong> and <strong>Help me refine</strong> to polish it and, more importantly, ask it to explain each change so I learn. You never leave the document.</p><h3>4. Canvas: build a living study sheet</h3><p><strong>Canvas</strong> is an editable side-by-side workspace. Ask Gemini to “create a one-page B1 cheat sheet for two-way prepositions” in Canvas, then keep refining it in place instead of copying answers around. It can also build small interactive apps, a flashcard quiz for your words, for example.</p><h3>5. Deep Research: understand any topic properly</h3><p><strong>Deep Research</strong> runs many searches and returns a cited report. I use it for grammar deep dives (“give me a complete, cited guide to German cases with common mistakes”), and for life admin (“how does the Anmeldung and SCHUFA process work for a new resident”). Same feature, any subject.</p><blockquote><em>“I prepare with Google. I practice with Germans. I learn from the reaction.”</em></blockquote><h3>Bonus for builders: Google AI Studio (free)</h3><p>If you want to go further, <strong>Google AI Studio</strong> (aistudio.google.com) is a free playground for the Gemini API. Set a <strong>system instruction</strong>, upload text/image/audio/video, test prompts, and click <strong>Get code</strong> to export a working Python or JavaScript snippet. I used it to prototype a tiny script that takes my daily voice note and returns a corrected version plus flashcards. Free tier note: on the free plan your prompts can be used to improve Google’s products, so keep anything sensitive out of it or use a billed project.</p><h3>Pillar 3: NotebookLM, turn your material into a podcast, a video, and slides</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KCPpq_6zUD8J8xRD.png" /></figure><p>This is the tool that surprises everyone. <strong>NotebookLM</strong> reads sources <em>you</em> give it, your lesson PDFs, your notes, a website, a YouTube link, up to 50 sources, and turns them into study material grounded in those sources with citations.</p><p><strong>How to start:</strong> go to notebooklm.google.com, create a notebook, and add sources (upload files, paste a link, or import Google Docs and Slides straight from Drive). Then generate:</p><p>Output What you get My use <strong>Audio Overview</strong> a podcast-style discussion, now in 50+ languages, with interactive mode a German podcast of this week’s lesson for the train <strong>Video Overview</strong> a narrated slide presentation a visual recap of a grammar topic <strong>Mind Map</strong> the structure of the material see how a chapter connects <strong>Study guide / Quiz / Flashcards</strong> ready revision material self-test before class</p><p>Audio Overview is also built into the Gemini app now, powered by the free Deep Research model (free accounts get a monthly limit).</p><p><strong>The language trick:</strong> set the Audio Overview output language to <strong>German</strong> for immersion, then generate it again in <strong>English</strong> to check you understood. Customization prompt I paste:</p><pre>Speak slow, clear German at B1 level. Topic: this week&#39;s lesson.<br>Explain the grammar simply, use everyday examples (asking for a seat on a train,<br>a doctor visit, an apartment viewing), and repeat the key vocabulary twice.</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0EIzXVlsN4O5OMmBPxXX7w.png" /></figure><h3>The everyday Google apps that teach you all day</h3><p>The big three do the heavy lifting. These four do the quiet, constant work.</p><ul><li><strong>Google Lens</strong> turns the physical world into a lesson. Point it at a poster, a form, a product label, and read it in German with the translation next to it. Context makes words stick.</li><li><strong>Google Keep</strong> is my “overheard German” notebook. When I hear a phrase I do not know, I dump a quick voice note. At night I paste the list into Gemini: <em>“Decode these phrases I heard today, explain the meaning and when to use them.”</em></li><li><strong>Google Calendar</strong> holds the loop together: a daily 10-minute review reminder and one recurring “speak to a real person today” nudge.</li></ul><h3>Store it once, let everything read it: Drive and Cloud</h3><p>The system dies if it lives in 40 browser tabs. Put everything in <strong>one Google Drive folder</strong>: your lesson notes, your recordings, your running error list. Then:</p><ul><li>Your <strong>Gemini Gem</strong> reads that folder live.</li><li><strong>NotebookLM</strong> imports those Docs directly as sources.</li><li><strong>Google Cloud Storage</strong> holds longer audio for Speech-to-Text.</li><li>Everything syncs across your phone and laptop automatically.</li></ul><p>One source of truth, read by every tool. That is the whole architecture.</p><h3>My weekly loop</h3><p>When Action Tool Every lesson Record it, transcribe, turn into a clean note Recorder / Cloud STT + Gemini 3x a week Speak 1 minute, transcribe, get corrected Docs Voice Typing + Gem Daily 10-minute review + one real German interaction Calendar + the street Weekly This week’s notes become a German podcast + quiz NotebookLM Before a real event Roleplay it (landlord, phone call) Gemini Live</p><h3>This is not only for language</h3><p>Swap “German” for “Machine Learning” and nothing else changes. For my Master’s I drop research papers into <strong>NotebookLM</strong> and get an audio overview to hear the argument on a walk. I use <strong>Deep Research</strong> to map a topic before I read deeply. I keep a Drive folder per course that my <strong>Gemini Gem</strong> tutors me from. I transcribe my own spoken explanation of a concept and ask Gemini where my understanding has gaps.</p><p>The method is universal: <strong>capture your input, let AI structure and quiz you, produce something, then test it against reality.</strong> Language just gives you the fastest, most honest feedback, because a real person reacts in one second.</p><h3>Start today: a 30-minute setup</h3><ol><li>Create one <strong>Google Drive folder</strong> called <em>Deutsch</em> (or your subject).</li><li>Build a <strong>Gemini Gem</strong> with the instruction above and connect it to that folder.</li><li>Open <strong>NotebookLM</strong>, create a notebook, add one document, and generate an Audio Overview.</li><li>Install <strong>Google Translate</strong>, download German offline, and pin the <strong>Phrasebook</strong>.</li><li>Put a <strong>Recorder / Voice Memo</strong> shortcut on your home screen.</li><li>Add two <strong>Calendar</strong> reminders: daily review, and “speak to one real person”.</li></ol><p>Then do the only step that matters: go outside and use it. Order in German. Ask a question you already know the answer to. Get a little bit into trouble. Come back, transcribe what went wrong, and let the tools help you fix it.</p><blockquote><em>“Do not let AI make you passive. Prepare with it, then go collect real feedback. That is where learning actually happens.”</em></blockquote><p>If this helped, I share more about building, learning, and moving countries on YouTube at <strong>@</strong><a href="https://www.youtube.com/@inneovation"><strong>inneovation</strong></a> and on <a href="https://www.linkedin.com/in/jepbarov/">LinkedIn at <strong>/in/jepbarov</strong></a>. Viel Erfolg.</p><p><a href="#AI">#AI</a> <a href="#Gemini">#Gemini</a> <a href="#NotebookLM">#NotebookLM</a> <a href="#GoogleCloud">#GoogleCloud</a> <a href="#GoogleAI">#GoogleAI</a> <a href="#LanguageLearning">#LanguageLearning</a> <a href="#LearnGerman">#LearnGerman</a> <a href="#DeutschLernen">#DeutschLernen</a> <a href="#GoogleDeveloperExpert">#GoogleDeveloperExpert</a> <a href="#GDE">#GDE</a> <a href="#DAAD">#DAAD</a> <a href="#StudyInGermany">#StudyInGermany</a> <a href="#Productivity">#Productivity</a> <a href="#LifelongLearning">#LifelongLearning</a> <a href="#EdTech">#EdTech</a> <a href="#AItools">#AItools</a> <a href="#Multilingual">#Multilingual</a> <a href="#DigitalNomad">#DigitalNomad</a> <a href="#SelfLearning">#SelfLearning</a> <a href="#GenerativeAI">#GenerativeAI</a></p><h3>Sources and further reading</h3><ul><li>NotebookLM features and 50+ language Audio Overviews: <a href="https://www.digitalocean.com/resources/articles/what-is-notebooklm">DigitalOcean guide</a>, <a href="https://www.techradar.com/computing/artificial-intelligence/google-is-turning-your-favorite-ai-podcast-hosts-into-polyglots">TechRadar on languages</a></li><li>Audio Overview in Gemini: <a href="https://www.techradar.com/computing/artificial-intelligence/get-ready-for-audio-overview-in-google-gemini-ive-used-it-in-notebook-lm-and-its-a-complete-game-changer">TechRadar</a></li><li>Gemini Gems, Canvas, Deep Research: <a href="https://workspace.google.com/learning/report/content/geminiatworkapracticalguide">Google Workspace practical guide</a>, <a href="https://freshvanroot.com/blog/google-gemini-review/">Fresh van Root</a></li><li>Google AI Studio free API: <a href="https://codeforgeek.com/how-to-use-google-gemini-api-for-free/">CodeForGeek</a>, <a href="https://reskilll.com/blogs/google-ai-studio-the-free-platform-to-build-with-gemini-in-2026/">reskilll 2026 guide</a></li><li>Cloud Speech-to-Text, Chirp, free tier: <a href="https://docs.cloud.google.com/speech-to-text/docs/models/chirp-3">Chirp 3 docs</a>, <a href="https://cloud.google.com/speech-to-text/pricing">Speech-to-Text pricing</a></li><li>Google Translate features: <a href="https://support.google.com/translate/answer/6142473">Google Translate Help, offline</a>, <a href="https://www.alltechnerd.com/ways-to-use-google-translate-camera-voice-and-offline-mode/">AllTechNerd camera/voice/offline</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9676186f275d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Which open source Knowledge Base system would I recommend today?]]></title>
            <link>https://medium.com/@mecreate/which-open-source-knowledge-base-system-would-i-recommend-today-1c60c2564f04?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/1c60c2564f04</guid>
            <category><![CDATA[wiki]]></category>
            <category><![CDATA[notebooklm]]></category>
            <category><![CDATA[documentation]]></category>
            <category><![CDATA[knowledge-management]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Wed, 26 Aug 2026 21:14:16 GMT</pubDate>
            <atom:updated>2026-08-26T21:14:16.139Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3tDRFRtv1o2YkeEDperIeQ.png" /></figure><p>Back in 2024, we had a pretty simple problem.</p><blockquote>We needed a proper Knowledge Base.</blockquote><p>Not just a folder full of Markdown files. Not another collection of random documents. We needed something where we could organize courses, documentation, homework, videos and user access in a way that actually made sense.</p><p>That was when I seriously discovered BookStack.<br>And the interesting part is that I still use it today.</p><h3>BookStack: simple, but structured</h3><p>For our <a href="https://eu.edu.tm/">Erkin Ugur</a> environment, BookStack turned out to be a surprisingly good fit. Especially for courses.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Qc6_yUSS05FBH6AS_wWF7Q.png" /></figure><p>When we launch a new course, we can create a dedicated user group, add people manually or through automated registration, and then control access to the relevant content.</p><p>The structure itself is also a very natural fit for education.</p><p>Book → Chapter → Page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/889/1*bS3Wq-yeI52qJPoOxoMDig.png" /></figure><p>A book becomes a course. A chapter becomes a section. A page becomes a lesson.</p><p>Inside those pages, we can use Markdown, HTML, images, videos and additional materials. We can also rely on external storage when we don’t want to keep everything inside the Knowledge Base itself.</p><p>It is simple.<br>But it is structured.<br>And that combination is exactly what I like about it.</p><p>You read the material. You complete a homework task. You add something. You come back months later and you can still understand where everything is. For me, that is one of the most important jobs of a Knowledge Base.</p><p>It should not only help you find information today.<br>It should preserve the knowledge and context you built yesterday.</p><h3>Wiki.js: flexibility with a price tag</h3><p>We also used Wiki.js for another type of problem.</p><p>And Wiki.js is a great project.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LGyVAdLIEyGs2ImL.png" /></figure><p>In one of our use cases, we needed a more architectural approach and the ability to work with the system through GraphQL. For that particular scenario, Wiki.js gave us the flexibility we needed.</p><p>But flexibility comes with a price.</p><p>There are a lot of configuration options and a lot of things you can customize in Wiki.js. That is great when you actually need them.<br>But sometimes you don’t want to configure a hundred things.</p><p>Sometimes you just want to install a Knowledge Base, create a structure, add users and start documenting.<br>This is where BookStack wins for me.<br>It may not be the most flexible Knowledge Base system out there.<br>But it is incredibly easy to turn it into a working tool.</p><h3>So, which one?</h3><p>So if I had to choose today, my recommendation would be pretty simple.</p><p>If you need a clean and straightforward Knowledge Base for documentation, courses, internal materials or learning programs, I would definitely give BookStack a try.</p><p>If you need deeper customization, a more complex architecture, integrations and a stronger API-driven approach, and you have the time to work through a much larger configuration surface, Wiki.js might be the better choice.</p><p>Both are great open source projects.<br>They are just built around slightly different priorities.</p><h3>A third path: staying inside Google</h3><p>But there’s a third path I’ve been experimenting with, and it’s worth mentioning.</p><p>Not everyone wants to run a server, patch a container, or think about backups. Sometimes the honest answer is: don’t self-host anything at all.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HaCIVXB1gGfIbElk.png" /></figure><p>This year I spent some time with NotebookLM Enterprise, and it’s a genuinely different animal from BookStack or Wiki.js. You’re not building a book with chapters and pages. You’re feeding it source material, PDFs, docs, transcripts, and it becomes a layer that answers questions grounded in exactly those sources, with citations back to where the answer came from. Google wraps it with IAM and VPC Service Controls, so a course’s materials can stay scoped to the people who should actually see them, the same way a BookStack user group would.</p><p>It’s not a replacement for a structured course library. There’s no Book → Chapter → Page hierarchy, no homework tracking, no sense of “this page belongs after that page.” What it’s good at is a different job entirely: someone asks a question, and the answer comes back grounded in your material instead of the model guessing.</p><p>For plain internal documentation that a team edits together, Google Sites still does the job it always did. It’s not exciting, and it won’t feel like a Knowledge Base in the BookStack sense, but if all you need is pages, some structure, and zero maintenance, it’s there and it’s free with a Workspace account.</p><p>So that’s the third option, and it’s really a different question than “BookStack or Wiki.js.” It’s “do I want to own this, or do I want Google to own it.” If you don’t want to think about updates, backups, or a database at all, and you’re fine with your content living inside Google’s ecosystem, that’s a completely reasonable choice. If you want the Book → Chapter → Page structure, the course-shaped access control, and the ability to move your content wherever you want, self-hosting is still the way I’d go.</p><h3>Self-hosting without babysitting a server</h3><p>And if you do want to self-host, but you don’t want to manage a VM either, there’s a middle ground worth knowing about.</p><p>Both BookStack and Wiki.js run fine as containers, and Google Cloud gives you two reasonable ways to run them without babysitting a server.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/960/0*2rT8i0Zl3XYK9mxW" /></figure><p><strong>Option 1: Cloud Run</strong>, the quick way. Push the image to Artifact Registry, point Cloud Run at it, and let Google handle scaling. The catch is that Cloud Run expects a stateless container, and BookStack or Wiki.js are not stateless on their own, they need a database and somewhere to keep uploaded files. The fix is to split those pieces out: a Cloud SQL instance for MySQL or MariaDB, and either Cloud Storage through an S3-compatible bucket or a mounted volume for uploads. Roughly:</p><pre>gcloud sql instances create bookstack-db --database-version=MYSQL_8_0 --tier=db-f1-micro --region=us-central1<br>gcloud artifacts repositories create kb-images --repository-format=docker --location=us-central1<br>docker build -t us-central1-docker.pkg.dev/PROJECT_ID/kb-images/bookstack .<br>docker push us-central1-docker.pkg.dev/PROJECT_ID/kb-images/bookstack<br>gcloud run deploy bookstack \<br>  --image us-central1-docker.pkg.dev/PROJECT_ID/kb-images/bookstack \<br>  --add-cloudsql-instances PROJECT_ID:us-central1:bookstack-db \<br>  --region us-central1</pre><p>That’s a working setup for a small team, and you’re not managing an operating system at all.</p><p><strong>Option 2: GKE Autopilot</strong>, if you outgrow that, or you’d rather have persistent volumes, background jobs and more control from day one. You still don’t manage nodes, Google provisions and scales them for you, but you get a real Kubernetes deployment with a persistent volume for uploads instead of juggling external storage:</p><pre>gcloud container clusters create-auto kb-cluster --region us-central1<br>gcloud container clusters get-credentials kb-cluster --region us-central1<br>kubectl apply -f bookstack-deployment.yaml</pre><p>Neither approach is complicated once you’ve done it once, and both keep you out of the business of patching an operating system, which is honestly most of what self-hosting used to mean.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/0*aiQTv1tKMQq_H7Fe.png" /></figure><h3>Closing thoughts</h3><p>For me, BookStack became one of those rare tools that I didn’t have to constantly rethink.</p><p>I installed it, structured the content and started using it.<br>And honestly, that might be one of the best compliments you can give to a piece of software.</p><p>By the way, for my personal knowledge base on my local machine, I use Obsidian. But that’s a story for another post.</p><p>BookStack: <a href="https://github.com/bookstackapp/bookstack">https://github.com/bookstackapp/bookstack</a> <br>Wiki.js: <a href="https://github.com/requarks/wiki">https://github.com/requarks/wiki</a></p><p>#opensource #knowledgebase #bookstack #wikijs #documentation #selfhosted #opensourceprojects #devtools #elearning #markdown #googlecloud #gke #cloudrun</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1c60c2564f04" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Stopped Uploading My Audio to the Cloud for Transcription services like Elevenlabs and Whispflow.]]></title>
            <link>https://medium.com/@mecreate/i-stopped-uploading-my-audio-to-the-cloud-for-transcription-services-like-elevenlabs-and-whispflow-a103895db2eb?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/a103895db2eb</guid>
            <category><![CDATA[transcription]]></category>
            <category><![CDATA[whisper]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Thu, 21 May 2026 14:27:53 GMT</pubDate>
            <atom:updated>2026-06-12T12:01:10.192Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>I Stopped Uploading My Audio to the Cloud for Transcription services like Elevenlabs and </strong>Whispflow.<strong> Here’s What I Built Instead.</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6GjE_J_GE5hOlOhOofr6Pw.jpeg" /></figure><p>In March I hit the free tier limit on 11 Labs.</p><p>This is not a dramatic event — it happens to everyone who actually uses a product — but the moment I started looking for alternatives, I found something that bothered me more than the paywall. I had been using 11 Labs to transcribe voice messages, lecture recordings, and meeting notes. The quality was genuinely good: fast, accurate, handles background noise reasonably well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G-NMN931_Jopy77qHmmitw.jpeg" /></figure><p>When the limit came up, I moved to TurboScribe, which offered three free files per day, up to thirty minutes each. I noticed pretty quickly that opening an incognito tab reset the counter. So I uploaded a few test recordings, got the transcripts, and closed the incognito window.</p><p>—</p><p>Watch the Video on the GDG Ashgabat’s Build With AI Sessions:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FeMTHCD0Gsqo%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DeMTHCD0Gsqo&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FeMTHCD0Gsqo%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/1f08c3ef2dbd2a0be693508027912c0e/href">https://medium.com/media/1f08c3ef2dbd2a0be693508027912c0e/href</a></iframe><p><strong>And then it hit me: I had no idea where those files went.</strong></p><p>I could not delete them. I did not even know which server they were sitting on. Those happened to be harmless test recordings, but the question stayed with me — what if they had been a client call? A medical consultation? Something confidential?</p><p>This is not a TurboScribe-specific problem. It is how every cloud transcription service works by design. TurboScribe’s privacy policy states they may retain your audio for up to thirty days. ElevenLabs’ terms include language about using content to improve their services. These are not hidden clauses — they are just things people scroll past.</p><p>For most casual use, this is a tolerable trade-off. For anything sensitive — legal, medical, journalistic, commercial — it is not.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TC6u4F8gBJpkQDTm8R5NQw.jpeg" /></figure><p>The question I kept coming back to was:<br>- <em>Does the transcription actually need to happen on their servers?<br>- Is there a technical reason the audio has to leave my machine?</em></p><p>The answer is that there is no technical reason at all.</p><p>OpenAI released <strong>Whisper</strong> as <strong>open source</strong> in 2022, and the model quality is on par with what the commercial services offer — because many of them are running Whisper or a derivative under the hood anyway. What was missing was a way to actually use it without spending an afternoon fighting CUDA dependencies and Python environment hell.</p><p>So I built one.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FG8NdLMoKUyNBXu-azDmAw.jpeg" /></figure><h3>The Stack</h3><p>The core of the project is <strong>faster-whisper</strong>, a reimplementation of OpenAI’s Whisper by SYSTRAN.</p><p>The key difference from the original is INT8 quantisation: the model weights are stored in a lower-precision format, which reduces memory usage and speeds up inference on CPU by roughly two to four times without any meaningful loss in transcription accuracy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l8Tc7s15460TqpR4-DKZ1A.jpeg" /></figure><p>The large-v3-turbo model weighs about 1.5 GB on disk and uses around 6 GB of RAM at runtime. It is the sweet spot between quality and speed. Before audio reaches Whisper, it goes through FFmpeg for format conversion.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lxOZ5gwT-Z5xt_HG7jjX-A.jpeg" /></figure><p>Whisper expects 16kHz mono WAV. In practice, your files will be MP3 voice messages, M4A recordings from your phone, MP4 exports from Zoom — FFmpeg normalises all of that in seconds. This is what makes the tool actually useful rather than just a demo: you do not have to think about format compatibility.</p><p>The third significant component is Silero VAD, a neural network for Voice Activity Detection.</p><p>Whisper has a known tendency to hallucinate — to “transcribe” words from silence. Running VAD before transcription eliminates most of that, and as a side effect, significantly reduces processing time on recordings with long quiet stretches.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lGbXZEdDzkNgdF96edui4Q.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Dj7o4AaTY0xR5YtfZOwjZQ.jpeg" /></figure><p>For speaker identification, the tool uses SpeechBrain with ECAPA-TDNN voice embeddings and agglomerative clustering.</p><p>The result is a transcript with speaker labels: who said what, with timestamps. This runs entirely locally.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OuBWPBou6lo-I3HQ1xXjHQ.jpeg" /></figure><h3>How I Structured the Code</h3><p>I went through a few iterations before landing on something I was happy with. The first version was a single monolithic file — everything crammed together. It worked, but it was a nightmare to maintain.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EHcyaxKG6rISAP4OvNSiow.jpeg" /></figure><p>The version I ended up with separates concerns properly.<br>The business logic lives in a core/ package:</p><ul><li>engine.py handles model loading and transcription</li><li>diarize.py handles speaker detection</li><li>format.py handles timestamps and segment grouping</li><li>audio.py wraps FFmpeg</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zzss_1DGM2UYlHh46jqOKw.jpeg" /></figure><p>None of these modules know anything about HTTP or the web interface.<br>The transcription engine looks like this:</p><pre># core/engine.py<br>_cache = {}<br>def get_model(model_name):<br>    if model_name not in _cache:<br>        from faster_whisper import WhisperModel<br>        _cache[model_name] = WhisperModel(<br>            model_name,<br>            device=&quot;cpu&quot;,<br>            compute_type=&quot;int8&quot;,<br>            cpu_threads=8<br>        )<br>    return _cache[model_name]<br><br>def transcribe(audio_path,<br>               model_name=&quot;large-v3-turbo&quot;,<br>               language=None,<br>               on_progress=None):<br>    model = get_model(model_name)<br>    segments_iter, info = model.transcribe(<br>        audio_path,<br>        language=language,<br>        vad_filter=True,<br>        vad_parameters={&quot;min_silence_duration_ms&quot;: 500},<br>        beam_size=5,<br>    )<br>    segments = []<br>    for s in segments_iter:<br>        segments.append({<br>            &quot;start&quot;: s.start,<br>            &quot;end&quot;: s.end,<br>            &quot;text&quot;: s.text.strip()<br>        })<br>        if on_progress:<br>            on_progress(s.end, len(segments))<br>    return segments, info.language</pre><p>A few things worth noting.</p><p>The model is loaded lazily — it only gets pulled into memory when somebody actually requests a transcription, not when the server starts.</p><p>That matters because the model takes several seconds to load and consumes a substantial amount of RAM, so you do not want that happening at import time.</p><p>The on_progress callback is how the web interface gets live progress updates: every time Whisper finishes a segment, the callback fires with the current timestamp and segment count, which the API then exposes to the frontend. The web layer is a FastAPI application.</p><p>When a file comes in, it gets written to a temp file and submitted to a ThreadPoolExecutor for background processing. The client gets back a job ID immediately and polls for status:</p><pre># api.py<br>_executor = ThreadPoolExecutor(max_workers=2)<br>@app.post(&quot;/v1/transcribe&quot;)<br>async def submit(file: UploadFile,<br>                 language=&quot;auto&quot;,<br>                 model=&quot;large-v3-turbo&quot;):<br>    tmp = save_to_temp(file)<br>    job_id = str(uuid4())<br>    JOBS[job_id] = {<br>        &quot;status&quot;: &quot;queued&quot;,<br>        ...<br>    }<br>    _executor.submit(<br>        _run,<br>        job_id,<br>        tmp.name,<br>        language,<br>        model,<br>        ...<br>    )<br>    return {<br>        &quot;job_id&quot;: job_id,<br>        &quot;status&quot;: &quot;queued&quot;<br>    }</pre><p>The frontend is plain HTML, CSS, and JavaScript. No React. No build step.</p><p>A file drop zone, language and model selectors, a progress bar that smoothly interpolates between backend updates, and an editable text area for the result.</p><p>You can <strong>record audio</strong> directly from your microphone as well, with a live waveform visualisation.</p><p>When transcription finishes, a synthesised chime plays — a small touch, but surprisingly useful when you have queued several files and walked away. The whole thing mounts as a static directory on the FastAPI app:</p><pre>app.mount(&quot;/&quot;, StaticFiles(directory=&quot;static&quot;, html=True), name=&quot;static&quot;)</pre><p>API routes are registered before the static mount, so:</p><ul><li>/v1/transcribe</li><li>/v1/jobs/{id}</li></ul><p>are handled by FastAPI whilst everything else falls through to index.html.</p><p>I also kept the command-line tools as thin wrappers over the same core/ package. <strong>If you prefer working in the terminal:</strong></p><pre>python transcribe.py -i meeting.mp3<br>python transcribe.py -i ~/recordings/ -l en<br>python diarize.py -i interview.mp3 -s 2</pre><h3>What It Actually Performs Like</h3><p>On an AMD Ryzen 7 PRO with 14 GB of RAM — a mid-range machine — the large-v3-turbo model processes audio at roughly 2x real time.</p><p>A ten-minute recording takes about five minutes. An hour-long meeting takes about thirty minutes. This is not instant.</p><p>Cloud services with GPU backends return results in seconds. If you need sub-minute turnaround, local CPU transcription will not match that.</p><p>But for the actual use cases that matter — transcribing yesterday’s meeting, processing a batch of interview recordings, converting voice messages to text — 2x real time is perfectly workable.</p><p>You queue the files, do something else, and come back to finished transcripts. If you need speed over accuracy, the smaller models are an option.</p><ul><li>medium runs at roughly 4x real time</li><li>small runs at roughly 7x real time</li></ul><p>For English-language content with clear audio, medium will often give you results close to large-v3-turbo at significantly higher speed.</p><h3>Deploying on Your Own Server</h3><p>Local transcription covers the laptop use case.<br>But what if you want to transcribe from your phone? Or share access with a small team whilst keeping data off third-party infrastructure?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N0h2EMvs1-JNpEOp7SxiJg.jpeg" /></figure><p>The concept is straightforward:<br>You run Edgescribe on a VPS you control, put it behind Nginx with HTTPS, and add an API key.</p><p>The result is functionally identical to TurboScribe:</p><ul><li>open a URL</li><li>upload a file</li><li>get a transcript</li></ul><p>Except the server is yours, the data stays on your infrastructure, and there are no usage limits. For a single user or small household, a Hetzner CPX41 (8 vCPU, 16 GB RAM) is a comfortable setup.</p><p>The 16 GB RAM figure is important: the model needs around 6 GB, and you want headroom for the OS and concurrent requests.</p><p>The <strong>Dockerfile</strong> is straightforward:</p><pre>FROM python:3.11-slim<br>RUN apt-get update &amp;&amp; apt-get install -y ffmpeg<br>WORKDIR /app<br>COPY requirements.txt .<br>RUN pip install -r requirements.txt<br>COPY . .<br>EXPOSE 8000<br>CMD [&quot;python&quot;, &quot;api.py&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;]</pre><pre>docker compose up -d</pre><p>For HTTPS and domain access, the Nginx configuration needs two things that are not immediately obvious if you have not done this before.</p><p>First, client_max_body_size needs to be large enough for audio files - I set it to 500 MB.</p><p>Second, proxy_read_timeout needs to be much longer than the default 60 seconds, because transcription requests take minutes, not milliseconds.</p><p>Without this, Nginx will kill the connection before the transcription finishes.</p><pre>server {<br>    listen 443 ssl;<br>    server_name transcribe.yourdomain.com;<br>    client_max_body_size 500M;<br>    location / {<br>        proxy_pass http://localhost:8000;<br>        proxy_read_timeout 3600;<br>        proxy_set_header Host $host;<br>        proxy_set_header X-Real-IP $remote_addr;<br>    }<br>}</pre><pre>certbot --nginx -d transcribe.yourdomain.com</pre><p>Let’s Encrypt via Certbot provides a free TLS certificate.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YAqRMvEqMwf8jOLdL8joDw.jpeg" /></figure><p>For access control, the API accepts an --api-key flag or reads from the EDGESCRIBE_API_KEY environment variable.</p><p>Every request then requires an X-API-Key header.</p><p>What you end up with is your own transcription endpoint, reachable from anywhere, using only your infrastructure.</p><p><em>You know exactly where the data is.<br>You can delete it.<br>You control the retention policy.<br>If the service shuts down, your transcription capability does not disappear.</em></p><h3>Why This Matters</h3><p>The argument for keeping audio on your own machine is not primarily about paranoia towards tech companies.</p><p>It is about <strong>professional</strong> <strong>responsibility</strong> and <strong>basic data hygiene</strong>.</p><p>If you work in a field where confidentiality matters — legal, medical, financial, journalistic — uploading sensitive recordings to a third-party service is not a neutral choice. It is a decision with liability implications that most people make by default because the SaaS option is frictionless and the local option requires setup.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eSbxpQrnZmq3rx1FuYyE4g.jpeg" /></figure><p>We are at a point where consumer hardware is genuinely capable of running state-of-the-art speech recognition offline, at accuracy levels that match what the paid services offer.</p><p>The gap between local and cloud is mostly about latency, not quality.<br>For transcription — an inherently asynchronous task — the latency gap matters less than people assume.<br>Your voice recordings contain conversations you had in confidence.<br>The question of where those recordings are processed and stored is worth at least as much attention as the question of whether the transcript is accurate.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n2_dLT3GByrmXxvJ-QDDbQ.jpeg" /></figure><p>With the right setup, you do not have to choose between quality and control. You can have both.</p><p><em>Edgescribe is open source.<br>Repository: </em><a href="http://github.com/mikebionic/edgescribe"><em>github.com/mikebionic/edgescribe</em></a><em><br>Stack: faster-whisper (SYSTRAN), FastAPI, SpeechBrain, Silero VAD, FFmpeg.<br>Runs on Python 3.9+, Linux, macOS, Windows (WSL2). No GPU required.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a103895db2eb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Как легализовать документы об образовании: пошаговый гайд.]]></title>
            <link>https://medium.com/@mecreate/%D0%BA%D0%B0%D0%BA-%D0%BB%D0%B5%D0%B3%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C-%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B-%D0%BE%D0%B1-%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B8-%D0%B2-%D1%82%D1%83%D1%80%D0%BA%D0%BC%D0%B5%D0%BD%D0%B8%D1%81%D1%82%D0%B0%D0%BD%D0%B5-%D0%BF%D0%BE%D1%88%D0%B0%D0%B3%D0%BE%D0%B2%D1%8B%D0%B9-%D0%B3%D0%B0%D0%B9%D0%B4-53712306d430?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/53712306d430</guid>
            <category><![CDATA[учеба]]></category>
            <category><![CDATA[легализация-документов]]></category>
            <category><![CDATA[образование]]></category>
            <category><![CDATA[сертификация]]></category>
            <category><![CDATA[заверение]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Thu, 23 Apr 2026 13:48:31 GMT</pubDate>
            <atom:updated>2026-04-23T14:11:29.828Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Личный опыт прохождения всей Административной процедуры — от университета до МИДа — при подаче на магистратуру.</em></p><blockquote>Автор не является юристом или официальным представителем каких-либо государственных органов. Информация в статье отражает личный опыт на момент прохождения процедуры и может не соответствовать актуальным требованиям. За официальной информацией обращайтесь напрямую в учреждения.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CmKzLGuKTwFoo8j-88TzJQ.png" /></figure><h3>Зачем это нужно?</h3><p>При подаче на магистратуру от вас потребуют <strong>легализованные (сертифицированные) копии</strong> документов об образовании: диплом, транскрипт (выписка с оценками), аттестат о среднем образовании.<br>Без печатей трёх инстанций — нотариата, Министерства юстиции и МИДа — ваши документы не примут.</p><p>Мой транскрипт был на английском языке. <strong>Нотариальное заверение в делается только на государственном языке</strong>, и это добавило целый дополнительный этап.</p><h3>Что вам понадобится</h3><p>Прежде чем начинать — соберите всё заранее:</p><p><strong>Паспорт</strong> — государственный паспорт (оригинал, обязательно при себе на каждом этапе)<br><strong>Документы</strong> — диплом, транскрипт, аттестат (оригиналы)<br><strong>Копии</strong> — по 1–2 копии каждого документа <br><strong>Банковская карта</strong> — для оплаты в МИДе (только безналичный расчёт)<br><strong>Время</strong> — минимум 4–5 рабочих дней, комфортно — неделя</p><blockquote><strong><em>Важно:</em></strong><em> Все действия должен выполнять </em><strong><em>лично владелец документов</em></strong><em>. Нельзя отправить кого-то вместо себя.</em></blockquote><h3>Маршрут: 5 этапов за 4–7 дней</h3><pre>[Университет] → [Нотариат] → [Минюст] → [МИД] → [Перевод]<br>  прошивка +     заверение    печать      финал    при необх.<br>  перевод        документов   Минюста     легализ.</pre><h3>Примерный таймлайн</h3><pre>День 1–2  │ Университет — перевод транскрипта и прошивка<br>День 3    │ Нотариат — заверение документов<br>День 4    │ Минюст — сдать документы<br>День 5    │ Минюст — забрать с печатью (утром) → сразу в МИД<br>День 6    │ МИД — забрать готовые документы (утром до обеда)</pre><h3>Этап 1. Университет — прошивка и верификация</h3><p><strong>Цель:</strong> получить прошитый транскрипт с печатью вуза и официальный перевод на государственный язык</p><p>Если ваш транскрипт на иностранном языке (как у меня — на английском):</p><ol><li><strong>Переведите транскрипт на государственный язык</strong> — это обязательное требование для нотариального заверения</li><li><strong>Отнесите перевод в деканат</strong> — отдел образования должен сверить перевод с оригиналом, поставить штамп и подпись декана</li><li><strong>Убедитесь, что транскрипт прошит</strong> — документ должен быть прошит университетом <em>до</em> похода к нотариусу</li></ol><p>На выходе: прошитый оригинал транскрипта + верифицированный перевод с печатью вуза.</p><h3>Этап 2. Нотариат — заверение документов</h3><p><strong>Цель:</strong> нотариально заверить все документы.</p><p><strong>Порядок действий:</strong></p><ol><li>Приходите с оригиналами и копиями, берёте талон электронной очереди</li><li>Нотариус сверяет копии с оригиналами и ставит печати</li></ol><p><strong>Оплата:</strong> нотариальные услуги платные.</p><h3>Лайфхак</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1Jt5mR4k1JgOEkaaNIQKAg.png" /></figure><p>Если нужно вдруг идти по срочным делам, перед выходом можно взять талон и вернуться к нужному времени.</p><h3>Этап 3. Министерство юстиции — легализация</h3><p><strong>Цель:</strong> получить печать Министерства юстиции.</p><p><strong>Где:</strong> здание Министерства юстиции (<em>Adalat ministrligi</em>), проспект Арчабил. Удобный подъезд со стороны проспекта Чандыбиль.</p><p><strong>Порядок действий:</strong></p><ol><li>Предъявите паспорт на входе и пройдите в <strong>отдел легализации</strong></li><li>Предоставьте нотариально заверенные документы</li><li>Документы заберут <strong>на один рабочий день</strong></li><li>На следующий день <strong>утром</strong> приходите и забираете документы с печатью Минюста</li></ol><p><strong>Оплата:</strong> бесплатно.</p><blockquote><strong><em>Тайминг:</em></strong><em> забрав документы утром, сразу езжайте в МИД.</em></blockquote><h3>Этап 4. МИД — финальная сертификация</h3><p><strong>Цель:</strong> получить финальную печать Министерства иностранных дел.</p><p><strong>Где:</strong> здание МИД.</p><p><strong>Порядок действий:</strong></p><ol><li>Предъявите паспорт на входе, возьмите электронный талон</li><li>Предоставьте легализованные документы + оригиналы + паспорт</li><li><strong>Оплатите</strong> обработку документов — <strong>банковской картой</strong></li><li>Документы заберут <strong>на один рабочий день</strong></li><li>На следующий день <strong>утром до обеда</strong> приходите с паспортом и забираете готовые сертифицированные документы</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dFUSL-ZXKBDjvcNx4kWLnQ.png" /></figure><h3>Этап 5. Перевод (при необходимости)</h3><p>После получения всех печатей — ваши документы <strong>полностью легализованы</strong>. Если приёмная комиссия требует документы на определённом языке (например, английском):</p><ol><li>Переведите легализованные страницы (включая страницы с печатями) на нужный язык</li><li>Соберите всё в единый <strong>PDF-файл</strong></li><li>Отправляйте в университет</li></ol><h3>Чеклист перед стартом</h3><ul><li>[ ] Оригиналы всех документов об образовании собраны</li><li>[ ] Сделаны копии (минимум по 1 шт. каждого)</li><li>[ ] Паспорт при себе</li><li>[ ] Банковская карта</li><li>[ ] Перевод транскрипта готов (если документы на иностранном языке)</li><li>[ ] Транскрипт прошит университетом</li></ul><h3>Словарь терминов</h3><p><em>Adalat ministrligi — </em>Министерство юстиции — Ministry of Justice<br><em>Daşary işler ministrligi — </em>МИД — Ministry of Foreign Affairs<br><em>Notarius — </em>Нотариат / нотариус — Notary<br><em>Kanunlaşdyrma — </em>Легализация — Legalization<br><em>Tassyklama — </em>Заверение / верификация — Certification</p><p><em>Этот гайд основан на личном опыте прохождения процедуры легализации документов. Процедуры могут меняться — уточняйте актуальные требования в учреждениях.</em></p><p>#Образование #Легализация #Документы #Магистратура #Учёба #ЛичныйОпыт</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=53712306d430" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Turned a 75-Minute Podcast Into 18 Instagram Reels in Under 10 Minutes Using AI]]></title>
            <link>https://medium.com/@mecreate/how-i-turned-a-75-minute-podcast-into-18-instagram-reels-in-under-10-minutes-using-ai-cfdd3c319640?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/cfdd3c319640</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[video-editing]]></category>
            <category><![CDATA[anthropic-claude]]></category>
            <category><![CDATA[ffmpeg]]></category>
            <category><![CDATA[blogging]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Wed, 25 Mar 2026 14:45:05 GMT</pubDate>
            <atom:updated>2026-03-25T15:14:25.043Z</atom:updated>
            <content:encoded><![CDATA[<p>#ai #ai #ai #ai #ai 🤪</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/778/1*6WRm0ez4wj5rHGtgpEDVOQ.png" /></figure><p>I recently published a podcast interview on YouTube. It’s a long one — about 75 minutes. A deep, meaningful conversation with the head of Uzbekistan’s largest IT community.<br>Here’s the video link: <a href="https://youtube.com/shorts/tFBFzx5j94s?feature=share">https://youtube.com/shorts/tFBFzx5j94s?feature=share</a></p><p>The content is great. But let’s be honest — nobody is watching a full 75-minute video on Instagram.</p><p>What I needed were short clips. 30 to 60 second Reels. The kind that grab attention instantly and make people curious enough to check out the full episode.</p><p>Normally, this would mean opening a video editor, scrolling through the entire timeline, manually selecting moments, exporting clips one by one. For 15–20 clips, that’s easily 2–3 hours of work.</p><p>This time, it took me less than 10 minutes.</p><p>Here’s exactly what I did.</p><h3>The Setup</h3><p>You only need three things:</p><ol><li>A transcript with timestamps (YouTube captions work fine)</li><li>ffmpeg (a free command-line tool)</li><li>An AI assistant</li></ol><p><strong>No Premiere Pro. No DaVinci. No timeline editing. Just text and a terminal.</strong></p><h3>Step 1: Get the Transcript</h3><p>Everything starts with the transcript. Mine looked something like this:</p><pre>Chapter 4: How to build the first team with zero budget  <br>4:38  <br>So, we started as a very small movement. There were eight of us.  <br>4:46  <br>At first we gathered 30 volunteers, and eight stayed...</pre><p>Each line has a timestamp. That’s all the AI needs. It doesn’t actually watch the video — it understands everything from text.</p><h3>Step 2: Let AI Do the Heavy Lifting</h3><p>I fed the full transcript into an AI assistant and gave it a simple instruction:</p><p><em>“Find short 30–60 second moments that would work well as Instagram Reels and make people want to watch the full episode.”</em></p><p>Within seconds, it scanned the entire conversation and suggested clips with exact timestamps and hooks.</p><p>At first, it gave me solid but pretty standard moments — interesting, but not very scroll-stopping.</p><p>So I pushed it:</p><p><em>“These aren’t strong enough. Find more emotional, surprising, or funny moments.”</em></p><p>That’s when things got good.</p><p>Instead of just “information,” it started picking moments with:</p><ul><li>Humor</li><li>Conflict</li><li>Personal stories</li><li>Unexpected twists</li></ul><p>For example:</p><ul><li>A story where someone tries to save a drowning person but grabs a ball instead</li><li>Copying another brand’s logo on purpose so people confuse them</li><li>Turning down a higher-paying job abroad to stay with the community</li><li>A strict rule: miss once — you’re out</li><li>A girl solving a family restriction by bringing her brother along</li></ul><p>That’s when it clicked:</p><p><strong>Emotion beats information. Every time.</strong></p><h3>Step 3: Generate the Clips Automatically</h3><p>For each selected moment, the AI generated an ffmpeg command like this:</p><pre>ffmpeg -y -ss 00:04:38 -to 00:05:50 \<br>  -i podcast.mp4 \<br>  -c:v libx264 -c:a aac \<br>  -avoid_negative_ts make_zero \<br>  clip_01.mp4</pre><p>Even better — it didn’t run them one by one. It launched all the cuts in parallel.</p><p>So instead of waiting 10–15 minutes, everything rendered in about 2–3 minutes.</p><h3>Step 4: Build a Trailer Mix</h3><p>Then I asked for a short trailer — a fast-paced mix of the best moments.</p><p>The process was simple:</p><ol><li>Cut small 5–8 second segments</li><li>Combine them into one video</li></ol><p>The result: a 60-second highlight reel that feels dynamic and engaging.</p><h3>Final Output</h3><p>In one session, without opening any editing software, I got:</p><ul><li>18 short clips (ready for Reels)</li><li>2 trailer videos</li><li>10 caption ideas with hashtags</li></ul><p>Total time spent: about 10 minutes Rendering time: around 3 minutes</p><h3>What I Learned</h3><p><strong>1. The transcript is everything</strong> No timestamps — no workflow. This is the foundation.</p><p><strong>2. AI doesn’t need video</strong> Text is enough. It understands structure, tone, and storytelling surprisingly well.</p><p><strong>3. The first result is rarely the best</strong> Iteration matters. The second or third pass is where quality jumps.</p><p><strong>4. Emotion &gt; information</strong> Stats don’t stop the scroll. Stories do.</p><p><strong>5. ffmpeg is insanely powerful</strong> It replaces hours of manual editing with a few commands — and AI can handle it easily.</p><h3>Try It Yourself</h3><p>All you need:</p><ul><li>A video</li><li>A timestamped transcript</li><li>ffmpeg installed</li><li>Any AI assistant</li></ul><p>Give it the transcript. Ask for clips. Refine the results. Export.</p><p>That’s it.<br>What used to take hours is now just a conversation.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cfdd3c319640" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Built a Synchronized Video Player in a Weekend Because Watch Parties Shouldn’t Be This Hard]]></title>
            <link>https://medium.com/@mecreate/i-built-a-synchronized-video-player-in-a-weekend-because-watch-parties-shouldnt-be-this-hard-2a0339a163c1?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/2a0339a163c1</guid>
            <category><![CDATA[streaming]]></category>
            <category><![CDATA[golang]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[cooperation]]></category>
            <category><![CDATA[websocket]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Tue, 27 Jan 2026 14:14:01 GMT</pubDate>
            <atom:updated>2026-01-27T14:39:42.382Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ik0PBW5TDifO_Z1U1AUHiA.png" /><figcaption>Coopcinema lobby and room</figcaption></figure><blockquote><strong>Try Yourself: </strong><a href="https://coopcinema.onrender.com/"><strong>https://coopcinema.onrender.com/</strong></a></blockquote><p>You know that moment when you’re on a call with a friend, you both open the same YouTube video, and someone says “okay, 3… 2… 1… play!” — and you’re still 4 seconds apart the entire time?</p><p>That was my pain. A stupid, small, annoying pain. And it turned into a weekend project.</p><h3>The Problem</h3><p>I live far from some of my closest friends. We like watching stuff together — movie trailers, conference talks, random YouTube rabbit holes at 2 AM. Every existing solution had a catch:</p><ul><li><strong>Teleparty / Watch2Gether</strong> — browser extensions, account walls, or limited to Netflix/YouTube</li><li><strong>Discord screen share</strong> — kills the video quality, eats bandwidth, and the person sharing can’t resize their window without everyone suffering</li><li><strong>“Ready? 3… 2… 1…”</strong> — never actually works</li></ul><p>I wanted something dead simple. Open a link, paste a YouTube URL, and you’re watching together. No installs, no accounts, no extensions.</p><h3>The Idea</h3><p>What if the server did almost nothing?</p><p>Seriously. What if the server was just a dumb relay — a WebSocket hub that receives a JSON message from one client and broadcasts it to everyone else in the room? No video processing. No transcoding. No storage. No authentication. Just: <em>“hey, someone pressed play at timestamp 42.5 seconds — pass it on.”</em></p><p>The clients handle everything. Each browser loads the video independently (YouTube iframe, Vimeo embed, local file, whatever), and they just keep each other in sync through tiny JSON messages.</p><p>That’s it. That’s the architecture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7L1JYJlYUlmphOO8e_cM3A.png" /></figure><h3>One Weekend, Go + Vanilla JS</h3><p>The backend is about 200 lines of Go. A WebSocket hub manages rooms. Clients connect, join a room, and every message they send gets broadcast to everyone else in that room (except themselves). The server doesn’t know or care what a “play” or “pause” means. It just relays.</p><pre>func (h *Hub) Broadcast(msg models.Message, sender *models.Client) {<br>    h.mu.RLock()<br>    defer h.mu.RUnlock()<br>    room := h.rooms[sender.RoomCode]<br>    for client := range room {<br>        if client != sender {<br>            client.Send &lt;- msg<br>        }<br>    }<br>}</pre><p>The frontend is vanilla JavaScript. No React, no Vue, no build step. One HTML file, one JS file, one CSS file. The JS handles:</p><ul><li>Detecting what kind of URL you pasted (YouTube? Vimeo? Twitch? Direct .mp4 link?)</li><li>Loading the right player (YouTube IFrame API, Vimeo SDK, Twitch Embed, or a plain &lt;video&gt; tag)</li><li>Intercepting play/pause/seek events and sending them over WebSocket</li><li>Receiving those events from peers and applying them locally</li></ul><p>The sync logic is surprisingly simple. When someone presses play, the client sends:</p><pre>{<br>  &quot;type&quot;: &quot;play&quot;,<br>  &quot;timestamp&quot;: 42.5,<br>  &quot;sentAt&quot;: 1706000000000<br>}</pre><p>The receiver checks: am I more than 0.5 seconds off from 42.5? If yes, seek first. Then play. The sentAt field lets receivers estimate network latency and adjust the seek target by half the round-trip time. It&#39;s not perfect, but it&#39;s close enough that you don&#39;t notice.</p><h3>Beyond the Basics</h3><p>Once the core sync worked, the fun features came fast:</p><p><strong>Chat with toast popups</strong> — a slide-in chat sidebar with notification toasts that stack up when the chat is closed. Click a toast, chat opens. No message missed.</p><p><strong>Emoji reactions</strong> — click an emoji and it floats up over the video with your name underneath it. Everyone sees it. Stupid simple, genuinely fun during a tense movie scene.</p><p><strong>Theater Fullscreen</strong> — YouTube’s native fullscreen hides everything — no chat, no reactions, no toasts. So I built a custom fullscreen that uses the Fullscreen API on a wrapper div containing the video, reactions, chat, and controls. Everything stays visible.</p><p><strong>Buffering sync</strong> — if one person’s connection stutters, everyone pauses automatically. When they’re ready, everyone resumes. No one misses a scene because their friend has slower internet.</p><p><strong>Host mode</strong> — the room creator can enable host mode, where only the host’s playback controls sync. Everyone else just watches. Click someone’s badge to transfer host.</p><h3>DevFest Ashgabat 2025</h3><p>I used Co-op Cinema as the demo project for my talk at <a href="https://gdg.community.dev/events/details/google-gdg-ashgabat-presents-devfest-ashgabat-2025-a-glimpse-into-the-future-of-tech/">DevFest Ashgabat 2025</a> — <em>“Code. Build. Deploy. Repeat. GitHub Pipelines &amp; Cloud Deployments for Go.”</em></p><p>The talk was about CI/CD pipelines with GitHub Actions and deploying Go apps to production. Co-op Cinema was the app we built the pipeline around — from feature branches to automated builds to cloud deployment. About 690 people attended.</p><p>You can watch the full session on <a href="https://youtu.be/yfzkSp3TlT4">YouTube</a> or check the <a href="https://docs.google.com/presentation/d/14vg2_i91qOmdQ1yzyZTImcgbh_ejWJ8Oi3I729aS3wA/edit?usp=sharing">slides</a>.</p><p>The irony of demonstrating a deployment pipeline for a watch party app — during a live event where hundreds of people were watching together in the same room — was not lost on me.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RVQM9MiuOl1eOKB18z1qjA.png" /></figure><h3>Deploying It (for Free)</h3><p>The whole thing is Dockerized. One command:</p><pre>cp .env.example .env<br>docker compose up -d</pre><p>For public access, I deployed to <a href="https://coopcinema.onrender.com/">Render</a> — free tier, Docker runtime, auto-deploys from GitHub on every push. The render.yaml blueprint means you can fork the repo and deploy in two clicks.</p><p>The Go binary is ~10MB. The Docker image is ~15MB. The server uses almost no memory or CPU because it does almost nothing. Perfect for a free tier.</p><h3>What I Learned</h3><p><strong>The dumbest server is sometimes the best server.</strong> A stateless WebSocket relay with zero business logic handles every video source, every player API, and every edge case — because the clients do all the work. The server will never need updating when I add a new video source.</p><p><strong>Vanilla JS is fine.</strong> No framework, no build step, no node_modules. The entire frontend is one HTML file, one JS file, one CSS file. It loads instantly. It works everywhere. Adding features means editing a function, not configuring a pipeline.</p><p><strong>Sync is about tolerance, not precision.</strong> You don’t need millisecond-perfect sync for a watch party. Half-second tolerance, debounced events, and a latency offset get you 95% of the way there. The remaining 5% doesn’t matter because humans can’t tell.</p><h3>Try It</h3><p>The project is open source. Fork it, deploy it, watch something with someone who’s far away.</p><p><strong>GitHub:</strong> <a href="https://github.com/mikebionic/coopcinema">coopcinema</a></p><p>Or just grab Docker and run it locally in 10 seconds:</p><pre>git clone https://github.com/mikebionic/coopcinema.git<br>cd coopcinema<br>cp .env.example .env<br>docker compose up -d</pre><p>Open localhost:8080, create a room, share the link. That&#39;s it.</p><p><em>Built with Go, gorilla/websocket, and vanilla JavaScript. No frameworks were harmed in the making of this project.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2a0339a163c1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Built a Browser-Based Beat Maker From Scratch Using Nothing But Vanilla JavaScript and the…]]></title>
            <link>https://medium.com/@mecreate/how-i-built-a-browser-based-beat-maker-from-scratch-using-nothing-but-vanilla-javascript-and-the-a87f2ee8c451?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/a87f2ee8c451</guid>
            <category><![CDATA[js]]></category>
            <category><![CDATA[audio-api]]></category>
            <category><![CDATA[weekend-project]]></category>
            <category><![CDATA[fun]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Tue, 27 Jan 2026 10:33:17 GMT</pubDate>
            <atom:updated>2026-01-27T10:34:07.590Z</atom:updated>
            <content:encoded><![CDATA[<h3>How I Built a Browser-Based Beat Maker From Scratch Using Nothing But Vanilla JavaScript and the Web Audio API</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3KtVtLHEEaThZQLfIZdktA.png" /></figure><blockquote>AI Didn’t Want to Give Me Exact MIDI Notes — It Didn’t Know It Was Dealing With a Musician :D</blockquote><p><a href="https://youtube.com/watch?v=bkmFZ_K3wP4">Video Demo</a> | <a href="https://mikebionic.github.io/webchestra/">Try It Yourself</a></p><p>There’s a moment every developer knows — the moment you discover something in the browser that makes you think “wait, you can do THAT?” For me, that moment was the <strong>Web Audio API</strong>.</p><p>I wanted to make music. Not by loading .mp3 files. Not by embedding SoundCloud players. I wanted the browser itself to <strong>synthesize sound from nothing</strong> — oscillators, noise, waveforms, frequencies. Real sound generation. No libraries, no frameworks, no external audio files.</p><p>The result is <a href="https://mikebionic.github.io/webchestra/">WebChestra</a> — a pure JavaScript, browser-based music mixer inspired by Incredibox and Sprunki Beats. Click emoji characters, layer instruments, build a beat. Zero dependencies.</p><h3>The Pain: Web Audio API Is Powerful but Unforgiving</h3><p>Let me be honest about the journey. The Web Audio API is incredibly capable — but it’s also incredibly low-level. There’s no playNote(&quot;C3&quot;) function. There&#39;s no createKickDrum(). You get raw oscillators, gain nodes, and biquad filters. You are the synthesizer.</p><h3>Pain Point #1: Everything Is Frequencies</h3><p>Want a bass note? You need to know that <strong>G2 is 98.00 Hz</strong>, <strong>C3 is 130.81 Hz</strong>, and <strong>D2 is 73.42 Hz</strong>. There’s no shortcut. You’re working with the physics of sound:</p><pre>A4 = 440 Hz (standard tuning reference)<br>Each octave = 2x the frequency<br>Each semitone = multiply by 2^(1/12) ~ 1.0595</pre><p>I manually mapped out every note frequency for both the bass (triangle wave, 73–196 Hz range) and lead synth (square wave, 392–880 Hz range). Nine notes each, painstakingly chosen to harmonize together.</p><h3>Pain Point #2: Drums Don’t Exist — You Build Them</h3><p>There’s no drum sample in the Web Audio API. A kick drum? That’s a <strong>sine wave oscillator that pitch-bends from 150 Hz down to 55 Hz in 100 milliseconds</strong>, passed through a lowpass filter at 500 Hz. A snare? <strong>White noise</strong> (literally Math.random() * 2 - 1 pumped into an audio buffer) filtered through a <strong>10 kHz bandpass filter</strong>. Hi-hat? Same white noise, but through a <strong>5 kHz highpass filter</strong> to get that sizzle.</p><pre>// This is how you make a kick drum from nothing:<br>osc.frequency.setValueAtTime(150, audioContext.currentTime);<br>osc.frequency.exponentialRampToValueAtTime(55, currentTime + 0.1);</pre><p>Every sound is built from first principles.</p><h3>Pain Point #3: Timing Is Harder Than You Think</h3><p>I used setInterval() running at 200ms per beat (effectively 300 BPM). It works — but JavaScript&#39;s event loop doesn&#39;t guarantee precise timing. Over long sessions, you can hear subtle drift. The &quot;proper&quot; solution is scheduling notes ahead of time using audioContext.currentTime, but for a fun experiment, setInterval gets the job done.</p><h3>Pain Point #4: Volume Balancing Is Trial and Error</h3><p>The lead synth at full volume obliterates everything else. I ended up with the bass at gain 0.4 and the lead at 0.05 — an 8x difference. There’s no mixing console. You tweak numbers, reload, listen, repeat. Over and over.</p><h3>The Architecture: One File, Zero Dependencies</h3><p>The entire project lives in a single index.html file. HTML structure, CSS styling, JavaScript logic — all together. Pragmatic? Yes. Production-ready? No. But it means you can literally download one file, open it in a browser, and make music.</p><h3>Five Instruments, Two Sound Engines</h3><p><strong>Melodic instruments</strong> (Bass and Lead) use oscillator nodes with predefined frequency arrays and 64-beat pattern loops:</p><ul><li><strong>Bass</strong>: Triangle wave — warm, rounded, sits underneath everything</li><li><strong>Lead</strong>: Square wave — bright, hollow, chiptune-like, cuts through the mix</li></ul><p><strong>Percussion</strong> (Kick, Snare, Hi-Hat) use generated white noise buffers filtered through biquad filters, running on an 8-beat loop:</p><ul><li><strong>Kick</strong>: Sine wave with pitch bend + lowpass filter</li><li><strong>Snare</strong>: White noise + bandpass filter at 10 kHz</li><li><strong>Hi-Hat</strong>: White noise + highpass filter at 5 kHz</li></ul><p>The pattern system encodes notes as arrays of integers — 0 means silence, 1–9 maps to a specific note index. Drums and melodic lines sync through modulo operations (currentBeat % 8 for drums, currentBeat % 64 for melodic).</p><h3>Where AI Fell Short (and Where Being a Musician Mattered)</h3><p>Here’s the part I enjoyed the most. When I tried to get AI assistance with the musical note frequencies and patterns, it was reluctant to provide <strong>exact frequency values</strong> for specific notes. It wanted to generalize, abstract, hand-wave.</p><p>But I’m a musician. I know what G2 sounds like. I know the difference between a triangle wave bass and a square wave bass. I know that a snare needs that bandpass filter crack, not a lowpass mush.</p><p><strong>The AI didn’t know it was dealing with someone who would actually listen to the output and know if it was wrong.</strong></p><p>So I did what any stubborn musician-developer would do: I looked up the frequency tables myself, tuned by ear, iterated on patterns until the groove felt right, and balanced volumes until the mix breathed.</p><p>The lesson: AI is a tool, not a producer. It can scaffold code, but it can’t tell you if the beat slaps.</p><h3>What Could Be Better</h3><p>I’m honest about the rough edges. Here’s what I’d improve:</p><ul><li><strong>Proper audio scheduling</strong> — Replace setInterval with Web Audio API clock-based scheduling for sample-accurate timing</li><li><strong>Tempo control</strong> — Currently hardcoded at 300 BPM, should be adjustable</li><li><strong>Volume sliders</strong> — Per-instrument mixing instead of hardcoded gain values</li><li><strong>MIDI input support</strong> — navigator.requestMIDIAccess() would let you plug in a keyboard</li><li><strong>Pattern editor UI</strong> — Visual grid to edit beat patterns instead of changing code</li><li><strong>Code modularization</strong> — Break the monolithic HTML file into proper modules</li><li><strong>More waveforms and effects</strong> — Reverb, delay, distortion using Web Audio’s ConvolverNode and WaveShaperNode</li><li><strong>Mobile optimization</strong> — Touch interactions and responsive layout tweaks</li><li><strong>Save/load compositions</strong> — Export your mix as JSON or even render to WAV using OfflineAudioContext</li></ul><h3>Try It</h3><p><a href="https://mikebionic.github.io/webchestra/">WebChestra — Live Demo</a><br><a href="https://youtube.com/watch?v=bkmFZ_K3wP4">Watch the Video Demo</a><br><a href="https://github.com/mikebionic/webchestra">Source Code on GitHub</a></p><p>No installs. No sign-ups. Just open your browser, click some emojis, and make a beat.</p><p><em>Built with nothing but curiosity, a browser, and stubbornness.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a87f2ee8c451" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kubernetes Crash Course: Your Essential Cheatsheet]]></title>
            <link>https://medium.com/@mecreate/kubernetes-crash-course-your-essential-cheatsheet-c1698b67c606?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/c1698b67c606</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[selfhost]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[crash-course]]></category>
            <category><![CDATA[k8s]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Sat, 13 Dec 2025 13:47:03 GMT</pubDate>
            <atom:updated>2025-12-13T13:47:03.829Z</atom:updated>
            <content:encoded><![CDATA[<p><em>A no-nonsense, practice-first guide to self-hosted Kubernetes with real deployments</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L_QRy-cJr1KAR48nUUtJHA.png" /></figure><p>Stop reading theory. Start building. This is the fastest path from zero to a working Kubernetes cluster with a real application running.</p><h3>What You’ll Build</h3><ul><li>Self-hosted Kubernetes cluster using <strong>kubeadm</strong></li><li>Control plane + worker nodes</li><li>Real <strong>FastAPI</strong> application in Docker</li><li>Deployments with replicas and autoscaling</li></ul><p><strong>Time to complete:</strong> 30–45 minutes</p><h3>Infrastructure Requirements</h3><p><strong>Control Plane (Master):</strong></p><ul><li>Ubuntu Linux</li><li>6 GB RAM, 4 CPU</li></ul><p><strong>Worker Nodes:</strong></p><ul><li>Ubuntu Linux</li><li>4 GB RAM, 2 CPU</li></ul><p><strong>All Nodes Must Have:</strong></p><ul><li>Unique hostname</li><li>Network connectivity</li><li>Swap disabled</li><li>2+ CPU cores</li></ul><h3>Step 1: Disable Swap (All Nodes)</h3><p>Kubernetes refuses to run with swap enabled.</p><pre>sudo swapoff -a</pre><p>Make it permanent by removing swap entries from /etc/fstab.</p><h3>Step 2: Install Kubernetes Tools (All Nodes)</h3><p>Install dependencies:</p><pre>sudo apt-get update<br>sudo apt-get install -y apt-transport-https ca-certificates curl gpg</pre><p>Add Kubernetes signing key:</p><pre>sudo mkdir -p -m 755 /etc/apt/keyrings<br>curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.34/deb/Release.key | \<br>  sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg</pre><p>Add Kubernetes repository:</p><pre>echo &#39;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.34/deb/ /&#39; | \<br>  sudo tee /etc/apt/sources.list.d/kubernetes.list</pre><p>Install kubeadm, kubelet, kubectl:</p><pre>sudo apt-get update<br>sudo apt-get install -y kubelet kubeadm kubectl<br>sudo apt-mark hold kubelet kubeadm kubectl<br>sudo systemctl enable --now kubelet</pre><h3>Step 3: Initialize Control Plane (Master Node Only)</h3><p>Check your network interface:</p><pre>ip route show</pre><p>Initialize the cluster:</p><pre>sudo kubeadm init</pre><p>Configure kubectl for your user:</p><pre>mkdir -p $HOME/.kube<br>sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config<br>sudo chown $(id -u):$(id -g) $HOME/.kube/config</pre><p><strong>Save the </strong><strong>kubeadm join command</strong> printed at the end. You&#39;ll need it for worker nodes.</p><h3>Step 4: Install Pod Network (Master Node)</h3><p>Without this, pods cannot communicate.</p><pre>kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml</pre><p>Verify:</p><pre>kubectl get nodes<br>kubectl get pods -A</pre><h3>Step 5: Join Worker Nodes</h3><p>On each worker node, run the kubeadm join command from Step 3:</p><pre>sudo kubeadm join &lt;master-ip&gt;:6443 --token &lt;token&gt; \<br>  --discovery-token-ca-cert-hash sha256:&lt;hash&gt;</pre><p>Verify from master:</p><pre>kubectl get nodes</pre><h3>Step 6: Build Your Application</h3><h3>Create main.py:</h3><pre>from fastapi import FastAPI, Request<br>import random<br>import socket<br><br>app = FastAPI()<br><br>def get_server_ip():<br>    try:<br>        hostname = socket.gethostname()<br>        return socket.gethostbyname(hostname)<br>    except:<br>        return &quot;Unknown&quot;<br><br>@app.get(&quot;/&quot;)<br>async def index():<br>    return {<br>        &quot;message&quot;: &quot;Salam Dünýä!&quot;,<br>        &quot;server_ip&quot;: get_server_ip()<br>    }<br><br>@app.get(&quot;/ping&quot;)<br>async def ping():<br>    return {&quot;response&quot;: &quot;pong&quot;}<br><br>@app.get(&quot;/client&quot;)<br>async def client_info(request: Request):<br>    return {<br>        &quot;client_ip&quot;: request.client.host,<br>        &quot;headers&quot;: dict(request.headers)<br>    }<br><br>@app.get(&quot;/random&quot;)<br>async def random_number():<br>    return {&quot;number&quot;: random.randint(1, 1_000_000)}</pre><h3>Create requirements.txt:</h3><pre>fastapi<br>uvicorn[standard]</pre><h3>Create Dockerfile:</h3><pre>FROM python:3.12-slim<br><br>WORKDIR /app<br><br>COPY requirements.txt .<br><br>RUN pip install --no-cache-dir -r requirements.txt<br><br>COPY . .<br><br>EXPOSE 8000<br><br>CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]</pre><h3>Build and push:</h3><pre>docker build -t yourregistry.com/yourapp:latest .<br>docker push yourregistry.com/yourapp:latest</pre><p><em>Replace </em><em>yourregistry.com with Docker Hub or your private registry.</em></p><h3>Step 7: Deploy to Kubernetes</h3><h3>Simple Deployment (1 pod)</h3><p>Create deployment-simple.yaml:</p><pre>apiVersion: apps/v1<br>kind: Deployment<br>metadata:<br>  name: app-deployment<br>spec:<br>  selector:<br>    matchLabels:<br>      app: myapp<br>  template:<br>    metadata:<br>      labels:<br>        app: myapp<br>    spec:<br>      containers:<br>      - name: myapp-container<br>        image: yourregistry.com/yourapp:latest<br>        ports:<br>        - containerPort: 8000</pre><p>Apply:</p><pre>kubectl apply -f deployment-simple.yaml</pre><h3>Step 8: Deployment with Replicas</h3><p>Create deployment-replicas.yaml:</p><pre>apiVersion: apps/v1<br>kind: Deployment<br>metadata:<br>  name: app-deployment-replicas<br>spec:<br>  replicas: 3<br>  selector:<br>    matchLabels:<br>      app: myapp-replicas<br>  template:<br>    metadata:<br>      labels:<br>        app: myapp-replicas<br>    spec:<br>      containers:<br>      - name: myapp-container<br>        image: yourregistry.com/yourapp:latest<br>        ports:<br>        - containerPort: 8000</pre><p>Apply:</p><pre>kubectl apply -f deployment-replicas.yaml</pre><h3>Step 9: Autoscaling (HPA)</h3><p>Create deployment-autoscaling.yaml:</p><pre>apiVersion: apps/v1<br>kind: Deployment<br>metadata:<br>  name: app-deployment-autoscaling<br>spec:<br>  replicas: 2<br>  selector:<br>    matchLabels:<br>      app: myapp-autoscaling<br>  template:<br>    metadata:<br>      labels:<br>        app: myapp-autoscaling<br>    spec:<br>      containers:<br>      - name: myapp-container<br>        image: yourregistry.com/yourapp:latest<br>        ports:<br>        - containerPort: 8000<br>---<br>apiVersion: autoscaling/v2<br>kind: HorizontalPodAutoscaler<br>metadata:<br>  name: app-autoscaler<br>spec:<br>  scaleTargetRef:<br>    apiVersion: apps/v1<br>    kind: Deployment<br>    name: app-deployment-autoscaling<br>  minReplicas: 2<br>  maxReplicas: 6<br>  metrics:<br>  - type: Resource<br>    resource:<br>      name: cpu<br>      target:<br>        type: Utilization<br>        averageUtilization: 70</pre><p>Apply:</p><pre>kubectl apply -f deployment-autoscaling.yaml</pre><p>Check autoscaling status:</p><pre>kubectl get hpa</pre><h3>Step 10: Access Your App</h3><h3>Port forwarding (quick test):</h3><pre>kubectl get pods<br>kubectl port-forward pod/&lt;pod-name&gt; 6789:8000</pre><p>Open browser: <a href="http://localhost:6789/">http://localhost:6789</a></p><h3>Essential Commands Cheatsheet</h3><h3>Cluster Status</h3><pre>kubectl get nodes<br>kubectl get pods -A<br>kubectl cluster-info</pre><h3>Deployments</h3><pre>kubectl get deployments<br>kubectl describe deployment &lt;name&gt;<br>kubectl delete deployment &lt;name&gt;</pre><h3>Pods</h3><pre>kubectl get pods<br>kubectl logs &lt;pod-name&gt;<br>kubectl describe pod &lt;pod-name&gt;<br>kubectl exec -it &lt;pod-name&gt; -- /bin/bash</pre><h3>Autoscaling</h3><pre>kubectl get hpa<br>kubectl describe hpa &lt;name&gt;</pre><h3>Cleanup</h3><pre>kubectl delete deployment --all<br>kubectl delete hpa --all</pre><h3>Reset Cluster</h3><pre>sudo kubeadm reset</pre><h3>Port Reference</h3><p>Component Port Kubernetes API 6443 etcd 2379–2380 Kubelet API 10250 kube-scheduler 10259 kube-controller 10257</p><p>Check port availability:</p><pre>nc 127.0.0.1 6443 -zv -w 2</pre><h3>Troubleshooting</h3><p><strong>Pods not starting?</strong></p><pre>kubectl describe pod &lt;pod-name&gt;<br>kubectl logs &lt;pod-name&gt;</pre><p><strong>Network issues?</strong></p><pre>kubectl get pods -n kube-system<br># Check Calico pods are running</pre><p><strong>Node not joining?</strong></p><pre># On worker node:<br>sudo kubeadm reset<br># Then rejoin with kubeadm join command</pre><p><strong>Forgot join command?</strong></p><pre># On master:<br>kubeadm token create --print-join-command</pre><p><strong>No abstractions. No magic. Just real Kubernetes.</strong></p><p>Once you understand this manual setup, managed services like EKS, GKE, and AKS become trivial.</p><h3>Next Steps</h3><ul><li>Set up <strong>Services</strong> and <strong>LoadBalancers</strong></li><li>Configure <strong>Ingress</strong> for routing</li><li>Add <strong>persistent storage</strong> with PVs/PVCs</li><li>Implement <strong>monitoring</strong> with Prometheus</li><li>Set up <strong>logging</strong> with ELK stack</li><li>Learn <strong>Helm</strong> for package management</li></ul><h3>Bookmark This Guide</h3><p>This is your reference for Kubernetes essentials. Keep it handy, and refer back whenever you need to spin up a cluster or deploy an application.</p><p>Happy clustering! 🚀</p><p><em>If you found this useful, follow for more practical DevOps guides!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c1698b67c606" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PostgreSQL for DevOps: Production Setup and Operations]]></title>
            <link>https://medium.com/@mecreate/postgresql-for-devops-production-setup-and-operations-2630f41ce779?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/2630f41ce779</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[essentials]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[practical-guides]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Wed, 19 Nov 2025 07:25:07 GMT</pubDate>
            <atom:updated>2025-11-19T07:25:07.828Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How to run PostgreSQL that doesn’t lose data, scales under load, and survives disasters</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y3Zx-SXDCuAR0gL4NLCcFA.png" /></figure><p>This guide teaches Backend developers and DevOps engineers the essential database knowledge they need. It is not a guide on becoming a Database Administrator (DBA). The focus is on practical knowledge: how to run a database that ensures data integrity, maintains high performance, and survives disasters.</p><p>I’ve consolidated all the necessary knowledge, tips, commands, and scripts to enable you to use this powerful database confidently and effectively.</p><p>Save this article as a reference to consult whenever you encounter a problem.</p><h3>At a Glance</h3><p>PostgreSQL is a relational database that guarantees ACID properties <strong>(Atomicity, Consistency, Isolation, Durability)</strong>. Here’s how it works internally:</p><h4><strong>The Write Process</strong></h4><pre>INSERT statement<br>    ↓<br>Write-Ahead Log (WAL) buffer<br>    ↓<br>WAL synced to disk (this is the commitment point)<br>    ↓<br>Data written to shared buffers (in-memory cache)<br>    ↓<br>Background writer flushes to data files<br>    ↓<br>Data persisted</pre><p><strong>Key insight</strong>: PostgreSQL writes to the transaction log FIRST, before writing to data files. If it crashes, it can replay the log and recover everything. This one of the reasons why PostgreSQL is reliable.</p><h3>Installation and Initial Setup</h3><p>I’ll start from basics and will go into more advanced information, to be completely buckled up!</p><h4>Install PostgreSQL</h4><pre># Ubuntu/Debian<br>sudo apt-get update<br>sudo apt-get install postgresql postgresql-contrib postgresql-client<br><br># macOS<br>brew install postgresql<br><br># Verify installation<br>psql --version<br># Output: psql (PostgreSQL) 16.0<br><br># Check if running<br>sudo systemctl status postgresql<br><br># Output: ● postgresql.service - PostgreSQL RDBMS<br>#         Loaded: loaded (/lib/systemd/system/postgresql.service)<br>#         Active: active (running)</pre><h4>Connect to PostgreSQL</h4><pre># Connect as postgres superuser<br>sudo -u postgres psql<br><br># Or if you have sudo access without password<br>psql -U postgres -h localhost<br><br># Verify you&#39;re connected<br>postgres=# \conninfo<br><br># Output: You are connected to database &quot;postgres&quot; as user &quot;postgres&quot; via socket in &quot;/var/run/postgresql&quot; at port 5432.</pre><h4>Basic PostgreSQL Commands</h4><pre># List all databases<br>\l<br><br># Connect to database<br>\c database_name<br><br># List all tables<br>\dt<br><br># List all schemas<br>\dn<br><br># Show table structure<br>\d table_name<br><br># Get help<br>\h CREATE TABLE<br><br># Exit<br>\q</pre><h3>Initial Configuration: postgresql.conf</h3><p>The postgresql.conf file is where PostgreSQL behavior is configured. Find it:</p><pre># Find location<br>sudo -u postgres psql -c &quot;SHOW config_file&quot;<br># Output: /etc/postgresql/16/main/postgresql.conf<br><br># View important settings<br>sudo -u postgres psql -c &quot;SHOW port&quot;<br># Output: 5432<br><br>sudo -u postgres psql -c &quot;SHOW data_directory&quot;<br># Output: /var/lib/postgresql/16/main</pre><h3>Essential Configuration for Production</h3><p>Edit /etc/postgresql/16/main/postgresql.conf:</p><pre># Network settings<br>listen_addresses = &#39;*&#39;           # Listen on all interfaces (restrict in production with IP)<br>port = 5432                      # Standard PostgreSQL port<br><br># Memory configuration<br>shared_buffers = 256MB           # 25% of system RAM for dedicated server<br>effective_cache_size = 1GB       # 50-75% of total RAM<br>work_mem = 16MB                  # Per operation memory<br>maintenance_work_mem = 64MB      # For maintenance operations<br><br># Checkpoint configuration (critical for performance)<br>checkpoint_timeout = 15min       # Force checkpoint every 15 minutes<br>checkpoint_completion_target = 0.9  # Spread checkpoint over 90% of interval<br>max_wal_size = 4GB              # Size of WAL before forcing checkpoint<br>min_wal_size = 1GB              # Don&#39;t delete WAL below this<br><br># Write-Ahead Logging (WAL) configuration<br>wal_level = replica             # Enable replication (critical for backup/failover)<br>max_wal_senders = 10            # Max replication connections<br>wal_keep_size = 1GB             # Keep WAL for replication<br>wal_compression = on            # Compress WAL<br><br># Connections<br>max_connections = 200           # Max client connections<br>superuser_reserved_connections = 10  # Reserve for superuser<br><br># Logging<br>logging_collector = on          # Enable log file collection<br>log_directory = &#39;log&#39;           # Relative to data_directory<br>log_filename = &#39;postgresql-%a.log&#39;<br>log_truncate_on_rotation = on   # Overwrite old logs<br><br># Log slow queries (anything over 1s)<br>log_min_duration_statement = 1000  # Milliseconds<br>log_statement = &#39;all&#39;           # Log all statements (use &#39;mod&#39; for less noise)<br>log_connections = on<br>log_disconnections = on<br><br># Performance<br>synchronous_commit = on         # Wait for WAL write (safer but slower)<br>fsync = on                      # Always fsync to disk (never disable in production)</pre><p>After editing, reload configuration:</p><pre># Reload without restart (some settings require restart)<br>sudo -u postgres psql -c &quot;SELECT pg_reload_conf()&quot;<br># Output: pg_reload_conf<br>#         ---------------<br>#         t</pre><pre># Or restart PostgreSQL (safer for critical changes)<br>sudo systemctl restart postgresql</pre><pre># Verify settings applied<br>sudo -u postgres psql -c &quot;SHOW shared_buffers&quot;<br># Output: 256MB</pre><h3>User and Database Management</h3><h4>Create Database and User</h4><pre># Connect as superuser<br>sudo -u postgres psql<br><br># Create database<br>CREATE DATABASE production_app;<br><br># Create user with password<br>CREATE USER app_user WITH ENCRYPTED PASSWORD &#39;secure_password_123&#39;;<br><br># Grant privileges to user<br>GRANT CONNECT ON DATABASE production_app TO app_user;<br>GRANT USAGE ON SCHEMA public TO app_user;<br>GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user;<br>GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app_user;<br><br># Give default privileges for future tables<br>ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO app_user;<br>ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO app_user;<br><br># Verify permissions<br>\du<br># Lists all roles and their attributes<br># Exit<br>\q</pre><h4>Connection String for Application</h4><pre># PostgreSQL connection string (DSN)<br>postgresql://app_user:secure_password_123@localhost:5432/production_app<br><br># Test connection from command line<br>psql postgresql://app_user:secure_password_123@localhost:5432/production_app -c &quot;SELECT 1&quot;<br><br># Output: ?column?<br>#         --------<br>#                1</pre><h3>Creating Tables and Indexes</h3><h4><strong>Create a Schema with Best Practices</strong></h4><pre>-- Connect as app_user<br>\c production_app app_user<br><br>-- Create table<br>CREATE TABLE users (<br>    id SERIAL PRIMARY KEY,<br>    email VARCHAR(255) NOT NULL,<br>    username VARCHAR(100) NOT NULL,<br>    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,<br>    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,<br>    is_active BOOLEAN DEFAULT true<br>);<br><br>-- Add indexes for common queries<br>CREATE UNIQUE INDEX idx_users_email ON users(email);<br>CREATE UNIQUE INDEX idx_users_username ON users(username);<br>CREATE INDEX idx_users_created_at ON users(created_at);<br>CREATE INDEX idx_users_is_active ON users(is_active);<br><br>-- View table structure<br>\d users<br>-- View indexes<br>\di users*<br><br>-- Check index size<br>SELECT <br>    schemaname,<br>    tablename,<br>    indexname,<br>    pg_size_pretty(pg_relation_size(indexrelid)) as size<br>FROM pg_indexes<br>WHERE tablename = &#39;users&#39;<br>ORDER BY pg_relation_size(indexrelid) DESC;</pre><h4>Add Constraints</h4><pre>-- Add constraint to ensure valid email<br>ALTER TABLE users <br>ADD CONSTRAINT check_valid_email <br>CHECK (email ~* &#39;^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$&#39;);<br><br>-- Add foreign key to another table<br>CREATE TABLE posts (<br>    id SERIAL PRIMARY KEY,<br>    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,<br>    title VARCHAR(255) NOT NULL,<br>    content TEXT,<br>    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP<br>);<br>-- View constraints<br>\d posts</pre><h3>Essential Queries for Monitoring</h3><h4>Check Database Health</h4><pre>-- Current connections<br>SELECT <br>    datname,<br>    count(*) as connections,<br>    max(extract(epoch from (now() - query_start))) as longest_query_seconds<br>FROM pg_stat_activity<br>GROUP BY datname<br>ORDER BY connections DESC;<br><br>-- Long-running queries (over 5 minutes)<br>SELECT <br>    pid,<br>    usename,<br>    query_start,<br>    extract(epoch from (now() - query_start)) as duration_seconds,<br>    query<br>FROM pg_stat_activity<br>WHERE query_start &lt; now() - interval &#39;5 minutes&#39;<br>ORDER BY query_start;<br>-- Kill long-running query (DANGER: use carefully)<br>SELECT pg_terminate_backend(pid);</pre><h4>Check Database Size</h4><pre>-- Total database size<br>SELECT <br>    datname,<br>    pg_size_pretty(pg_database_size(datname)) as size<br>FROM pg_database<br>WHERE datname NOT IN (&#39;template0&#39;, &#39;template1&#39;)<br>ORDER BY pg_database_size(datname) DESC;<br><br>-- Size per table<br>SELECT <br>    schemaname,<br>    tablename,<br>    pg_size_pretty(pg_total_relation_size(schemaname||&#39;.&#39;||tablename)) as size<br>FROM pg_tables<br>WHERE schemaname NOT IN (&#39;pg_catalog&#39;, &#39;information_schema&#39;)<br>ORDER BY pg_total_relation_size(schemaname||&#39;.&#39;||tablename) DESC<br>LIMIT 20;<br><br>-- Unused indexes (wasting space and slowing writes)<br>SELECT <br>    schemaname,<br>    tablename,<br>    indexname,<br>    idx_scan,<br>    pg_size_pretty(pg_relation_size(indexrelid)) as size<br>FROM pg_stat_user_indexes<br>WHERE idx_scan = 0<br>ORDER BY pg_relation_size(indexrelid) DESC;</pre><h4>Check Slow Queries</h4><p>First, ensure logging is configured:</p><pre># Check log file location<br>sudo -u postgres psql -c &quot;SHOW log_directory&quot;<br># Output: log<br><br># View recent logs<br>sudo tail -f /var/lib/postgresql/16/main/log/postgresql-*.log | grep &quot;duration:&quot;<br><br># Output example:<br># LOG:  duration: 2534.123 ms  statement: SELECT * FROM large_table;</pre><p>Find and optimize slow queries:</p><pre>-- Queries taking longest (requires pg_stat_statements extension)<br>CREATE EXTENSION IF NOT EXISTS pg_stat_statements;<br><br>SELECT <br>    query,<br>    calls,<br>    total_time,<br>    mean_time,<br>    max_time<br>FROM pg_stat_statements<br>ORDER BY mean_time DESC<br>LIMIT 10;<br>-- Clear stats<br>SELECT pg_stat_statements_reset();</pre><h4><strong>Analyze Query Performance</strong></h4><pre>-- EXPLAIN shows query plan WITHOUT executing<br>EXPLAIN SELECT * FROM users WHERE email = &#39;test@example.com&#39;;<br><br>-- Output:<br>-- QUERY PLAN<br>-- ─────────────────────────────────────────────<br>--  Index Scan using idx_users_email on users<br>--    Index Cond: (email = &#39;test@example.com&#39;)<br><br>-- EXPLAIN ANALYZE executes and shows actual performance<br>EXPLAIN ANALYZE SELECT * FROM users WHERE email = &#39;test@example.com&#39;;<br><br>-- Output includes actual timing and row counts<br>-- ─────────────────────────────────────────────<br>--  Index Scan using idx_users_email on users<br>--    Index Cond: (email = &#39;test@example.com&#39;)<br>--    Planning Time: 0.123 ms<br>--    Execution Time: 0.456 ms<br><br>-- Complex query analysis<br>EXPLAIN (ANALYZE, BUFFERS) <br>SELECT u.username, COUNT(p.id) as posts<br>FROM users u<br>LEFT JOIN posts p ON u.id = p.user_id<br>GROUP BY u.id<br>ORDER BY posts DESC;</pre><h3>Write-Ahead Log (WAL): The Safety Net</h3><p>WAL is PostgreSQL’s transaction log. It’s what makes PostgreSQL reliable.</p><h4>Understanding WAL</h4><pre># Check WAL directory<br>ls -lah /var/lib/postgresql/16/main/pg_wal/<br><br># Output:<br># -rw------- 1 postgres postgres 16M Nov 15 10:23 000000010000000000000001<br># -rw------- 1 postgres postgres 16M Nov 15 10:25 000000010000000000000002<br># -rw------- 1 postgres postgres 16M Nov 15 10:27 000000010000000000000003<br># Each WAL file is 16MB<br># Files are named sequentially<br># Older files are deleted after checkpoint (unless archiving is enabled)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*s32eSpPlLl74xvXD.png" /></figure><h4>Configure WAL Archiving (for backups)</h4><pre># Create archive directory<br>sudo mkdir -p /backup/wal_archive<br>sudo chown postgres:postgres /backup/wal_archive<br>sudo chmod 700 /backup/wal_archive<br><br># Edit postgresql.conf<br>sudo nano /etc/postgresql/16/main/postgresql.conf<br><br># Add these lines:<br># wal_level = replica<br># archive_mode = on<br># archive_command = &#39;cp %p /backup/wal_archive/%f&#39;<br># archive_timeout = 300  # Force archiving every 5 minutes</pre><p>After editing:</p><pre># Reload configuration<br>sudo -u postgres psql -c &quot;SELECT pg_reload_conf()&quot;<br><br># Force a checkpoint to start archiving<br>sudo -u postgres psql -c &quot;CHECKPOINT&quot;<br><br># Verify WAL files are being archived<br>ls -la /backup/wal_archive/<br><br># Output: WAL files copied here</pre><h4>Monitor WAL</h4><pre>-- Check WAL level<br>SHOW wal_level;<br>-- Output: replica<br><br>-- Check WAL stats<br>SELECT <br>    count(*) as wal_files,<br>    pg_size_pretty(sum(size)) as total_size<br>FROM (<br>    SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), &#39;0/0&#39;)::bigint as size<br>) as t;<br><br>-- Current WAL position<br>SELECT pg_current_wal_lsn();<br>-- Output: 0/17AB3F0</pre><h3>Backup Strategies</h3><p>To avoid worst scenarios, it’s required to setup backup strategy. Here are some approaches we can use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*NoQojxejXf-4wbdj" /></figure><h4>Strategy 1: pg_dump (Best for small-medium databases)</h4><pre># Simple backup of one database<br>pg_dump -U postgres -d production_app -F c -f backup.dump<br><br># Backup with compression<br>pg_dump -U postgres -d production_app \<br>    -F c \<br>    -f production_app_$(date +%Y%m%d_%H%M%S).dump<br><br># Backup all databases<br>pg_dumpall -U postgres -f all_databases.sql<br><br># Custom format (recommended for large DBs)<br>pg_dump -U postgres -d production_app \<br>    -F c \<br>    --verbose \<br>    -f backup.dump<br><br># Backup with jobs (parallel, faster)<br>pg_dump -U postgres -d production_app \<br>    -F d \<br>    -j 4 \<br>    -f backup_directory/<br><br># Show backup size<br>ls -lh backup.dump</pre><h4>Strategy 2: Restore from Backup</h4><pre># Restore from custom format dump<br>pg_restore -U postgres -d production_app -F c backup.dump<br><br># List what&#39;s in the dump before restoring<br>pg_restore -l backup.dump | head -20<br><br># Restore specific table<br>pg_restore -U postgres -d production_app -F c \<br>    -t users backup.dump<br><br># Restore with progress<br>pg_restore -U postgres -d production_app \<br>    -F c -j 4 --verbose backup.dump</pre><h4>Strategy 3: Continuous Archiving + Point-in-Time Recovery</h4><p>This is the most reliable backup strategy:</p><pre># 1. Full backup<br>pg_basebackup -h localhost -U postgres -D /backup/base_backup -Fp -Pv<br><br># 2. WAL archiving runs continuously (configured in postgresql.conf)<br># 3. To restore to specific point in time:<br><br># Stop PostgreSQL<br>sudo systemctl stop postgresql<br><br># Move current data directory<br>sudo mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.old<br><br># Restore base backup<br>sudo cp -r /backup/base_backup /var/lib/postgresql/16/main<br>sudo chown -R postgres:postgres /var/lib/postgresql/16/main<br><br># Create recovery.signal to enable recovery<br>sudo touch /var/lib/postgresql/16/main/recovery.signal<br><br># Configure recovery settings<br>sudo nano /var/lib/postgresql/16/main/postgresql.conf<br><br># Add:<br># restore_command = &#39;cp /backup/wal_archive/%f %p&#39;<br># recovery_target_timeline = &#39;latest&#39;<br># recovery_target_time = &#39;2024-01-15 14:30:00&#39;<br><br># Start PostgreSQL (it will replay WAL)<br>sudo systemctl start postgresql<br># Verify recovery<br>sudo -u postgres psql -c &quot;SELECT now()&quot;</pre><h4>Strategy 4: Automated Backup Script</h4><pre>#!/bin/bash<br># backup.sh<br><br>BACKUP_DIR=&quot;/backup/postgresql&quot;<br>DB_USER=&quot;postgres&quot;<br>RETENTION_DAYS=30<br># Create backup directory<br>mkdir -p $BACKUP_DIR<br># Full backup daily at 2 AM<br>BACKUP_FILE=&quot;$BACKUP_DIR/db_$(date +%Y%m%d_%H%M%S).dump&quot;<br>echo &quot;Starting backup...&quot;<br>pg_dump -U $DB_USER -d production_app -F c -f $BACKUP_FILE<br>if [ $? -eq 0 ]; then<br>    echo &quot;✓ Backup successful: $BACKUP_FILE&quot;<br>    <br>    # Verify backup<br>    pg_restore -U $DB_USER -d production_app -F c -l $BACKUP_FILE &gt; /dev/null<br>    if [ $? -eq 0 ]; then<br>        echo &quot;✓ Backup verified&quot;<br>    fi<br>else<br>    echo &quot;✗ Backup failed&quot;<br>    exit 1<br>fi<br># Remove old backups (older than RETENTION_DAYS)<br>find $BACKUP_DIR -type f -name &quot;*.dump&quot; -mtime +$RETENTION_DAYS -delete<br>echo &quot;✓ Cleanup completed&quot;<br># Optional: Copy to remote storage<br># scp $BACKUP_FILE remote_server:/remote/backup/</pre><p>Schedule it:</p><pre># Add to crontab<br>crontab -e<br><br># Add line (runs daily at 2 AM)<br>0 2 * * * /home/postgres/backup.sh &gt;&gt; /var/log/pg_backup.log 2&gt;&amp;1<br><br># List cron jobs<br>crontab -l</pre><h3>How to Set Up Streaming Replication</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KKxdwz3Mb64Ctbu3.png" /></figure><p><strong>Primary Server (Production):</strong></p><pre># Edit postgresql.conf<br>sudo nano /etc/postgresql/16/main/postgresql.conf<br><br># Add:<br>wal_level = replica<br>max_wal_senders = 10<br>wal_keep_size = 1GB<br>hot_standby = on<br><br># Edit pg_hba.conf for replication user<br>sudo nano /etc/postgresql/16/main/pg_hba.conf<br># Add (allow replica connection):<br>host    replication    replica_user    192.168.1.0/24    md5<br><br># Create replication user<br>sudo -u postgres psql -c &quot;CREATE USER replica_user WITH REPLICATION ENCRYPTED PASSWORD &#39;replica_password&#39;;&quot;<br><br># Restart<br>sudo systemctl restart postgresql</pre><p><strong>Replica Server (Standby):</strong></p><pre># Stop replica if running<br>sudo systemctl stop postgresql<br><br># Clear old data directory<br>sudo rm -rf /var/lib/postgresql/16/main/*<br><br># Take base backup from primary<br>pg_basebackup -h 192.168.1.100 -U replica_user -D /var/lib/postgresql/16/main -Fp -Pv<br><br># Create standby.signal (tells PostgreSQL this is standby)<br>sudo touch /var/lib/postgresql/16/main/standby.signal<br><br># Configure replica postgresql.conf<br>sudo nano /var/lib/postgresql/16/main/postgresql.conf<br><br># Add:<br>primary_conninfo = &#39;host=192.168.1.100 port=5432 user=replica_user password=replica_password&#39;<br>hot_standby = on<br><br># Set correct permissions<br>sudo chown -R postgres:postgres /var/lib/postgresql/16/main<br>sudo chmod 700 /var/lib/postgresql/16/main<br><br># Start replica<br>sudo systemctl start postgresql<br><br># Verify replication is working<br>sudo -u postgres psql -c &quot;SELECT * FROM pg_stat_replication;&quot;<br># On replica:<br>sudo -u postgres psql -c &quot;SELECT * FROM pg_last_wal_receive_lsn();&quot;</pre><h3>Monitor Replication</h3><pre>-- On Primary: Check replica lag<br>SELECT <br>    client_addr,<br>    state,<br>    sent_lsn,<br>    write_lsn,<br>    flush_lsn,<br>    replay_lsn,<br>    sync_state<br>FROM pg_stat_replication;<br><br>-- On Replica: Check replication status<br>SELECT <br>    pg_last_wal_receive_lsn() as receive_lsn,<br>    pg_last_wal_replay_lsn() as replay_lsn,<br>    pg_is_wal_replay_paused() as is_paused;<br><br>-- Calculate replication lag (in bytes)<br>SELECT <br>    pg_wal_lsn_diff(sent_lsn, replay_lsn) as lag_bytes<br>FROM pg_stat_replication;</pre><h3>Promote Replica to Primary</h3><p>If primary fails:</p><pre># On replica<br>sudo -u postgres psql -c &quot;SELECT pg_promote();&quot;<br><br># Verify it&#39;s now primary<br>sudo -u postgres psql -c &quot;SELECT pg_is_in_recovery();&quot;<br># Output: f (false = it&#39;s primary now)</pre><h3>Some Performance Tuning and log analyzing</h3><pre># Enable query logging<br>sudo -u postgres psql -c &quot;ALTER SYSTEM SET log_min_duration_statement = 1000;&quot;<br>sudo -u postgres psql -c &quot;SELECT pg_reload_conf();&quot;<br><br># View logs<br>tail -f /var/lib/postgresql/16/main/log/postgresql-*.log | grep &quot;duration:&quot;<br><br># Parse and analyze<br>grep &quot;duration:&quot; /var/lib/postgresql/16/main/log/postgresql-*.log | \<br>    awk &#39;{print $NF}&#39; | sort -rn | head -20</pre><h4>If needed, we can optimize common issues:</h4><pre>-- 1. Add missing indexes<br>-- Query to find queries without indexes:<br>SELECT query FROM pg_stat_statements WHERE query LIKE &#39;%WHERE%&#39; LIMIT 5;<br><br>-- Add index after analyzing with EXPLAIN<br>CREATE INDEX idx_posts_user_id_created_at ON posts(user_id, created_at DESC);<br><br>-- 2. Analyze table statistics<br>ANALYZE users;<br>ANALYZE posts;<br><br>-- 3. Vacuum (reclaim space, update statistics)<br>VACUUM ANALYZE;<br><br>-- 4. Check for table bloat<br>SELECT <br>    schemaname,<br>    tablename,<br>    pg_size_pretty(pg_total_relation_size(schemaname||&#39;.&#39;||tablename)) as total_size,<br>    pg_size_pretty(pg_total_relation_size(schemaname||&#39;.&#39;||tablename) - pg_relation_size(schemaname||&#39;.&#39;||tablename)) as indexes_size<br>FROM pg_tables<br>ORDER BY pg_total_relation_size(schemaname||&#39;.&#39;||tablename) DESC<br>LIMIT 10;<br><br>-- 5. Enable auto-vacuum (maintenance)<br>SHOW autovacuum;<br>ALTER SYSTEM SET autovacuum = on;<br>SELECT pg_reload_conf();</pre><h3>Monitoring and Alerting: Key metrics to monitor</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_iFhoCmCq-JjyXna.png" /></figure><pre># 1. Check active connections<br>sudo -u postgres psql -c &quot;SELECT count(*) FROM pg_stat_activity;&quot;<br><br># 2. Check cache hit ratio (should be &gt; 99%)<br>sudo -u postgres psql &lt;&lt; EOF<br>SELECT <br>    sum(heap_blks_read) as heap_read,<br>    sum(heap_blks_hit) as heap_hit,<br>    sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as ratio<br>FROM pg_statio_user_tables;<br>EOF<br><br># 3. Check disk usage<br>df -h /var/lib/postgresql<br><br># 4. Check transaction ID age (prevent wraparound)<br>sudo -u postgres psql -c &quot;SELECT datname, age(datfrozenxid) FROM pg_database;&quot;<br><br># 5. Check replication lag (if using replication)<br>sudo -u postgres psql -c &quot;SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), &#39;0/0&#39;)/1024/1024 as lag_mb;&quot;</pre><h3>Create Monitoring Alert Script</h3><pre>#!/bin/bash<br># pg_monitor.sh<br><br>DB_USER=&quot;postgres&quot;<br>DB_NAME=&quot;production_app&quot;<br>WARNING_CONNECTIONS=150<br>CRITICAL_CONNECTIONS=180<br># Check connections<br>CONNECTIONS=$(sudo -u $DB_USER psql -d $DB_NAME -t -c &quot;SELECT count(*) FROM pg_stat_activity;&quot;)<br>if [ &quot;$CONNECTIONS&quot; -gt &quot;$CRITICAL_CONNECTIONS&quot; ]; then<br>    echo &quot;CRITICAL: PostgreSQL connections: $CONNECTIONS/$MAX_CONNECTIONS&quot;<br>    # Send alert (email, PagerDuty, Slack)<br>elif [ &quot;$CONNECTIONS&quot; -gt &quot;$WARNING_CONNECTIONS&quot; ]; then<br>    echo &quot;WARNING: PostgreSQL connections: $CONNECTIONS&quot;<br>fi<br># Check cache hit ratio<br>CACHE_HIT=$(sudo -u $DB_USER psql -d $DB_NAME -t -c &quot;SELECT round(sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) * 100, 2) FROM pg_statio_user_tables;&quot;)<br>if (( $(echo &quot;$CACHE_HIT &lt; 95&quot; | bc -l) )); then<br>    echo &quot;WARNING: Low cache hit ratio: $CACHE_HIT%&quot;<br>fi<br># Check disk usage<br>DISK_USED=$(df /var/lib/postgresql | awk &#39;NR==2 {print $5}&#39; | sed &#39;s/%//&#39;)<br>if [ &quot;$DISK_USED&quot; -gt 80 ]; then<br>    echo &quot;CRITICAL: Disk usage: $DISK_USED%&quot;<br>fi</pre><p>To schedule monitoring I use <strong>crontab</strong>:</p><pre># Add to crontab (check every 5 minutes)<br>*/5 * * * * /home/postgres/pg_monitor.sh &gt;&gt; /var/log/pg_monitor.log 2&gt;&amp;1</pre><h3>Disaster Recovery Checklist</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/758/0*S97R7ZYCP85cUKPx.png" /></figure><h4><strong>Before Disaster</strong></h4><pre># 1. Verify backups work<br>pg_restore -U postgres -d test_db -F c /backup/database.dump<br><br># 2. Document recovery procedure<br># Create recovery_procedure.txt with all steps<br><br># 3. Test failover<br># Promote replica to primary<br><br># 4. Document connection strings<br># Primary: postgresql://user:pass@primary-ip:5432/db<br># Replica: postgresql://user:pass@replica-ip:5432/db<br><br># 5. Monitor backup age<br>find /backup -name &quot;*.dump&quot; -type f -mtime +1 -exec ls -la {} \;</pre><h4>When Disaster Strikes</h4><pre># 1. Assess the damage<br>sudo systemctl status postgresql<br>sudo journalctl -n 50 -u postgresql<br><br># 2. Check data directory integrity<br>sudo -u postgres pg_controldata /var/lib/postgresql/16/main | head -20<br><br># 3. Try recovery first (don&#39;t restore unless necessary)<br>sudo systemctl restart postgresql<br>sudo tail -f /var/lib/postgresql/16/main/log/postgresql-*.log<br><br># 4. If corrupted, restore from backup<br># Follow &quot;Strategy 3: Continuous Archiving&quot; section above<br><br># 5. Verify data after recovery<br>sudo -u postgres psql -c &quot;SELECT count(*) FROM users;&quot;</pre><h3>Common Commands Quick Reference</h3><pre># Connection &amp; Authentication<br>psql -U postgres -h localhost -d postgres<br>psql postgresql://user:password@host:5432/dbname</pre><pre># Database Operations<br>\l                    # List databases<br>\c dbname            # Connect to database<br>CREATE DATABASE name;<br>DROP DATABASE name;<br><br># Table Operations<br>\dt                  # List tables<br>\d tablename         # Show table structure<br>\di                  # List indexes<br>\dn                  # List schemas<br><br># User Management<br>\du                  # List users<br>CREATE USER name WITH PASSWORD &#39;pass&#39;;<br>DROP USER name;<br>GRANT ALL ON DATABASE db TO user;<br><br># Backup &amp; Restore<br>pg_dump -U user -d db -F c -f backup.dump<br>pg_restore -U user -d db -F c backup.dump<br>pg_basebackup -U user -D /path -Fp -Pv<br><br># Monitoring<br>SELECT * FROM pg_stat_activity;<br>SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database;<br>EXPLAIN ANALYZE SELECT ...;<br><br># Performance<br>VACUUM ANALYZE;<br>REINDEX TABLE tablename;<br>ALTER TABLE tablename SET (fillfactor=80);<br><br># Replication<br>SELECT * FROM pg_stat_replication;<br>SELECT pg_promote();</pre><h3>Production Deployment Checklist</h3><pre>☐ postgresql.conf tuned (shared_buffers, wal_level, etc.)<br>☐ pg_hba.conf restricted (only necessary IPs)<br>☐ Users created with appropriate privileges<br>☐ Backup strategy implemented and tested<br>☐ WAL archiving configured<br>☐ Monitoring and alerting in place<br>☐ Replication configured (if high availability needed)<br>☐ Connection pooling configured (in application)<br>☐ Slow query logging enabled<br>☐ Regular VACUUM ANALYZE scheduled<br>☐ Cache hit ratio verified (&gt;99%)<br>☐ Disk space monitored<br>☐ Recovery procedure documented<br>☐ Disaster recovery tested<br>☐ pg_stat_statements extension installed<br>☐ SSL/TLS configured for remote connections</pre><h3>You now understand:</h3><ul><li>How PostgreSQL writes data reliably (WAL)</li><li>How to back up data safely (pg_dump, pg_basebackup)</li><li>How to recover from disaster (point-in-time recovery)</li><li>How to scale without downtime (replication)</li><li>How to optimize performance (indexes, monitoring)</li></ul><p><em>This isn’t DBA-level knowledge. This is DevOps knowledge. Enough to run PostgreSQL safely in production.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2630f41ce779" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Prometheus from Zero: Metrics Collection and Alerting]]></title>
            <link>https://medium.com/@mecreate/prometheus-from-zero-metrics-collection-and-alerting-15feb15031b1?source=rss-8bc0f2c771a------2</link>
            <guid isPermaLink="false">https://medium.com/p/15feb15031b1</guid>
            <category><![CDATA[alerting]]></category>
            <category><![CDATA[logs]]></category>
            <category><![CDATA[monitoring]]></category>
            <category><![CDATA[metrics]]></category>
            <category><![CDATA[prometheus]]></category>
            <dc:creator><![CDATA[Muhammetberdi Jepbarov]]></dc:creator>
            <pubDate>Sat, 15 Nov 2025 09:30:07 GMT</pubDate>
            <atom:updated>2025-11-15T09:30:07.480Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How to see inside your applications and know when things break before your users complain</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zDBe4A2442U4pOFJX-pvMw.png" /></figure><h3>The Blind Spot</h3><p>Are you always shure the system is healthy?<br>If not, what would be your approach checking it? SSH into a server and check disk space &amp; logs?</p><p>This is the reality for engineers without proper monitoring. You’re flying blind. You don’t know if your application is slow because of database queries or network latency. You don’t know if you’re approaching capacity. You discover outages from customer complaints on Twitter.</p><p>Then one day you realize: <em>what if you could see everything?</em></p><p>Not logs. Logs tell you what happened after it already broke. You need metrics — real-time data about your system’s behavior. Response times. Error rates. Memory usage. Database connections. Every number that matters.</p><p>This is what <strong>Prometheus</strong> does.</p><p>Prometheus is a time-series database that scrapes metrics from your applications, stores them efficiently, and lets you query them to understand your system’s behavior. Unlike traditional monitoring that pulls from agents, Prometheus works differently: your applications <em>push</em> their metrics in a standard format, and Prometheus periodically <em>pulls</em> them.</p><p>By the end of this article, you’ll have a production-capable monitoring system that shows you exactly what’s happening inside your applications.</p><h3>Why Prometheus? Why Now?</h3><p>Before Prometheus, monitoring was complicated. Nagios. Zabbix. Complex agent installations. Steep learning curves. Proprietary formats.</p><p>Prometheus changed the game with four radical simplifications:</p><ol><li><strong>Simple text format</strong> — Metrics are readable human-friendly text, not binary protocols</li><li><strong>Pull-based, not push</strong> — No complex agent infrastructure, just HTTP endpoints</li><li><strong>Powerful query language</strong> — PromQL lets you ask complex questions about your data</li><li><strong>Designed for microservices</strong> — Built to handle ephemeral containers and dynamic infrastructure</li></ol><p>Here’s the key insight: Prometheus doesn’t complicate things. It simplifies them.</p><h3>The Mental Model</h3><p>Understanding Prometheus requires understanding three concepts:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qHvhBt3E4VrVD0h9.png" /></figure><h3>1. Metrics</h3><p>A metric is a measurement. Response time. Error count. Memory usage. Queue depth. Every number from your system is a metric.</p><p>Prometheus has four metric types:</p><ul><li><strong>Counter</strong> — Only goes up (errors, requests processed)</li><li><strong>Gauge</strong> — Goes up and down (memory usage, active connections)</li><li><strong>Histogram</strong> — Tracks distribution (response times)</li><li><strong>Summary</strong> — Like histogram, calculates percentiles</li></ul><h3>2. Labels</h3><p>A metric alone isn’t enough. You need context. The same metric means different things on different servers.</p><p>That’s where labels come in. They’re key-value pairs that add context:</p><pre>http_requests_total{method=&quot;GET&quot;, path=&quot;/api/users&quot;, status=&quot;200&quot;} 1534</pre><p>This single line tells you: “1534 GET requests to /api/users that returned status 200.”</p><p>Labels let you slice your data any way you want. Query errors by endpoint. Compare response times across servers. Drill down to specific problems.</p><h3>3. Scraping</h3><p>Prometheus doesn’t wait for data. It periodically connects to your application at /metrics endpoint and pulls all metrics. This is called &quot;scraping.&quot;</p><p>Your application exposes metrics. Prometheus scrapes them every 15 seconds (or whatever you configure). Prometheus stores them. You query them.</p><p>This is the pull model. Simple. Reliable. Resilient.</p><h3>Building a Go Microservice with Metrics</h3><p>Let’s write some Go!<br>Most of my articles are connected with this brilliant language.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/0*bRJ-zmmG4w1GHBVL.gif" /></figure><p>I’ll do a simple API service that tracks:</p><ul><li>How many requests it receives</li><li>How long requests take</li><li>What errors occur</li><li>Custom business metrics</li></ul><p>Create main.go:</p><pre>package main<br><br>import (<br> &quot;fmt&quot;<br> &quot;github.com/prometheus/client_golang/prometheus&quot;<br> &quot;github.com/prometheus/client_golang/prometheus/promauto&quot;<br> &quot;github.com/prometheus/client_golang/prometheus/promhttp&quot;<br> &quot;log&quot;<br> &quot;math/rand&quot;<br> &quot;net/http&quot;<br> &quot;time&quot;<br>)<br><br>var (<br> // Counter: total requests received<br> httpRequestsTotal = promauto.NewCounterVec(<br>  prometheus.CounterOpts{<br>   Name: &quot;http_requests_total&quot;,<br>   Help: &quot;Total number of HTTP requests&quot;,<br>  },<br>  []string{&quot;method&quot;, &quot;endpoint&quot;, &quot;status&quot;},<br> )<br><br> // Histogram: request duration in seconds<br> httpRequestDuration = promauto.NewHistogramVec(<br>  prometheus.HistogramOpts{<br>   Name:    &quot;http_request_duration_seconds&quot;,<br>   Help:    &quot;HTTP request latency in seconds&quot;,<br>   Buckets: prometheus.DefBuckets,<br>  },<br>  []string{&quot;method&quot;, &quot;endpoint&quot;},<br> )<br><br> // Gauge: active connections<br> activeConnections = promauto.NewGauge(<br>  prometheus.GaugeOpts{<br>   Name: &quot;active_connections&quot;,<br>   Help: &quot;Number of active connections&quot;,<br>  },<br> )<br><br> // Counter: database errors<br> databaseErrors = promauto.NewCounterVec(<br>  prometheus.CounterOpts{<br>   Name: &quot;database_errors_total&quot;,<br>   Help: &quot;Total number of database errors&quot;,<br>  },<br>  []string{&quot;operation&quot;},<br> )<br><br> // Gauge: items in processing queue<br> queueDepth = promauto.NewGauge(<br>  prometheus.GaugeOpts{<br>   Name: &quot;queue_depth&quot;,<br>   Help: &quot;Number of items in processing queue&quot;,<br>  },<br> )<br>)<br><br>func metricsMiddleware(next http.Handler) http.Handler {<br> return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {<br>  start := time.Now()<br>  activeConnections.Inc()<br>  defer activeConnections.Dec()<br><br>  wrapped := &amp;responseWriter{ResponseWriter: w, statusCode: http.StatusOK}<br>  next.ServeHTTP(wrapped, r)<br><br>  // Record metrics<br>  duration := time.Since(start).Seconds()<br>  status := fmt.Sprintf(&quot;%d&quot;, wrapped.statusCode)<br>  httpRequestsTotal.WithLabelValues(<br>   r.Method,<br>   r.URL.Path,<br>   status,<br>  ).Inc()<br>  httpRequestDuration.WithLabelValues(<br>   r.Method,<br>   r.URL.Path,<br>  ).Observe(duration)<br> })<br>}<br><br>type responseWriter struct {<br> http.ResponseWriter<br> statusCode int<br>}<br><br>func (rw *responseWriter) WriteHeader(code int) {<br> rw.statusCode = code<br> rw.ResponseWriter.WriteHeader(code)<br>}<br><br>func simulateDatabaseOperation() error {<br> // 10% chance of error<br> if rand.Float32() &lt; 0.1 {<br>  databaseErrors.WithLabelValues(&quot;query&quot;).Inc()<br>  return fmt.Errorf(&quot;database error&quot;)<br> }<br> return nil<br>}<br><br>func handleGetUsers(w http.ResponseWriter, r *http.Request) {<br> // Simulate processing time (10-100ms)<br> delay := time.Duration(rand.Intn(90)+10) * time.Millisecond<br> time.Sleep(delay)<br> if err := simulateDatabaseOperation(); err != nil {<br>  http.Error(w, &quot;Database error&quot;, http.StatusInternalServerError)<br>  return<br> }<br> w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)<br> fmt.Fprintf(w, `{&quot;users&quot;: [{&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;}]}`)<br>}<br><br>func handleGetUser(w http.ResponseWriter, r *http.Request) {<br> delay := time.Duration(rand.Intn(50)+5) * time.Millisecond<br> time.Sleep(delay)<br> if err := simulateDatabaseOperation(); err != nil {<br>  http.Error(w, &quot;Database error&quot;, http.StatusInternalServerError)<br>  return<br> }<br> w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)<br> fmt.Fprintf(w, `{&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;}`)<br>}<br><br>func handleCreateUser(w http.ResponseWriter, r *http.Request) {<br> delay := time.Duration(rand.Intn(200)+50) * time.Millisecond<br> time.Sleep(delay)<br> if err := simulateDatabaseOperation(); err != nil {<br>  http.Error(w, &quot;Database error&quot;, http.StatusInternalServerError)<br>  return<br> }<br> w.WriteHeader(http.StatusCreated)<br> fmt.Fprintf(w, `{&quot;id&quot;: 123}`)<br>}<br><br>// We will Simulate background job that processes queue<br>func simulateBackgroundJob() {<br> ticker := time.NewTicker(5 * time.Second)<br> for range ticker.C {<br>  // Random queue depth between 0-50<br>  depth := rand.Intn(51)<br>  queueDepth.Set(float64(depth))<br> }<br>}<br>func main() {<br> go simulateBackgroundJob()<br><br> mux := http.NewServeMux()<br> mux.HandleFunc(&quot;/api/users&quot;, handleGetUsers)<br> mux.HandleFunc(&quot;/api/users/1&quot;, handleGetUser)<br> mux.HandleFunc(&quot;/api/users/create&quot;, handleCreateUser)<br><br> handler := metricsMiddleware(mux)<br><br> // Expose Prometheus metrics at /metrics<br> http.Handle(&quot;/metrics&quot;, promhttp.Handler())<br> http.HandleFunc(&quot;/&quot;, handler.ServeHTTP)<br> log.Println(&quot;Service started on :8080&quot;)<br> log.Println(&quot;Metrics available at http://localhost:8080/metrics&quot;)<br> log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))<br>}</pre><p>To run this, you need the Prometheus client library:</p><pre>go mod init user-service<br>go get github.com/prometheus/client_golang/...<br>go run main.go</pre><p>Now visit http://localhost:8080/metrics and you&#39;ll see raw Prometheus format:</p><pre># HELP http_requests_total Total number of HTTP requests<br># TYPE http_requests_total counter<br>http_requests_total{endpoint=&quot;/api/users&quot;,method=&quot;GET&quot;,status=&quot;200&quot;} 42<br><br># HELP http_request_duration_seconds HTTP request latency in seconds<br># TYPE http_request_duration_seconds histogram<br>http_request_duration_seconds_bucket{endpoint=&quot;/api/users&quot;,method=&quot;GET&quot;,le=&quot;0.005&quot;} 5<br>http_request_duration_seconds_bucket{endpoint=&quot;/api/users&quot;,method=&quot;GET&quot;,le=&quot;0.01&quot;} 12<br>...<br># HELP active_connections Number of active connections<br># TYPE active_connections gauge<br>active_connections 0<br><br># HELP database_errors_total Total number of database errors<br># TYPE database_errors_total counter<br>database_errors_total{operation=&quot;query&quot;} 3<br><br># HELP queue_depth Number of items in processing queue<br># TYPE queue_depth gauge<br>queue_depth 17</pre><p>This is Prometheus format. Your application is now instrumented.</p><h3>Installing and Configuring Prometheus</h3><p>Now that your application exposes metrics, you need Prometheus to scrape them.</p><h3>Installation</h3><pre># Download Prometheus<br>wget https://github.com/prometheus/prometheus/releases/download/v2.48.0/prometheus-2.48.0.linux-amd64.tar.gz<br>tar xvfz prometheus-2.48.0.linux-amd64.tar.gz<br>cd prometheus-2.48.0.linux-amd64</pre><p>Or using Docker:</p><pre>docker run -d -p 9090:9090 prom/prometheus</pre><h3>Configuration</h3><p>Create prometheus.yml:</p><pre># Global settings<br>global:<br>  scrape_interval: 15s        # How often to scrape metrics<br>  evaluation_interval: 15s    # How often to evaluate alert rules<br>  external_labels:<br>    monitor: &#39;production&#39;     # Label all metrics with this<br><br># Alert manager configuration (we&#39;ll use this later)<br>alerting:<br>  alertmanagers:<br>    - static_configs:<br>        - targets:<br>            - &#39;localhost:9093&#39;<br><br># Alert rules<br>rule_files:<br>  - &#39;alerts.yml&#39;<br><br># Scrape configurations<br>scrape_configs:<br><br>  # Prometheus itself<br>  - job_name: &#39;prometheus&#39;<br>    static_configs:<br>      - targets: [&#39;localhost:9090&#39;]<br><br>  # Our user service<br>  - job_name: &#39;user-service&#39;<br>    static_configs:<br>      - targets: [&#39;localhost:8080&#39;]<br><br>    # Override global scrape interval for this job<br>    scrape_interval: 5s<br>    scrape_timeout: 5s<br><br>    # Add custom labels to all metrics from this job<br>    metric_relabel_configs:<br>      - source_labels: [__name__]<br>        target_label: service<br>        replacement: &#39;user-service&#39;</pre><p>Key concepts here:</p><ul><li>scrape_interval — How often Prometheus pulls metrics (15 seconds by default)</li><li>evaluation_interval — How often alert rules are checked</li><li>job_name — Logical name for this target</li><li>targets — What to scrape (IP:port)</li><li>metric_relabel_configs — Transform metrics as they&#39;re scraped</li></ul><h3>Run Prometheus</h3><pre># If installed locally<br>./prometheus --config.file=prometheus.yml<br><br># If using Docker<br>docker run -d -p 9090:9090 \<br>  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \<br>  prom/prometheus</pre><p>Visit http://localhost:9090 and you&#39;ll see the Prometheus UI.</p><h3>PromQL: Asking Questions About Your Data</h3><p>Prometheus is useless if you can’t query it. That’s where PromQL comes in — a query language designed for time-series data.</p><p>Let’s explore it using the Prometheus UI. Click on the “Graph” tab and enter queries.</p><h3>Simple Queries</h3><p><strong>What’s the current queue depth?</strong></p><pre>queue_depth</pre><p>Returns: 17</p><p><strong>How many total requests have we received?</strong></p><pre>http_requests_total</pre><p>Returns all metrics with this name, broken down by labels.</p><p><strong>How many successful GET requests to /api/users?</strong></p><pre>http_requests_total{method=&quot;GET&quot;, endpoint=&quot;/api/users&quot;, status=&quot;200&quot;}</pre><h3>Ranges and Aggregation</h3><p><strong>Average response time over the last 5 minutes?</strong></p><pre>avg(rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]))</pre><p>This looks complex, but it’s calculating: total seconds / total count = average.</p><p><strong>Request rate (requests per second) over last minute?</strong></p><pre>rate(http_requests_total[1m])</pre><p>rate() calculates per-second rate of change. Over 1 minute, it shows requests/sec.</p><p><strong>Error rate for /api/users endpoint?</strong></p><pre>rate(http_requests_total{endpoint=&quot;/api/users&quot;, status=&quot;5xx&quot;}[5m]) / rate(http_requests_total{endpoint=&quot;/api/users&quot;}[5m])</pre><p>Errors divided by total = error rate. Multiply by 100 for percentage.</p><h3>Aggregation Across Labels</h3><p><strong>Total requests across all endpoints?</strong></p><pre>sum(rate(http_requests_total[5m]))</pre><p><strong>By method?</strong></p><pre>sum(rate(http_requests_total[5m])) by (method)</pre><p><strong>By endpoint?</strong></p><pre>sum(rate(http_requests_total[5m])) by (endpoint)</pre><p><strong>Top 3 slowest endpoints?</strong></p><pre>topk(3, avg(http_request_duration_seconds) by (endpoint))</pre><h3>Comparing Time Periods</h3><p><strong>How much traffic increase compared to 1 hour ago?</strong></p><pre>rate(http_requests_total[1m]) / rate(http_requests_total offset 1h [1m])</pre><p>The offset modifier lets you reference past data.</p><h3>PromQL Operators</h3><ul><li>+ - * / — Arithmetic</li><li>== != &gt; &lt; &gt;= &lt;= — Comparison</li><li>and or unless — Logical</li><li>bool — Convert comparison to 0/1</li></ul><p><strong>Which endpoints are slower than 100ms?</strong></p><pre>(http_request_duration_seconds &gt; 0.1) * bool</pre><p>The beauty of PromQL is that it’s designed for the questions you actually ask: “Is this broken? How much traffic? Compare to yesterday? Which thing is slowest?”</p><h3>Prometheus from Zero: Metrics Collection and Alerting</h3><p><em>How to see inside your applications and know when things break before your users complain</em></p><h3>The Blind Spot</h3><p>It’s 3 PM on a Wednesday. Your boss walks by: “Is the system healthy?”</p><p>Your answer: “…I think so? Let me SSH into a server and check disk space.”</p><p>This is the reality for engineers without proper monitoring. You’re flying blind. You don’t know if your application is slow because of database queries or network latency. You don’t know if you’re approaching capacity. You discover outages from customer complaints on Twitter.</p><p>Then one day you realize: <em>what if you could see everything?</em></p><p>Not logs. Logs tell you what happened after it already broke. You need metrics — real-time data about your system’s behavior. Response times. Error rates. Memory usage. Database connections. Every number that matters.</p><p>This is what Prometheus does.</p><p>Prometheus is a time-series database that scrapes metrics from your applications, stores them efficiently, and lets you query them to understand your system’s behavior. Unlike traditional monitoring that pulls from agents, Prometheus works differently: your applications <em>push</em> their metrics in a standard format, and Prometheus periodically <em>pulls</em> them.</p><p>By the end of this article, you’ll have a production-capable monitoring system that shows you exactly what’s happening inside your applications.</p><h3>Why Prometheus? Why Now?</h3><p>Before Prometheus, monitoring was complicated. Nagios. Zabbix. Complex agent installations. Steep learning curves. Proprietary formats.</p><p>Prometheus changed the game with four radical simplifications:</p><ol><li><strong>Simple text format</strong> — Metrics are readable human-friendly text, not binary protocols</li><li><strong>Pull-based, not push</strong> — No complex agent infrastructure, just HTTP endpoints</li><li><strong>Powerful query language</strong> — PromQL lets you ask complex questions about your data</li><li><strong>Designed for microservices</strong> — Built to handle ephemeral containers and dynamic infrastructure</li></ol><p>Here’s the key insight: Prometheus doesn’t complicate things. It simplifies them.</p><h3>The Mental Model</h3><p>Understanding Prometheus requires understanding three concepts:</p><h3>1. Metrics</h3><p>A metric is a measurement. Response time. Error count. Memory usage. Queue depth. Every number from your system is a metric.</p><p>Prometheus has four metric types:</p><ul><li><strong>Counter</strong> — Only goes up (errors, requests processed)</li><li><strong>Gauge</strong> — Goes up and down (memory usage, active connections)</li><li><strong>Histogram</strong> — Tracks distribution (response times)</li><li><strong>Summary</strong> — Like histogram, calculates percentiles</li></ul><h3>2. Labels</h3><p>A metric alone isn’t enough. You need context. The same metric means different things on different servers.</p><p>That’s where labels come in. They’re key-value pairs that add context:</p><pre>http_requests_total{method=&quot;GET&quot;, path=&quot;/api/users&quot;, status=&quot;200&quot;} 1534</pre><p>This single line tells you: “1534 GET requests to /api/users that returned status 200.”</p><p>Labels let you slice your data any way you want. Query errors by endpoint. Compare response times across servers. Drill down to specific problems.</p><h3>3. Scraping</h3><p>Prometheus doesn’t wait for data. It periodically connects to your application at /metrics endpoint and pulls all metrics. This is called &quot;scraping.&quot;</p><p>Your application exposes metrics. Prometheus scrapes them every 15 seconds (or whatever you configure). Prometheus stores them. You query them.</p><p>This is the pull model. Simple. Reliable. Resilient.</p><h3>Building a Go Microservice with Metrics</h3><p>Let’s build something real. A simple API service that tracks:</p><ul><li>How many requests it receives</li><li>How long requests take</li><li>What errors occur</li><li>Custom business metrics</li></ul><p>Create main.go:</p><pre>package main</pre><pre>import (<br>	&quot;fmt&quot;<br>	&quot;log&quot;<br>	&quot;math/rand&quot;<br>	&quot;net/http&quot;<br>	&quot;time&quot;</pre><pre>	&quot;github.com/prometheus/client_golang/prometheus&quot;<br>	&quot;github.com/prometheus/client_golang/prometheus/promauto&quot;<br>	&quot;github.com/prometheus/client_golang/prometheus/promhttp&quot;<br>)</pre><pre>var (<br>	// Counter: total requests received<br>	httpRequestsTotal = promauto.NewCounterVec(<br>		prometheus.CounterOpts{<br>			Name: &quot;http_requests_total&quot;,<br>			Help: &quot;Total number of HTTP requests&quot;,<br>		},<br>		[]string{&quot;method&quot;, &quot;endpoint&quot;, &quot;status&quot;},<br>	)</pre><pre>	// Histogram: request duration in seconds<br>	httpRequestDuration = promauto.NewHistogramVec(<br>		prometheus.HistogramOpts{<br>			Name:    &quot;http_request_duration_seconds&quot;,<br>			Help:    &quot;HTTP request latency in seconds&quot;,<br>			Buckets: prometheus.DefBuckets,<br>		},<br>		[]string{&quot;method&quot;, &quot;endpoint&quot;},<br>	)</pre><pre>	// Gauge: active connections<br>	activeConnections = promauto.NewGauge(<br>		prometheus.GaugeOpts{<br>			Name: &quot;active_connections&quot;,<br>			Help: &quot;Number of active connections&quot;,<br>		},<br>	)</pre><pre>	// Counter: database errors<br>	databaseErrors = promauto.NewCounterVec(<br>		prometheus.CounterOpts{<br>			Name: &quot;database_errors_total&quot;,<br>			Help: &quot;Total number of database errors&quot;,<br>		},<br>		[]string{&quot;operation&quot;},<br>	)</pre><pre>	// Gauge: items in processing queue<br>	queueDepth = promauto.NewGauge(<br>		prometheus.GaugeOpts{<br>			Name: &quot;queue_depth&quot;,<br>			Help: &quot;Number of items in processing queue&quot;,<br>		},<br>	)<br>)</pre><pre>// Middleware to track request metrics<br>func metricsMiddleware(next http.Handler) http.Handler {<br>	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {<br>		start := time.Now()<br>		activeConnections.Inc()<br>		defer activeConnections.Dec()</pre><pre>		// Create a response writer wrapper to capture status code<br>		wrapped := &amp;responseWriter{ResponseWriter: w, statusCode: http.StatusOK}</pre><pre>		next.ServeHTTP(wrapped, r)</pre><pre>		// Record metrics<br>		duration := time.Since(start).Seconds()<br>		status := fmt.Sprintf(&quot;%d&quot;, wrapped.statusCode)</pre><pre>		httpRequestsTotal.WithLabelValues(<br>			r.Method,<br>			r.URL.Path,<br>			status,<br>		).Inc()</pre><pre>		httpRequestDuration.WithLabelValues(<br>			r.Method,<br>			r.URL.Path,<br>		).Observe(duration)<br>	})<br>}</pre><pre>type responseWriter struct {<br>	http.ResponseWriter<br>	statusCode int<br>}</pre><pre>func (rw *responseWriter) WriteHeader(code int) {<br>	rw.statusCode = code<br>	rw.ResponseWriter.WriteHeader(code)<br>}</pre><pre>// Simulate database operation<br>func simulateDatabaseOperation() error {<br>	// 10% chance of error<br>	if rand.Float32() &lt; 0.1 {<br>		databaseErrors.WithLabelValues(&quot;query&quot;).Inc()<br>		return fmt.Errorf(&quot;database error&quot;)<br>	}<br>	return nil<br>}</pre><pre>// Handlers<br>func handleGetUsers(w http.ResponseWriter, r *http.Request) {<br>	// Simulate processing time (10-100ms)<br>	delay := time.Duration(rand.Intn(90)+10) * time.Millisecond<br>	time.Sleep(delay)</pre><pre>	if err := simulateDatabaseOperation(); err != nil {<br>		http.Error(w, &quot;Database error&quot;, http.StatusInternalServerError)<br>		return<br>	}</pre><pre>	w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)<br>	fmt.Fprintf(w, `{&quot;users&quot;: [{&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;}]}`)<br>}</pre><pre>func handleGetUser(w http.ResponseWriter, r *http.Request) {<br>	delay := time.Duration(rand.Intn(50)+5) * time.Millisecond<br>	time.Sleep(delay)</pre><pre>	if err := simulateDatabaseOperation(); err != nil {<br>		http.Error(w, &quot;Database error&quot;, http.StatusInternalServerError)<br>		return<br>	}</pre><pre>	w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)<br>	fmt.Fprintf(w, `{&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;}`)<br>}</pre><pre>func handleCreateUser(w http.ResponseWriter, r *http.Request) {<br>	delay := time.Duration(rand.Intn(200)+50) * time.Millisecond<br>	time.Sleep(delay)</pre><pre>	if err := simulateDatabaseOperation(); err != nil {<br>		http.Error(w, &quot;Database error&quot;, http.StatusInternalServerError)<br>		return<br>	}</pre><pre>	w.WriteHeader(http.StatusCreated)<br>	fmt.Fprintf(w, `{&quot;id&quot;: 123}`)<br>}</pre><pre>// Simulate background job that processes queue<br>func simulateBackgroundJob() {<br>	ticker := time.NewTicker(5 * time.Second)<br>	for range ticker.C {<br>		// Random queue depth between 0-50<br>		depth := rand.Intn(51)<br>		queueDepth.Set(float64(depth))<br>	}<br>}</pre><pre>func main() {<br>	// Start background job<br>	go simulateBackgroundJob()</pre><pre>	// Setup routes with metrics middleware<br>	mux := http.NewServeMux()<br>	mux.HandleFunc(&quot;/api/users&quot;, handleGetUsers)<br>	mux.HandleFunc(&quot;/api/users/1&quot;, handleGetUser)<br>	mux.HandleFunc(&quot;/api/users/create&quot;, handleCreateUser)</pre><pre>	// Wrap all handlers with metrics middleware<br>	handler := metricsMiddleware(mux)</pre><pre>	// Expose Prometheus metrics at /metrics<br>	http.Handle(&quot;/metrics&quot;, promhttp.Handler())<br>	http.HandleFunc(&quot;/&quot;, handler.ServeHTTP)</pre><pre>	log.Println(&quot;Service started on :8080&quot;)<br>	log.Println(&quot;Metrics available at http://localhost:8080/metrics&quot;)<br>	log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))<br>}</pre><p>To run this, you need the Prometheus client library:</p><pre>go mod init user-service<br>go get github.com/prometheus/client_golang/...<br>go run main.go</pre><p>Now visit http://localhost:8080/metrics and you&#39;ll see raw Prometheus format:</p><pre># HELP http_requests_total Total number of HTTP requests<br># TYPE http_requests_total counter<br>http_requests_total{endpoint=&quot;/api/users&quot;,method=&quot;GET&quot;,status=&quot;200&quot;} 42</pre><pre># HELP http_request_duration_seconds HTTP request latency in seconds<br># TYPE http_request_duration_seconds histogram<br>http_request_duration_seconds_bucket{endpoint=&quot;/api/users&quot;,method=&quot;GET&quot;,le=&quot;0.005&quot;} 5<br>http_request_duration_seconds_bucket{endpoint=&quot;/api/users&quot;,method=&quot;GET&quot;,le=&quot;0.01&quot;} 12<br>...</pre><pre># HELP active_connections Number of active connections<br># TYPE active_connections gauge<br>active_connections 0</pre><pre># HELP database_errors_total Total number of database errors<br># TYPE database_errors_total counter<br>database_errors_total{operation=&quot;query&quot;} 3</pre><pre># HELP queue_depth Number of items in processing queue<br># TYPE queue_depth gauge<br>queue_depth 17</pre><p>This is Prometheus format. Your application is now instrumented.</p><h3>Installing and Configuring Prometheus</h3><p>Now that your application exposes metrics, you need Prometheus to scrape them.</p><h3>Installation</h3><pre># Download Prometheus<br>wget https://github.com/prometheus/prometheus/releases/download/v2.48.0/prometheus-2.48.0.linux-amd64.tar.gz<br>tar xvfz prometheus-2.48.0.linux-amd64.tar.gz<br>cd prometheus-2.48.0.linux-amd64</pre><p>Or using Docker:</p><pre>docker run -d -p 9090:9090 prom/prometheus</pre><h3>Configuration</h3><p>Create prometheus.yml:</p><pre># Global settings<br>global:<br>  scrape_interval: 15s        # How often to scrape metrics<br>  evaluation_interval: 15s    # How often to evaluate alert rules<br>  external_labels:<br>    monitor: &#39;production&#39;     # Label all metrics with this</pre><pre># Alert manager configuration (we&#39;ll use this later)<br>alerting:<br>  alertmanagers:<br>    - static_configs:<br>        - targets:<br>            - &#39;localhost:9093&#39;</pre><pre># Alert rules<br>rule_files:<br>  - &#39;alerts.yml&#39;</pre><pre># Scrape configurations<br>scrape_configs:<br>  # Prometheus itself<br>  - job_name: &#39;prometheus&#39;<br>    static_configs:<br>      - targets: [&#39;localhost:9090&#39;]</pre><pre>  # Our user service<br>  - job_name: &#39;user-service&#39;<br>    static_configs:<br>      - targets: [&#39;localhost:8080&#39;]<br>    # Override global scrape interval for this job<br>    scrape_interval: 5s<br>    scrape_timeout: 5s<br>    # Add custom labels to all metrics from this job<br>    metric_relabel_configs:<br>      - source_labels: [__name__]<br>        target_label: service<br>        replacement: &#39;user-service&#39;</pre><p>Key concepts here:</p><ul><li>scrape_interval — How often Prometheus pulls metrics (15 seconds by default)</li><li>evaluation_interval — How often alert rules are checked</li><li>job_name — Logical name for this target</li><li>targets — What to scrape (IP:port)</li><li>metric_relabel_configs — Transform metrics as they&#39;re scraped</li></ul><h3>Run Prometheus</h3><pre># If installed locally<br>./prometheus --config.file=prometheus.yml</pre><pre># If using Docker<br>docker run -d -p 9090:9090 \<br>  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \<br>  prom/prometheus</pre><p>Visit http://localhost:9090 and you&#39;ll see the Prometheus UI.</p><h3>PromQL: Asking Questions About Your Data</h3><p>Prometheus is useless if you can’t query it. That’s where PromQL comes in — a query language designed for time-series data.</p><p>Let’s explore it using the Prometheus UI. Click on the “Graph” tab and enter queries.</p><h3>Simple Queries</h3><p><strong>What’s the current queue depth?</strong></p><pre>queue_depth</pre><p>Returns: 17</p><p><strong>How many total requests have we received?</strong></p><pre>http_requests_total</pre><p>Returns all metrics with this name, broken down by labels.</p><p><strong>How many successful GET requests to /api/users?</strong></p><pre>http_requests_total{method=&quot;GET&quot;, endpoint=&quot;/api/users&quot;, status=&quot;200&quot;}</pre><h3>Ranges and Aggregation</h3><p><strong>Average response time over the last 5 minutes?</strong></p><pre>avg(rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]))</pre><p>This looks complex, but it’s calculating: total seconds / total count = average.</p><p><strong>Request rate (requests per second) over last minute?</strong></p><pre>rate(http_requests_total[1m])</pre><p>rate() calculates per-second rate of change. Over 1 minute, it shows requests/sec.</p><p><strong>Error rate for /api/users endpoint?</strong></p><pre>rate(http_requests_total{endpoint=&quot;/api/users&quot;, status=&quot;5xx&quot;}[5m]) / rate(http_requests_total{endpoint=&quot;/api/users&quot;}[5m])</pre><p>Errors divided by total = error rate. Multiply by 100 for percentage.</p><h3>Aggregation Across Labels</h3><p><strong>Total requests across all endpoints?</strong></p><pre>sum(rate(http_requests_total[5m]))</pre><p><strong>By method?</strong></p><pre>sum(rate(http_requests_total[5m])) by (method)</pre><p><strong>By endpoint?</strong></p><pre>sum(rate(http_requests_total[5m])) by (endpoint)</pre><p><strong>Top 3 slowest endpoints?</strong></p><pre>topk(3, avg(http_request_duration_seconds) by (endpoint))</pre><h3>Comparing Time Periods</h3><p><strong>How much traffic increase compared to 1 hour ago?</strong></p><pre>rate(http_requests_total[1m]) / rate(http_requests_total offset 1h [1m])</pre><p>The offset modifier lets you reference past data.</p><h3>PromQL Operators</h3><ul><li>+ - * / — Arithmetic</li><li>== != &gt; &lt; &gt;= &lt;= — Comparison</li><li>and or unless — Logical</li><li>bool — Convert comparison to 0/1</li></ul><p><strong>Which endpoints are slower than 100ms?</strong></p><pre>(http_request_duration_seconds &gt; 0.1) * bool</pre><p>The beauty of PromQL is that it’s designed for the questions you actually ask: “Is this broken? How much traffic? Compare to yesterday? Which thing is slowest?”</p><h3>Creating Dashboards</h3><p>Queries are powerful, but you don’t want to type them every time. Dashboards let you visualize multiple metrics at once.</p><p>In the Prometheus UI, click “+” and create a new dashboard. Add graphs for:</p><ol><li><strong>Request Rate</strong></li></ol><ul><li>sum(rate(http_requests_total[1m])) by (endpoint)</li></ul><ol><li><strong>Error Rate</strong></li></ol><ul><li>sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) / sum(rate(http_requests_total[5m])) * 100</li></ul><ol><li><strong>Response Time (P95)</strong></li></ol><ul><li>histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))</li></ul><ol><li><strong>Active Connections</strong></li></ol><ul><li>active_connections</li></ul><ol><li><strong>Database Errors</strong></li></ol><ul><li>rate(database_errors_total[5m])</li></ul><ol><li><strong>Queue Depth</strong></li></ol><ul><li>queue_depth</li></ul><p>Pro tip: Save your dashboard. Then share it with the team. Everyone can now see the same metrics without knowing PromQL.</p><h3>Alerting: Know When Things Break</h3><p>Metrics are great, but monitoring a dashboard 24/7 isn’t realistic. You need alerts that notify you when things go wrong.</p><p>Prometheus uses an “Alertmanager” for routing alerts, but let’s start with the alert rules themselves.</p><p>Create alerts.yml:</p><pre>groups:<br>  - name: application_alerts<br>    interval: 30s<br>    rules:<br>      # Alert if error rate &gt; 5% for 5 minutes<br>      - alert: HighErrorRate<br>        expr: |<br>          (sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) / sum(rate(http_requests_total[5m]))) &gt; 0.05<br>        for: 5m<br>        labels:<br>          severity: critical<br>        annotations:<br>          summary: &quot;High error rate detected&quot;<br>          description: &quot;Error rate is {{ $value | humanizePercentage }} for {{ $labels.job }}&quot;</pre><pre>      # Alert if p95 response time &gt; 1 second<br>      - alert: HighLatency<br>        expr: |<br>          histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) &gt; 1<br>        for: 5m<br>        labels:<br>          severity: warning<br>        annotations:<br>          summary: &quot;High latency detected&quot;<br>          description: &quot;P95 latency is {{ $value | humanizeDuration }}&quot;</pre><pre>      # Alert if database errors &gt; 5 per minute<br>      - alert: DatabaseErrors<br>        expr: |<br>          rate(database_errors_total[1m]) &gt; 0.05<br>        for: 2m<br>        labels:<br>          severity: warning<br>        annotations:<br>          summary: &quot;Database errors detected&quot;<br>          description: &quot;{{ $value | humanize }} database errors per second&quot;</pre><pre>      # Alert if queue depth &gt; 100<br>      - alert: QueueBacklog<br>        expr: |<br>          queue_depth &gt; 100<br>        for: 2m<br>        labels:<br>          severity: warning<br>        annotations:<br>          summary: &quot;Queue backlog detected&quot;<br>          description: &quot;Queue depth is {{ $value }}&quot;</pre><pre>      # Alert if service is down (no metrics for 2 minutes)<br>      - alert: ServiceDown<br>        expr: |<br>          up{job=&quot;user-service&quot;} == 0<br>        for: 2m<br>        labels:<br>          severity: critical<br>        annotations:<br>          summary: &quot;Service is down&quot;<br>          description: &quot;user-service has been unreachable for 2 minutes&quot;</pre><p>Add this to your prometheus.yml:</p><pre>rule_files:<br>  - &#39;alerts.yml&#39;</pre><p>Restart Prometheus. Visit the Alerts tab. You’ll see your alert rules and their current state (firing, pending, or inactive).</p><h3>Understanding Alert Rules</h3><p>Each alert has:</p><ul><li><strong>expr</strong> — PromQL expression that evaluates to true when alert should fire</li><li><strong>for</strong> — How long condition must be true before alerting (prevents flapping)</li><li><strong>labels</strong> — Metadata about the alert (severity, team, etc.)</li><li><strong>annotations</strong> — Human-readable description and summary</li></ul><p>The for: 5m prevents false alerts from brief spikes. The alert only fires if the condition persists for 5 minutes.</p><h3>Alertmanager: Where Alerts Go</h3><p>Prometheus evaluates alert rules, but where do the alerts go? Alertmanager routes them.</p><h3>Install Alertmanager</h3><pre>wget https://github.com/prometheus/alertmanager/releases/download/v0.26.0/alertmanager-0.26.0.linux-amd64.tar.gz<br>tar xvfz alertmanager-0.26.0.linux-amd64.tar.gz<br>cd alertmanager-0.26.0.linux-amd64</pre><p>Create alertmanager.yml:</p><pre>global:<br>  # Email configuration<br>  smtp_smarthost: &#39;smtp.gmail.com:587&#39;<br>  smtp_auth_username: &#39;your-email@gmail.com&#39;<br>  smtp_auth_password: &#39;your-app-password&#39;<br>  smtp_require_tls: true</pre><pre># Templates for alert messages<br>templates:<br>  - &#39;/etc/alertmanager/templates/*.tmpl&#39;</pre><pre># Routes define how alerts are processed<br>route:<br>  # Default receiver for all alerts<br>  receiver: &#39;team-email&#39;<br>  <br>  # Group alerts by these labels<br>  group_by: [&#39;alertname&#39;, &#39;cluster&#39;, &#39;service&#39;]<br>  <br>  # Wait this long to group alerts before sending<br>  group_wait: 30s<br>  <br>  # Wait this long after a new alert joins group<br>  group_interval: 5m<br>  <br>  # How long to wait before repeating notification<br>  repeat_interval: 4h<br>  <br>  # Sub-routes: specific handling for certain alerts<br>  routes:<br>    # Critical alerts go to ops team immediately<br>    - match:<br>        severity: critical<br>      receiver: &#39;ops-pagerduty&#39;<br>      group_wait: 10s<br>      repeat_interval: 1h<br>    <br>    # Warnings can be batched<br>    - match:<br>        severity: warning<br>      receiver: &#39;team-email&#39;<br>      group_wait: 2m</pre><pre># Receivers define where alerts are sent<br>receivers:<br>  - name: &#39;team-email&#39;<br>    email_configs:<br>      - to: &#39;team@company.com&#39;</pre><pre>  - name: &#39;ops-pagerduty&#39;<br>    pagerduty_configs:<br>      - service_key: &#39;your-pagerduty-key&#39;</pre><p>Run Alertmanager:</p><pre>./alertmanager --config.file=alertmanager.yml</pre><p>Now when Prometheus fires an alert, it sends to Alertmanager, which routes it according to your rules.</p><h3>Alert Routing Logic</h3><p>Alertmanager’s power is in routing:</p><ul><li>Group similar alerts so you get one notification instead of 100</li><li>Send critical alerts immediately to PagerDuty</li><li>Send warnings via email (batched)</li><li>Prevent alert spam with repeat intervals</li><li>Match alerts based on labels and route accordingly</li></ul><p>This is how professional teams handle alerts. You’re not overwhelmed with notifications. Critical issues wake you up. Warnings are batched.</p><h3>Production Best Practices</h3><h3>1. Use Recording Rules</h3><p>Instead of calculating the same queries repeatedly, pre-calculate them:</p><pre>groups:<br>  - name: recording_rules<br>    interval: 1m<br>    rules:<br>      # Record error rate every minute<br>      - record: http:error_rate:1m<br>        expr: sum(rate(http_requests_total{status=~&quot;5..&quot;}[1m])) / sum(rate(http_requests_total[1m]))</pre><pre>      # Record p95 latency every minute<br>      - record: http:p95_latency:1m<br>        expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m]))</pre><p>Then reference these in alerts:</p><pre>- alert: HighErrorRate<br>  expr: http:error_rate:1m &gt; 0.05</pre><p>Recording rules make queries faster and alerts more efficient.</p><h3>2. Retention Strategy</h3><p>Prometheus stores metrics in memory. Large environments need data retention limits:</p><pre>global:<br>  scrape_interval: 15s<br>  # Keep data for 30 days<br>  # Adjust based on storage available</pre><p>Run Prometheus with:</p><pre>./prometheus --storage.tsdb.retention.time=30d</pre><h3>3. Storage Considerations</h3><p>Prometheus stores roughly 1–2 bytes per sample. With 1000 metrics scraping every 15 seconds:</p><ul><li>Per day: ~172 MB</li><li>Per month: ~5 GB</li><li>Per year: ~60 GB</li></ul><p>For production, consider:</p><ul><li>Running on fast SSD</li><li>Using Prometheus remote storage for long-term retention</li><li>Archiving old data regularly</li></ul><h3>4. High Availability</h3><p>Run two independent Prometheus servers scraping the same targets. If one fails, you still have data. Tools like Thanos can deduplicate metrics from both.</p><h3>5. Naming Conventions</h3><p>Follow these conventions so metrics are discoverable:</p><pre>&lt;namespace&gt;_&lt;subsystem&gt;_&lt;name&gt;_&lt;unit&gt;</pre><pre>http_request_duration_seconds<br>database_connections_active<br>memory_usage_bytes<br>api_calls_total</pre><ul><li>No underscores between words</li><li>Include units</li><li>Use suffixes: _total, _count, _sum, _bucket, _seconds</li></ul><h3>6. Documentation</h3><p>Every metric needs documentation:</p><pre>http_requests_total = promauto.NewCounterVec(<br>	prometheus.CounterOpts{<br>		Name: &quot;http_requests_total&quot;,<br>		Help: &quot;Total number of HTTP requests processed&quot;,<br>	},<br>	[]string{&quot;method&quot;, &quot;endpoint&quot;, &quot;status&quot;},<br>)</pre><p>The Help field is critical. A year later, someone needs to understand what this metric means.</p><h3>The Observability Stack</h3><p>Prometheus is part of a larger observability picture:</p><pre>┌─────────────────────────────────────────┐<br>│        Observability Stack              │<br>├─────────────────────────────────────────┤<br>│ Metrics (Prometheus)   - What happened? │<br>│ Logs (ELK/Loki)        - Why it happened│<br>│ Traces (Jaeger)        - How it happened│<br>└─────────────────────────────────────────┘</pre><p>Prometheus gives you <em>what happened</em>: error rates, latencies, resource usage. Logs tell you the story. Traces show the path a request took.</p><p>Together, they’re unstoppable for debugging.</p><h3>From Blindness to Visibility</h3><p>Remember the question “Is the system healthy?”</p><p>With Prometheus, your answer is now:</p><pre>- Error rate: 0.1% (green)<br>- P95 latency: 45ms (green)<br>- Database errors: 0 per minute (green)<br>- Queue depth: 2 items (green)<br>- Last check: 30 seconds ago</pre><p>Specific. Quantified. Real-time. You’re no longer flying blind.</p><p>When something breaks, you see it immediately. You have data to explain what went wrong. You can correlate the outage with what changed.</p><p>This is observability.</p><h3>Practical Troubleshooting</h3><h3>“My metrics aren’t appearing”</h3><p>Check:</p><pre># Can Prometheus reach your service?<br>curl http://localhost:8080/metrics</pre><pre># Is the job in Prometheus UI?<br># Targets tab -&gt; check if &quot;up&quot; is 1 for your job</pre><p>If metrics exist but aren’t showing, you’re likely looking at a scrape timing issue.</p><h3>“Alerts aren’t firing”</h3><pre># Check prometheus.yml syntax<br>./prometheus --config.file=prometheus.yml --dry-run</pre><pre># Look at Alerts tab in Prometheus UI<br># Is the expression evaluating?<br># Is the condition actually true?</pre><p>Test your alert expression manually in the Graph tab first.</p><h3>“Alertmanager not receiving alerts”</h3><pre># Check alertmanager.yml syntax<br>./amtool config routes</pre><pre># Look at Alertmanager UI at <a href="http://localhost:9093/">http://localhost:9093</a><br># Check Alertmanager logs</pre><p>Verify your Prometheus configuration points to Alertmanager.</p><h3>Your First Week: A Learning Path</h3><p><strong>Day 1</strong>: Install Prometheus, run the demo service, view metrics at /metrics</p><p><strong>Day 2</strong>: Configure Prometheus, query basic metrics in UI (request count, latency)</p><p><strong>Day 3</strong>: Learn PromQL: error rates, percentiles, aggregations</p><p><strong>Day 4</strong>: Create dashboards visualizing your most important metrics</p><p><strong>Day 5</strong>: Write alert rules for critical scenarios (errors, latency, down)</p><p><strong>Day 6</strong>: Install Alertmanager, route alerts to email/Slack/PagerDuty</p><p><strong>Day 7</strong>: Apply to your actual application, iterate on metrics and alerts</p><p>By week two, you’ll have visibility into your production systems you’ve never had before.</p><h3>The Philosophy: Monitoring as Code</h3><p>Here’s what changed: you stopped monitoring after deployment. You started building monitoring <em>into</em> your system from day one.</p><p>Every line of code that matters gets a metric. Every critical scenario gets an alert. Your monitoring grows with your application.</p><p>Version control your alert rules. Code review them. Deploy them. This is monitoring as code.</p><h3>Alerting: Know When Things Break</h3><p>Metrics are great, but monitoring a dashboard 24/7 isn’t realistic. You need alerts that notify you when things go wrong.</p><p>Prometheus uses an “Alertmanager” for routing alerts, but let’s start with the alert rules themselves.</p><p>Create alerts.yml:</p><pre>groups:<br>  - name: application_alerts<br>    interval: 30s<br>    rules:<br>      # Alert if error rate &gt; 5% for 5 minutes<br>      - alert: HighErrorRate<br>        expr: |<br>          (sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) / sum(rate(http_requests_total[5m]))) &gt; 0.05<br>        for: 5m<br>        labels:<br>          severity: critical<br>        annotations:<br>          summary: &quot;High error rate detected&quot;<br>          description: &quot;Error rate is {{ $value | humanizePercentage }} for {{ $labels.job }}&quot;<br>      <br>      # Alert if p95 response time &gt; 1 second<br>      - alert: HighLatency<br>        expr: |<br>          histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) &gt; 1<br>        for: 5m<br>        labels:<br>          severity: warning<br>        annotations:<br>          summary: &quot;High latency detected&quot;<br>          description: &quot;P95 latency is {{ $value | humanizeDuration }}&quot;<br>      # Alert if database errors &gt; 5 per minute<br>      - alert: DatabaseErrors<br>        expr: |<br>          rate(database_errors_total[1m]) &gt; 0.05<br>        for: 2m<br>        labels:<br>          severity: warning<br>        annotations:<br>          summary: &quot;Database errors detected&quot;<br>          description: &quot;{{ $value | humanize }} database errors per second&quot;<br>      # Alert if queue depth &gt; 100<br>      - alert: QueueBacklog<br>        expr: |<br>          queue_depth &gt; 100<br>        for: 2m<br>        labels:<br>          severity: warning<br>        annotations:<br>          summary: &quot;Queue backlog detected&quot;<br>          description: &quot;Queue depth is {{ $value }}&quot;<br>      # Alert if service is down (no metrics for 2 minutes)<br>      - alert: ServiceDown<br>        expr: |<br>          up{job=&quot;user-service&quot;} == 0<br>        for: 2m<br>        labels:<br>          severity: critical<br>        annotations:<br>          summary: &quot;Service is down&quot;<br>          description: &quot;user-service has been unreachable for 2 minutes&quot;</pre><p>Add this to your prometheus.yml:</p><pre>rule_files:<br>  - &#39;alerts.yml&#39;</pre><p>Restart Prometheus. Visit the Alerts tab. You’ll see your alert rules and their current state (firing, pending, or inactive).</p><h3>Understanding Alert Rules</h3><p>Each alert has:</p><ul><li><strong>expr</strong> — PromQL expression that evaluates to true when alert should fire</li><li><strong>for</strong> — How long condition must be true before alerting (prevents flapping)</li><li><strong>labels</strong> — Metadata about the alert (severity, team, etc.)</li><li><strong>annotations</strong> — Human-readable description and summary</li></ul><p>The for: 5m prevents false alerts from brief spikes. The alert only fires if the condition persists for 5 minutes.</p><h3>Alertmanager: Where Alerts Go</h3><p>Prometheus evaluates alert rules, but where do the alerts go? Alertmanager routes them.</p><h3>Install Alertmanager</h3><pre>wget https://github.com/prometheus/alertmanager/releases/download/v0.26.0/alertmanager-0.26.0.linux-amd64.tar.gz<br>tar xvfz alertmanager-0.26.0.linux-amd64.tar.gz<br>cd alertmanager-0.26.0.linux-amd64</pre><p>Create alertmanager.yml:</p><pre>global:<br>  # Email configuration<br>  smtp_smarthost: &#39;smtp.gmail.com:587&#39;<br>  smtp_auth_username: &#39;your-email@gmail.com&#39;<br>  smtp_auth_password: &#39;your-app-password&#39;<br>  smtp_require_tls: true<br><br># Templates for alert messages<br>templates:<br>  - &#39;/etc/alertmanager/templates/*.tmpl&#39;<br># Routes define how alerts are processed<br>route:<br>  # Default receiver for all alerts<br>  receiver: &#39;team-email&#39;<br>  <br>  # Group alerts by these labels<br>  group_by: [&#39;alertname&#39;, &#39;cluster&#39;, &#39;service&#39;]<br>  <br>  # Wait this long to group alerts before sending<br>  group_wait: 30s<br>  <br>  # Wait this long after a new alert joins group<br>  group_interval: 5m<br>  <br>  # How long to wait before repeating notification<br>  repeat_interval: 4h<br>  <br>  # Sub-routes: specific handling for certain alerts<br>  routes:<br>    # Critical alerts go to ops team immediately<br>    - match:<br>        severity: critical<br>      receiver: &#39;ops-pagerduty&#39;<br>      group_wait: 10s<br>      repeat_interval: 1h<br>    <br>    # Warnings can be batched<br>    - match:<br>        severity: warning<br>      receiver: &#39;team-email&#39;<br>      group_wait: 2m<br># Receivers define where alerts are sent<br>receivers:<br>  - name: &#39;team-email&#39;<br>    email_configs:<br>      - to: &#39;team@company.com&#39;<br>  - name: &#39;ops-pagerduty&#39;<br>    pagerduty_configs:<br>      - service_key: &#39;your-pagerduty-key&#39;</pre><p>Run Alertmanager:</p><pre>./alertmanager --config.file=alertmanager.yml</pre><p>Now when Prometheus fires an alert, it sends to Alertmanager, which routes it according to your rules.</p><h3>Alert Routing Logic</h3><p>Alertmanager’s power is in routing:</p><ul><li>Group similar alerts so you get one notification instead of 100</li><li>Send critical alerts immediately to PagerDuty</li><li>Send warnings via email (batched)</li><li>Prevent alert spam with repeat intervals</li><li>Match alerts based on labels and route accordingly</li></ul><p>This is how professional teams handle alerts. You’re not overwhelmed with notifications. Critical issues wake you up. Warnings are batched.</p><h3>Production Best Practices</h3><h3>1. Use Recording Rules</h3><p>Instead of calculating the same queries repeatedly, pre-calculate them:</p><pre>groups:<br>  - name: recording_rules<br>    interval: 1m<br>    rules:<br>      # Record error rate every minute<br>      - record: http:error_rate:1m<br>        expr: sum(rate(http_requests_total{status=~&quot;5..&quot;}[1m])) / sum(rate(http_requests_total[1m]))<br>      # Record p95 latency every minute<br>      - record: http:p95_latency:1m<br>        expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m]))</pre><p>Then reference these in alerts:</p><pre>- alert: HighErrorRate<br>  expr: http:error_rate:1m &gt; 0.05</pre><p>Recording rules make queries faster and alerts more efficient.</p><h3>2. Retention Strategy</h3><p>Prometheus stores metrics in memory. Large environments need data retention limits:</p><pre>global:<br>  scrape_interval: 15s<br>  # Keep data for 30 days<br>  # Adjust based on storage available</pre><p>Run Prometheus with:</p><pre>./prometheus --storage.tsdb.retention.time=30d</pre><h3>3. Storage Considerations</h3><p>Prometheus stores roughly 1–2 bytes per sample. With 1000 metrics scraping every 15 seconds:</p><ul><li>Per day: ~172 MB</li><li>Per month: ~5 GB</li><li>Per year: ~60 GB</li></ul><p>For production, consider:</p><ul><li>Running on fast SSD</li><li>Using Prometheus remote storage for long-term retention</li><li>Archiving old data regularly</li></ul><h3>4. High Availability</h3><p>Run two independent Prometheus servers scraping the same targets. If one fails, you still have data. Tools like Thanos can deduplicate metrics from both.</p><h3>5. Naming Conventions</h3><p>Follow these conventions so metrics are discoverable:</p><pre>&lt;namespace&gt;_&lt;subsystem&gt;_&lt;name&gt;_&lt;unit&gt;<br><br>http_request_duration_seconds<br>database_connections_active<br>memory_usage_bytes<br>api_calls_total</pre><ul><li>No underscores between words</li><li>Include units</li><li>Use suffixes: _total, _count, _sum, _bucket, _seconds</li></ul><h3>6. Documentation</h3><p>Every metric needs documentation:</p><pre>http_requests_total = promauto.NewCounterVec(<br>	prometheus.CounterOpts{<br>		Name: &quot;http_requests_total&quot;,<br>		Help: &quot;Total number of HTTP requests processed&quot;,<br>	},<br>	[]string{&quot;method&quot;, &quot;endpoint&quot;, &quot;status&quot;},<br>)</pre><p>The Help field is critical. A year later, someone needs to understand what this metric means.</p><h3>The Observability Stack</h3><p>Prometheus is part of a larger observability picture:</p><pre>┌─────────────────────────────────────────┐<br>│        Observability Stack              │<br>├─────────────────────────────────────────┤<br>│ Metrics (Prometheus)   - What happened? │<br>│ Logs (ELK/Loki)        - Why it happened│<br>│ Traces (Jaeger)        - How it happened│<br>└─────────────────────────────────────────┘</pre><p>Prometheus gives you <em>what happened</em>: error rates, latencies, resource usage. Logs tell you the story. Traces show the path a request took.</p><p>Together, they’re unstoppable for debugging.</p><h3>From Blindness to Visibility</h3><p>Remember the question “Is the system healthy?”</p><p>With Prometheus, your answer is now:</p><pre>- Error rate: 0.1% (green)<br>- P95 latency: 45ms (green)<br>- Database errors: 0 per minute (green)<br>- Queue depth: 2 items (green)<br>- Last check: 30 seconds ago</pre><p>Specific. Quantified. Real-time. You’re no longer flying blind.</p><p>When something breaks, you see it immediately. You have data to explain what went wrong. You can correlate the outage with what changed.</p><p>This is observability.</p><h3>Practical Troubleshooting</h3><blockquote>“My metrics aren’t appearing”</blockquote><p>Check:</p><pre># Can Prometheus reach your service?<br>curl http://localhost:8080/metrics<br># Is the job in Prometheus UI?<br># Targets tab -&gt; check if &quot;up&quot; is 1 for your job</pre><p>If metrics exist but aren’t showing, you’re likely looking at a scrape timing issue.</p><blockquote>“Alerts aren’t firing”</blockquote><pre># Check prometheus.yml syntax<br>./prometheus --config.file=prometheus.yml --dry-run<br><br># Look at Alerts tab in Prometheus UI<br># Is the expression evaluating?<br># Is the condition actually true?</pre><p>Test your alert expression manually in the Graph tab first.</p><blockquote>“Alertmanager not receiving alerts”</blockquote><pre># Check alertmanager.yml syntax<br>./amtool config routes<br><br># Look at Alertmanager UI at http://localhost:9093<br># Check Alertmanager logs</pre><p>Verify your Prometheus configuration points to Alertmanager.</p><h3>A Learning Path to get up and running!</h3><p><strong>1</strong>: Install Prometheus, run the demo service, view metrics at /metrics</p><p><strong>2</strong>: Configure Prometheus, query basic metrics in UI (request count, latency)</p><p><strong>3</strong>: Learn PromQL: error rates, percentiles, aggregations</p><p><strong>4</strong>: Create dashboards visualizing your most important metrics</p><p><strong>5</strong>: Write alert rules for critical scenarios (errors, latency, down)</p><p><strong>6</strong>: Install Alertmanager, route alerts to email/Slack/PagerDuty</p><p><strong>7</strong>: Apply to your actual application, iterate on metrics and alerts</p><p><em>Are you ready to change the way to monitor the logs? Clap this post and share with friends!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=15feb15031b1" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>