diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..750bb3c
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,39 @@
+# Server Configuration
+PORT=8080
+HOST=0.0.0.0
+ENVIRONMENT=development # development, staging, production
+READ_TIMEOUT=15s
+WRITE_TIMEOUT=15s
+IDLE_TIMEOUT=60s
+SHUTDOWN_TIMEOUT=30s
+
+# Database Configuration
+DB_TYPE=file # file or postgres
+DB_FILE_PATH=posts.json
+
+# PostgreSQL Configuration (when DB_TYPE=postgres)
+DB_HOST=localhost
+DB_PORT=5432
+DB_USER=postgres
+DB_PASSWORD=postgres
+DB_NAME=postanalyzer
+DB_SSL_MODE=disable
+DB_MAX_CONNS=25
+DB_MIN_CONNS=5
+
+# Security Configuration
+RATE_LIMIT_REQUESTS=100
+RATE_LIMIT_WINDOW=1m
+MAX_BODY_SIZE=1048576 # 1MB in bytes
+ALLOWED_ORIGINS=* # Comma-separated list or * for all
+TRUSTED_PROXIES= # Comma-separated list of trusted proxy IPs
+
+# Logging Configuration
+LOG_LEVEL=info # debug, info, warn, error
+LOG_FORMAT=json # json or text
+LOG_OUTPUT=stdout # stdout or file path
+LOG_TIME_FORMAT=2006-01-02T15:04:05Z07:00
+
+# External API Configuration
+JSONPLACEHOLDER_URL=https://jsonplaceholder.typicode.com/posts
+HTTP_TIMEOUT=30s
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
new file mode 100644
index 0000000..2429f68
--- /dev/null
+++ b/.github/workflows/ci-cd.yml
@@ -0,0 +1,199 @@
+name: CI/CD Pipeline
+
+on:
+ push:
+ branches: [ main, master, claude/* ]
+ pull_request:
+ branches: [ main, master ]
+
+env:
+ GO_VERSION: '1.21'
+ DOCKER_IMAGE: post-analyzer
+
+jobs:
+ # Linting and code quality
+ lint:
+ name: Lint Code
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Run golangci-lint
+ uses: golangci/golangci-lint-action@v4
+ with:
+ version: latest
+ args: --timeout=5m
+
+ # Unit and integration tests
+ test:
+ name: Run Tests
+ runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_DB: testdb
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Cache Go modules
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/go-build
+ ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - name: Download dependencies
+ run: go mod download
+
+ - name: Run tests
+ run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
+ env:
+ DB_HOST: localhost
+ DB_PORT: 5432
+ DB_USER: postgres
+ DB_PASSWORD: postgres
+ DB_NAME: testdb
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ file: ./coverage.txt
+ fail_ci_if_error: false
+
+ # Build and verify
+ build:
+ name: Build Application
+ runs-on: ubuntu-latest
+ needs: [lint, test]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ env.GO_VERSION }}
+
+ - name: Build application
+ run: |
+ go build -v -ldflags="-w -s" -o post-analyzer main_new.go
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: post-analyzer
+ path: post-analyzer
+ retention-days: 1
+
+ # Docker build and push
+ docker:
+ name: Build and Push Docker Image
+ runs-on: ubuntu-latest
+ needs: [build]
+ if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ if: github.event_name == 'push'
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=sha,prefix={{branch}}-
+ type=raw,value=latest,enable={{is_default_branch}}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: ${{ github.event_name == 'push' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ # Security scanning
+ security:
+ name: Security Scan
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Run Trivy vulnerability scanner
+ uses: aquasecurity/trivy-action@master
+ with:
+ scan-type: 'fs'
+ scan-ref: '.'
+ format: 'sarif'
+ output: 'trivy-results.sarif'
+
+ - name: Upload Trivy results to GitHub Security
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: 'trivy-results.sarif'
+
+ # Deployment (optional - customize for your deployment target)
+ deploy:
+ name: Deploy Application
+ runs-on: ubuntu-latest
+ needs: [docker]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Deploy to production
+ run: |
+ echo "Add your deployment steps here"
+ echo "Examples: kubectl apply, helm upgrade, SSH to server, etc."
+ # Uncomment and customize based on your deployment method:
+ # - name: Deploy to Kubernetes
+ # run: |
+ # kubectl set image deployment/post-analyzer post-analyzer=${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE }}:latest
+ #
+ # - name: Deploy to Render
+ # run: |
+ # curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a075cea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,58 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool
+*.out
+coverage.txt
+coverage.html
+
+# Dependency directories
+vendor/
+
+# Go workspace file
+go.work
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# IDE files
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Application specific
+posts.json
+*.log
+logs/
+
+# Build output
+bin/
+dist/
+build/
+
+# Database
+*.db
+*.sqlite
+*.sqlite3
+
+# Docker volumes
+data/
+
+# Temporary files
+tmp/
+temp/
diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md
new file mode 100644
index 0000000..9d2db48
--- /dev/null
+++ b/API_DOCUMENTATION.md
@@ -0,0 +1,688 @@
+# Post Analyzer API Documentation
+
+**Version:** 2.0.0
+**Base URL:** `http://localhost:8080/api/v1`
+
+## Overview
+
+The Post Analyzer API provides a comprehensive RESTful interface for managing and analyzing posts. The API features include:
+
+- ✅ Full CRUD operations for posts
+- ✅ Advanced filtering and pagination
+- ✅ Bulk operations
+- ✅ Character frequency analytics
+- ✅ Data export (JSON/CSV)
+- ✅ Request validation and error handling
+- ✅ Rate limiting and security
+
+## Authentication
+
+Currently, the API is open and does not require authentication. Authentication will be added in a future release.
+
+## Rate Limiting
+
+- **Default Limit:** 100 requests per minute per IP
+- **Headers:** Rate limit status is available in response headers
+- **429 Response:** Returns when limit is exceeded
+
+## Base Response Format
+
+### Success Response
+```json
+{
+ "data": { /* response data */ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:30:00Z"
+ }
+}
+```
+
+### Error Response
+```json
+{
+ "error": {
+ "code": "ERROR_CODE",
+ "message": "Human readable error message",
+ "fields": {
+ "fieldName": "Field-specific error"
+ }
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:30:00Z"
+ }
+}
+```
+
+## Endpoints
+
+### 1. List Posts
+
+Retrieve a paginated list of posts with optional filtering and sorting.
+
+**Endpoint:** `GET /api/v1/posts`
+
+**Query Parameters:**
+
+| Parameter | Type | Description | Default |
+|-----------|------|-------------|---------|
+| `page` | integer | Page number | 1 |
+| `pageSize` | integer | Items per page (max 100) | 20 |
+| `userId` | integer | Filter by user ID | - |
+| `search` | string | Search in title and body | - |
+| `sortBy` | string | Sort field (id, title, createdAt, updatedAt) | id |
+| `sortOrder` | string | Sort order (asc, desc) | desc |
+
+**Example Request:**
+```bash
+curl "http://localhost:8080/api/v1/posts?page=1&pageSize=10&sortBy=createdAt&sortOrder=desc"
+```
+
+**Example Response:**
+```json
+{
+ "data": [
+ {
+ "id": 1,
+ "userId": 1,
+ "title": "Sample Post",
+ "body": "This is the post content...",
+ "createdAt": "2025-01-16T10:00:00Z",
+ "updatedAt": "2025-01-16T10:00:00Z"
+ }
+ ],
+ "pagination": {
+ "page": 1,
+ "pageSize": 10,
+ "totalItems": 100,
+ "totalPages": 10,
+ "hasNext": true,
+ "hasPrev": false
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:30:00Z"
+ }
+}
+```
+
+### 2. Get Post by ID
+
+Retrieve a single post by its ID.
+
+**Endpoint:** `GET /api/v1/posts/{id}`
+
+**Path Parameters:**
+- `id` (integer, required): Post ID
+
+**Example Request:**
+```bash
+curl http://localhost:8080/api/v1/posts/1
+```
+
+**Example Response:**
+```json
+{
+ "data": {
+ "id": 1,
+ "userId": 1,
+ "title": "Sample Post",
+ "body": "This is the post content...",
+ "createdAt": "2025-01-16T10:00:00Z",
+ "updatedAt": "2025-01-16T10:00:00Z"
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:30:00Z"
+ }
+}
+```
+
+**Error Response (404):**
+```json
+{
+ "error": {
+ "code": "NOT_FOUND",
+ "message": "Post not found"
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:30:00Z"
+ }
+}
+```
+
+### 3. Create Post
+
+Create a new post.
+
+**Endpoint:** `POST /api/v1/posts`
+
+**Request Body:**
+```json
+{
+ "userId": 1,
+ "title": "My New Post",
+ "body": "This is the content of my new post."
+}
+```
+
+**Validation Rules:**
+- `title`: Required, 1-500 characters
+- `body`: Required, 1-10,000 characters
+- `userId`: Optional, defaults to 1
+
+**Example Request:**
+```bash
+curl -X POST http://localhost:8080/api/v1/posts \
+ -H "Content-Type: application/json" \
+ -d '{
+ "userId": 1,
+ "title": "My New Post",
+ "body": "This is the content of my new post."
+ }'
+```
+
+**Example Response (201 Created):**
+```json
+{
+ "data": {
+ "id": 101,
+ "userId": 1,
+ "title": "My New Post",
+ "body": "This is the content of my new post.",
+ "createdAt": "2025-01-16T10:30:00Z",
+ "updatedAt": "2025-01-16T10:30:00Z"
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:30:00Z"
+ }
+}
+```
+
+**Validation Error Response (422):**
+```json
+{
+ "error": {
+ "code": "VALIDATION_FAILED",
+ "message": "validation failed",
+ "fields": {
+ "title": "title is required",
+ "body": "body too long (max 10000 characters)"
+ }
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:30:00Z"
+ }
+}
+```
+
+### 4. Update Post
+
+Update an existing post.
+
+**Endpoint:** `PUT /api/v1/posts/{id}`
+
+**Path Parameters:**
+- `id` (integer, required): Post ID
+
+**Request Body:**
+```json
+{
+ "title": "Updated Title",
+ "body": "Updated content..."
+}
+```
+
+**Note:** All fields are optional. Only provided fields will be updated.
+
+**Example Request:**
+```bash
+curl -X PUT http://localhost:8080/api/v1/posts/1 \
+ -H "Content-Type: application/json" \
+ -d '{
+ "title": "Updated Title",
+ "body": "Updated content..."
+ }'
+```
+
+**Example Response:**
+```json
+{
+ "data": {
+ "id": 1,
+ "userId": 1,
+ "title": "Updated Title",
+ "body": "Updated content...",
+ "createdAt": "2025-01-16T10:00:00Z",
+ "updatedAt": "2025-01-16T10:35:00Z"
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:35:00Z"
+ }
+}
+```
+
+### 5. Delete Post
+
+Delete a post by ID.
+
+**Endpoint:** `DELETE /api/v1/posts/{id}`
+
+**Path Parameters:**
+- `id` (integer, required): Post ID
+
+**Example Request:**
+```bash
+curl -X DELETE http://localhost:8080/api/v1/posts/1
+```
+
+**Example Response:**
+```json
+{
+ "message": "Post deleted successfully",
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:40:00Z"
+ }
+}
+```
+
+### 6. Bulk Create Posts
+
+Create multiple posts in a single request.
+
+**Endpoint:** `POST /api/v1/posts/bulk`
+
+**Request Body:**
+```json
+{
+ "posts": [
+ {
+ "userId": 1,
+ "title": "First Post",
+ "body": "Content of first post"
+ },
+ {
+ "userId": 1,
+ "title": "Second Post",
+ "body": "Content of second post"
+ }
+ ]
+}
+```
+
+**Limits:**
+- Minimum: 1 post
+- Maximum: 1000 posts per request
+
+**Example Request:**
+```bash
+curl -X POST http://localhost:8080/api/v1/posts/bulk \
+ -H "Content-Type: application/json" \
+ -d '{
+ "posts": [
+ {"userId": 1, "title": "Post 1", "body": "Content 1"},
+ {"userId": 1, "title": "Post 2", "body": "Content 2"}
+ ]
+ }'
+```
+
+**Example Response (201/207):**
+```json
+{
+ "data": {
+ "created": 2,
+ "failed": 0,
+ "errors": [],
+ "postIds": [102, 103]
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:45:00Z"
+ }
+}
+```
+
+**Partial Success (207 Multi-Status):**
+```json
+{
+ "data": {
+ "created": 1,
+ "failed": 1,
+ "errors": [
+ "post 2: validation failed"
+ ],
+ "postIds": [102]
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:45:00Z"
+ }
+}
+```
+
+### 7. Export Posts
+
+Export posts in JSON or CSV format.
+
+**Endpoint:** `GET /api/v1/posts/export`
+
+**Query Parameters:**
+
+| Parameter | Type | Description | Default |
+|-----------|------|-------------|---------|
+| `format` | string | Export format (json, csv) | json |
+| `userId` | integer | Filter by user ID | - |
+| `search` | string | Search in title and body | - |
+
+**Example Request (JSON):**
+```bash
+curl "http://localhost:8080/api/v1/posts/export?format=json" \
+ -o posts_export.json
+```
+
+**Example Request (CSV):**
+```bash
+curl "http://localhost:8080/api/v1/posts/export?format=csv" \
+ -o posts_export.csv
+```
+
+**JSON Response:**
+```json
+[
+ {
+ "id": 1,
+ "userId": 1,
+ "title": "Sample Post",
+ "body": "Content...",
+ "createdAt": "2025-01-16T10:00:00Z",
+ "updatedAt": "2025-01-16T10:00:00Z"
+ }
+]
+```
+
+**CSV Response:**
+```csv
+ID,UserID,Title,Body,CreatedAt,UpdatedAt
+1,1,"Sample Post","Content...","2025-01-16T10:00:00Z","2025-01-16T10:00:00Z"
+```
+
+### 8. Analyze Posts
+
+Perform character frequency analysis on all posts.
+
+**Endpoint:** `GET /api/v1/posts/analytics`
+
+**Example Request:**
+```bash
+curl http://localhost:8080/api/v1/posts/analytics
+```
+
+**Example Response:**
+```json
+{
+ "data": {
+ "totalPosts": 100,
+ "totalCharacters": 50000,
+ "uniqueChars": 95,
+ "charFrequency": {
+ "32": 8500,
+ "97": 4200,
+ "101": 6500
+ },
+ "topCharacters": [
+ {
+ "character": " ",
+ "count": 8500,
+ "frequency": 17.0
+ },
+ {
+ "character": "e",
+ "count": 6500,
+ "frequency": 13.0
+ }
+ ],
+ "statistics": {
+ "averagePostLength": 500.0,
+ "medianPostLength": 475,
+ "postsPerUser": {
+ "1": 50,
+ "2": 30
+ },
+ "timeDistribution": {
+ "morning": 25,
+ "afternoon": 40,
+ "evening": 30,
+ "night": 5
+ }
+ }
+ },
+ "meta": {
+ "requestId": "550e8400-e29b-41d4-a716-446655440000",
+ "timestamp": "2025-01-16T10:50:00Z",
+ "duration": "250ms"
+ }
+}
+```
+
+## Error Codes
+
+| Code | HTTP Status | Description |
+|------|-------------|-------------|
+| `NOT_FOUND` | 404 | Resource not found |
+| `INVALID_INPUT` | 400 | Invalid input data |
+| `VALIDATION_FAILED` | 422 | Validation failed |
+| `UNAUTHORIZED` | 401 | Authentication required |
+| `FORBIDDEN` | 403 | Insufficient permissions |
+| `CONFLICT` | 409 | Resource conflict |
+| `RATE_LIMIT_EXCEEDED` | 429 | Rate limit exceeded |
+| `INTERNAL_ERROR` | 500 | Internal server error |
+| `SERVICE_UNAVAILABLE` | 503 | Service unavailable |
+
+## Pagination
+
+All list endpoints support pagination with the following parameters:
+
+- `page`: Page number (starts at 1)
+- `pageSize`: Number of items per page (max 100, default 20)
+
+**Example:**
+```bash
+curl "http://localhost:8080/api/v1/posts?page=2&pageSize=25"
+```
+
+## Filtering
+
+List endpoints support various filters:
+
+- `userId`: Filter by user ID
+- `search`: Full-text search in title and body
+
+**Example:**
+```bash
+curl "http://localhost:8080/api/v1/posts?userId=1&search=golang"
+```
+
+## Sorting
+
+List endpoints support sorting with:
+
+- `sortBy`: Field to sort by (id, title, createdAt, updatedAt)
+- `sortOrder`: Sort direction (asc, desc)
+
+**Example:**
+```bash
+curl "http://localhost:8080/api/v1/posts?sortBy=createdAt&sortOrder=desc"
+```
+
+## Code Examples
+
+### JavaScript/Node.js
+
+```javascript
+const axios = require('axios');
+
+// Create a post
+async function createPost() {
+ try {
+ const response = await axios.post('http://localhost:8080/api/v1/posts', {
+ userId: 1,
+ title: 'My Post',
+ body: 'Post content...'
+ });
+ console.log('Created:', response.data);
+ } catch (error) {
+ console.error('Error:', error.response.data);
+ }
+}
+
+// List posts with pagination
+async function listPosts(page = 1) {
+ const response = await axios.get('http://localhost:8080/api/v1/posts', {
+ params: { page, pageSize: 20 }
+ });
+ return response.data;
+}
+```
+
+### Python
+
+```python
+import requests
+
+BASE_URL = 'http://localhost:8080/api/v1'
+
+# Create a post
+def create_post():
+ response = requests.post(f'{BASE_URL}/posts', json={
+ 'userId': 1,
+ 'title': 'My Post',
+ 'body': 'Post content...'
+ })
+ return response.json()
+
+# Get posts with filtering
+def get_posts(user_id=None, search=None):
+ params = {}
+ if user_id:
+ params['userId'] = user_id
+ if search:
+ params['search'] = search
+
+ response = requests.get(f'{BASE_URL}/posts', params=params)
+ return response.json()
+
+# Analyze posts
+def analyze_posts():
+ response = requests.get(f'{BASE_URL}/posts/analytics')
+ return response.json()
+```
+
+### Go
+
+```go
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+)
+
+const baseURL = "http://localhost:8080/api/v1"
+
+type Post struct {
+ ID int `json:"id,omitempty"`
+ UserID int `json:"userId"`
+ Title string `json:"title"`
+ Body string `json:"body"`
+}
+
+func createPost(post Post) (*Post, error) {
+ data, _ := json.Marshal(post)
+ resp, err := http.Post(baseURL+"/posts", "application/json", bytes.NewBuffer(data))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var result struct {
+ Data Post `json:"data"`
+ }
+ json.NewDecoder(resp.Body).Decode(&result)
+ return &result.Data, nil
+}
+```
+
+### cURL Examples
+
+```bash
+# Create a post
+curl -X POST http://localhost:8080/api/v1/posts \
+ -H "Content-Type: application/json" \
+ -d '{"userId":1,"title":"Test","body":"Content"}'
+
+# Get all posts
+curl http://localhost:8080/api/v1/posts
+
+# Get post by ID
+curl http://localhost:8080/api/v1/posts/1
+
+# Update post
+curl -X PUT http://localhost:8080/api/v1/posts/1 \
+ -H "Content-Type: application/json" \
+ -d '{"title":"Updated Title"}'
+
+# Delete post
+curl -X DELETE http://localhost:8080/api/v1/posts/1
+
+# Search posts
+curl "http://localhost:8080/api/v1/posts?search=golang&sortBy=createdAt&sortOrder=desc"
+
+# Export to CSV
+curl "http://localhost:8080/api/v1/posts/export?format=csv" -o export.csv
+
+# Get analytics
+curl http://localhost:8080/api/v1/posts/analytics
+```
+
+## Versioning
+
+The API uses URL path versioning. Current version is `v1`.
+
+- **v1 Endpoints:** `/api/v1/*`
+- **Default:** `/api/*` routes to v1
+
+## Best Practices
+
+1. **Always include Content-Type header** for POST/PUT requests
+2. **Handle pagination** for large datasets
+3. **Use bulk endpoints** for multiple operations
+4. **Implement retry logic** with exponential backoff
+5. **Cache responses** where appropriate
+6. **Monitor rate limits** in response headers
+7. **Validate input** before sending requests
+8. **Handle errors gracefully** with proper error messages
+
+## Rate Limit Headers
+
+```http
+X-RateLimit-Limit: 100
+X-RateLimit-Remaining: 95
+X-RateLimit-Reset: 1642348800
+```
+
+## Support
+
+For issues and feature requests, please visit:
+https://github.com/hoangsonww/Post-Analyzer-Webserver/issues
+
+---
+
+**Last Updated:** January 16, 2025
+**API Version:** 2.0.0
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d3eb103
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,56 @@
+# Build stage
+FROM golang:1.21-alpine AS builder
+
+# Install build dependencies
+RUN apk add --no-cache git ca-certificates tzdata
+
+# Set working directory
+WORKDIR /build
+
+# Copy go mod files
+COPY go.mod go.sum ./
+
+# Download dependencies
+RUN go mod download
+
+# Copy source code
+COPY . .
+
+# Build the application
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o post-analyzer main_new.go
+
+# Final stage
+FROM alpine:latest
+
+# Install runtime dependencies
+RUN apk --no-cache add ca-certificates tzdata
+
+# Create non-root user
+RUN addgroup -g 1000 appuser && \
+ adduser -D -u 1000 -G appuser appuser
+
+# Set working directory
+WORKDIR /app
+
+# Copy binary from builder
+COPY --from=builder /build/post-analyzer .
+
+# Copy templates and assets
+COPY home.html ./
+COPY assets ./assets
+
+# Create data directory
+RUN mkdir -p /app/data && chown -R appuser:appuser /app
+
+# Switch to non-root user
+USER appuser
+
+# Expose port
+EXPOSE 8080
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
+
+# Run the application
+CMD ["./post-analyzer"]
diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
new file mode 100644
index 0000000..f38d8b2
--- /dev/null
+++ b/MIGRATION_GUIDE.md
@@ -0,0 +1,372 @@
+# Migration Guide: v1.0 to v2.0 (Production Ready)
+
+This guide will help you migrate from the simple version (v1.0) to the production-ready version (v2.0) of the Post Analyzer Webserver.
+
+## What's Changed?
+
+### Major Changes
+
+1. **Application Architecture**
+ - Restructured into modular packages
+ - Introduced storage abstraction layer
+ - Added comprehensive middleware stack
+
+2. **Storage Layer**
+ - File storage now has thread-safe operations
+ - Added PostgreSQL support
+ - Automatic schema management
+
+3. **Configuration**
+ - Environment-based configuration
+ - Validation on startup
+ - Support for multiple environments
+
+4. **Observability**
+ - Structured JSON logging
+ - Prometheus metrics
+ - Request tracing with IDs
+
+5. **Security**
+ - Input validation and sanitization
+ - Rate limiting
+ - Security headers
+ - CORS configuration
+
+## Migration Steps
+
+### Step 1: Backup Your Data
+
+If you're using file storage:
+
+```bash
+# Backup your existing posts.json
+cp posts.json posts.json.backup
+```
+
+### Step 2: Update Dependencies
+
+```bash
+# Download new dependencies
+go mod download
+go mod tidy
+```
+
+### Step 3: Configuration
+
+Create a `.env` file from the example:
+
+```bash
+cp .env.example .env
+```
+
+Edit `.env` to match your setup. For file storage (similar to v1.0):
+
+```env
+# Keep using file storage
+DB_TYPE=file
+DB_FILE_PATH=posts.json
+
+# Other settings
+PORT=8080
+ENVIRONMENT=production
+LOG_LEVEL=info
+```
+
+For PostgreSQL (recommended for production):
+
+```env
+# Use PostgreSQL
+DB_TYPE=postgres
+DB_HOST=localhost
+DB_PORT=5432
+DB_USER=postgres
+DB_PASSWORD=yourpassword
+DB_NAME=postanalyzer
+
+# Other settings
+PORT=8080
+ENVIRONMENT=production
+LOG_LEVEL=info
+```
+
+### Step 4: Run the Application
+
+#### Option A: Direct Run (File Storage)
+
+```bash
+# Run with default file storage
+go run main.go
+```
+
+#### Option B: With PostgreSQL
+
+```bash
+# Start PostgreSQL with Docker
+docker run -d \
+ --name postgres \
+ -e POSTGRES_DB=postanalyzer \
+ -e POSTGRES_PASSWORD=postgres \
+ -p 5432:5432 \
+ postgres:16-alpine
+
+# Run the application
+export DB_TYPE=postgres
+export DB_PASSWORD=postgres
+go run main.go
+```
+
+#### Option C: Full Docker Stack
+
+```bash
+# Start everything with Docker Compose
+docker-compose up -d
+```
+
+This includes:
+- Application
+- PostgreSQL
+- Prometheus
+- Grafana
+
+### Step 5: Migrate Existing Data
+
+If you have existing data in `posts.json` and want to move to PostgreSQL:
+
+```bash
+# The application will automatically handle this
+# Just ensure posts.json exists in the same directory
+# On first run with DB_TYPE=postgres, the data will be available
+```
+
+Or manually import:
+
+```bash
+# Start application with file storage first
+DB_TYPE=file go run main.go
+
+# In another terminal, fetch to ensure data is in posts.json
+curl http://localhost:8080/fetch
+
+# Stop the application (Ctrl+C)
+
+# Start with PostgreSQL
+DB_TYPE=postgres DB_PASSWORD=postgres go run main.go
+
+# Fetch again to populate PostgreSQL
+curl http://localhost:8080/fetch
+```
+
+## Compatibility
+
+### API Endpoints
+
+All original endpoints remain functional:
+
+| v1.0 Endpoint | v2.0 Status | Notes |
+|---------------|-------------|-------|
+| `/` | ✅ Compatible | Enhanced with better error handling |
+| `/fetch` | ✅ Compatible | Now supports batch operations |
+| `/analyze` | ✅ Compatible | Improved performance |
+| `/add` | ✅ Compatible | Added input validation |
+
+### New Endpoints
+
+| Endpoint | Purpose |
+|----------|---------|
+| `/health` | Health check for monitoring |
+| `/readiness` | Kubernetes-style readiness probe |
+| `/metrics` | Prometheus metrics |
+
+### File Format
+
+The `posts.json` file format remains compatible:
+
+```json
+[
+ {
+ "userId": 1,
+ "id": 1,
+ "title": "Post Title",
+ "body": "Post body content"
+ }
+]
+```
+
+v2.0 adds optional fields:
+- `createdAt`: Timestamp when post was created
+- `updatedAt`: Timestamp when post was last updated
+
+These are automatically managed and backward compatible.
+
+## Feature Comparison
+
+| Feature | v1.0 | v2.0 |
+|---------|------|------|
+| Post Management | ✅ | ✅ |
+| Character Analysis | ✅ | ✅ (faster) |
+| External API Fetch | ✅ | ✅ |
+| File Storage | ✅ | ✅ (improved) |
+| Database Support | ❌ | ✅ |
+| Health Checks | ❌ | ✅ |
+| Metrics | ❌ | ✅ |
+| Structured Logging | ❌ | ✅ |
+| Input Validation | ❌ | ✅ |
+| Rate Limiting | ❌ | ✅ |
+| Security Headers | ❌ | ✅ |
+| CORS | ❌ | ✅ |
+| Graceful Shutdown | ❌ | ✅ |
+| Docker Support | ❌ | ✅ |
+| CI/CD Pipeline | ❌ | ✅ |
+| Test Suite | ❌ | ✅ |
+| API Documentation | ❌ | ✅ |
+
+## Troubleshooting
+
+### Issue: Application Won't Start
+
+**Error**: `invalid configuration: environment must be one of: development, staging, production`
+
+**Solution**: Set the ENVIRONMENT variable:
+```bash
+export ENVIRONMENT=development
+```
+
+### Issue: Database Connection Failed
+
+**Error**: `failed to ping database`
+
+**Solution**: Ensure PostgreSQL is running:
+```bash
+# Check if PostgreSQL is running
+docker ps | grep postgres
+
+# Or check locally
+pg_isready -h localhost
+```
+
+### Issue: Posts Not Showing
+
+**Symptom**: Empty home page
+
+**Solution**: Fetch posts first:
+```bash
+curl http://localhost:8080/fetch
+```
+
+### Issue: Rate Limited
+
+**Error**: 429 Too Many Requests
+
+**Solution**: Increase rate limit:
+```bash
+export RATE_LIMIT_REQUESTS=1000
+```
+
+Or wait for the rate limit window to reset (default: 1 minute).
+
+## Performance Considerations
+
+### File Storage vs PostgreSQL
+
+**File Storage**:
+- ✅ Simple setup
+- ✅ No external dependencies
+- ❌ Not suitable for high concurrency
+- ❌ No advanced querying
+
+**PostgreSQL**:
+- ✅ High concurrency support
+- ✅ ACID transactions
+- ✅ Advanced querying
+- ❌ Requires external service
+
+**Recommendation**: Use file storage for development, PostgreSQL for production.
+
+## Security Updates
+
+v2.0 includes important security improvements:
+
+1. **Input Sanitization**: All user input is sanitized
+2. **Rate Limiting**: Prevents abuse
+3. **Security Headers**: CSP, X-Frame-Options, etc.
+4. **Request Timeouts**: Prevents resource exhaustion
+
+**Action Required**: Review your CORS configuration in `.env`:
+```env
+# Development - allow all
+ALLOWED_ORIGINS=*
+
+# Production - specify allowed origins
+ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
+```
+
+## Monitoring Setup
+
+### Basic Monitoring (Logs)
+
+```bash
+# View logs in production
+tail -f /path/to/app/logs
+
+# Or with Docker
+docker logs -f post-analyzer-app
+```
+
+### Advanced Monitoring (Prometheus + Grafana)
+
+```bash
+# Start monitoring stack
+docker-compose up -d prometheus grafana
+
+# Access Grafana
+open http://localhost:3000
+# Login: admin/admin
+```
+
+Add Prometheus data source:
+1. Go to Configuration → Data Sources
+2. Add Prometheus
+3. URL: `http://prometheus:9090`
+4. Save & Test
+
+Import dashboard from `grafana-dashboard.json` (if provided).
+
+## Rollback Plan
+
+If you need to rollback to v1.0:
+
+```bash
+# Stop v2.0
+pkill post-analyzer
+
+# Restore old version
+mv main.go main_v2.go
+mv main_old.go main.go
+
+# Run v1.0
+go run main.go
+```
+
+Your data in `posts.json` remains compatible.
+
+## Getting Help
+
+- 📖 [Full Documentation](README_PRODUCTION.md)
+- 🐛 [Report Issues](https://github.com/hoangsonww/Post-Analyzer-Webserver/issues)
+- 💬 [Discussions](https://github.com/hoangsonww/Post-Analyzer-Webserver/discussions)
+
+## Next Steps
+
+After successful migration:
+
+1. ✅ Review configuration in `.env`
+2. ✅ Set up monitoring (Prometheus/Grafana)
+3. ✅ Configure backups (for PostgreSQL)
+4. ✅ Set up CI/CD pipeline
+5. ✅ Review security settings
+6. ✅ Load test your application
+7. ✅ Set up log aggregation
+
+---
+
+**Need assistance?** Open an issue on GitHub!
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8dd0735
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,164 @@
+.PHONY: help build run test clean docker-build docker-up docker-down lint format install-tools dev migrate
+
+# Variables
+APP_NAME := post-analyzer
+MAIN_FILE := main_new.go
+BINARY := $(APP_NAME)
+DOCKER_IMAGE := $(APP_NAME):latest
+GO := go
+GOFLAGS := -v
+LDFLAGS := -w -s
+
+# Colors for output
+BLUE := \033[0;34m
+GREEN := \033[0;32m
+YELLOW := \033[0;33m
+NC := \033[0m # No Color
+
+## help: Display this help message
+help:
+ @echo "$(BLUE)Post Analyzer Webserver - Makefile Commands$(NC)"
+ @echo ""
+ @grep -E '^## ' Makefile | sed 's/## / /' | column -t -s ':'
+
+## install: Install dependencies
+install:
+ @echo "$(GREEN)Installing dependencies...$(NC)"
+ $(GO) mod download
+ $(GO) mod verify
+
+## install-tools: Install development tools
+install-tools:
+ @echo "$(GREEN)Installing development tools...$(NC)"
+ $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
+ $(GO) install github.com/swaggo/swag/cmd/swag@latest
+
+## build: Build the application
+build:
+ @echo "$(GREEN)Building $(APP_NAME)...$(NC)"
+ $(GO) build $(GOFLAGS) -ldflags="$(LDFLAGS)" -o $(BINARY) $(MAIN_FILE)
+ @echo "$(GREEN)Build complete: $(BINARY)$(NC)"
+
+## run: Run the application
+run: build
+ @echo "$(GREEN)Running $(APP_NAME)...$(NC)"
+ ./$(BINARY)
+
+## dev: Run the application in development mode with file watching
+dev:
+ @echo "$(GREEN)Running in development mode...$(NC)"
+ @if command -v air > /dev/null; then \
+ air; \
+ else \
+ echo "$(YELLOW)Air not installed. Running normally...$(NC)"; \
+ $(GO) run $(MAIN_FILE); \
+ fi
+
+## test: Run all tests
+test:
+ @echo "$(GREEN)Running tests...$(NC)"
+ $(GO) test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
+
+## test-coverage: Run tests with coverage report
+test-coverage: test
+ @echo "$(GREEN)Generating coverage report...$(NC)"
+ $(GO) tool cover -html=coverage.txt -o coverage.html
+ @echo "$(GREEN)Coverage report generated: coverage.html$(NC)"
+
+## lint: Run linter
+lint:
+ @echo "$(GREEN)Running linter...$(NC)"
+ golangci-lint run --timeout=5m
+
+## format: Format Go code
+format:
+ @echo "$(GREEN)Formatting code...$(NC)"
+ $(GO) fmt ./...
+ gofmt -s -w .
+
+## clean: Clean build artifacts
+clean:
+ @echo "$(YELLOW)Cleaning build artifacts...$(NC)"
+ rm -f $(BINARY)
+ rm -f coverage.txt coverage.html
+ rm -rf dist/
+ $(GO) clean
+
+## docker-build: Build Docker image
+docker-build:
+ @echo "$(GREEN)Building Docker image...$(NC)"
+ docker build -t $(DOCKER_IMAGE) .
+
+## docker-up: Start all services with Docker Compose
+docker-up:
+ @echo "$(GREEN)Starting services...$(NC)"
+ docker-compose up -d
+ @echo "$(GREEN)Services started. Application available at http://localhost:8080$(NC)"
+ @echo "$(GREEN)Prometheus available at http://localhost:9090$(NC)"
+ @echo "$(GREEN)Grafana available at http://localhost:3000 (admin/admin)$(NC)"
+
+## docker-down: Stop all services
+docker-down:
+ @echo "$(YELLOW)Stopping services...$(NC)"
+ docker-compose down
+
+## docker-logs: View logs from all services
+docker-logs:
+ docker-compose logs -f
+
+## docker-restart: Restart all services
+docker-restart: docker-down docker-up
+
+## migrate: Run database migrations
+migrate:
+ @echo "$(GREEN)Running database migrations...$(NC)"
+ @echo "$(YELLOW)Migrations are automatically handled by the application$(NC)"
+
+## db-shell: Connect to PostgreSQL database
+db-shell:
+ @echo "$(GREEN)Connecting to database...$(NC)"
+ docker-compose exec postgres psql -U postgres -d postanalyzer
+
+## benchmark: Run benchmarks
+benchmark:
+ @echo "$(GREEN)Running benchmarks...$(NC)"
+ $(GO) test -bench=. -benchmem ./...
+
+## security: Run security checks
+security:
+ @echo "$(GREEN)Running security checks...$(NC)"
+ @if command -v gosec > /dev/null; then \
+ gosec ./...; \
+ else \
+ echo "$(YELLOW)gosec not installed. Install with: go install github.com/securego/gosec/v2/cmd/gosec@latest$(NC)"; \
+ fi
+
+## deps-update: Update dependencies
+deps-update:
+ @echo "$(GREEN)Updating dependencies...$(NC)"
+ $(GO) get -u ./...
+ $(GO) mod tidy
+
+## check: Run all checks (lint, test, security)
+check: lint test security
+ @echo "$(GREEN)All checks passed!$(NC)"
+
+## prod-build: Build for production
+prod-build:
+ @echo "$(GREEN)Building for production...$(NC)"
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -ldflags="$(LDFLAGS)" -o $(BINARY) $(MAIN_FILE)
+ @echo "$(GREEN)Production build complete$(NC)"
+
+## init: Initialize development environment
+init: install install-tools
+ @echo "$(GREEN)Creating .env file from example...$(NC)"
+ @if [ ! -f .env ]; then cp .env.example .env 2>/dev/null || echo "$(YELLOW)No .env.example found$(NC)"; fi
+ @echo "$(GREEN)Development environment ready!$(NC)"
+
+## version: Display version information
+version:
+ @echo "$(BLUE)Post Analyzer Webserver$(NC)"
+ @echo "Go version: $$($(GO) version)"
+ @echo "Git commit: $$(git rev-parse --short HEAD 2>/dev/null || echo 'N/A')"
+
+.DEFAULT_GOAL := help
diff --git a/README_PRODUCTION.md b/README_PRODUCTION.md
new file mode 100644
index 0000000..2098d99
--- /dev/null
+++ b/README_PRODUCTION.md
@@ -0,0 +1,477 @@
+# Post Analyzer Webserver - Production Ready
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Overview
+
+A **production-ready** web application built with Go for analyzing and managing posts. This application demonstrates enterprise-grade software development practices including microservices patterns, observability, security, and DevOps best practices.
+
+## Features
+
+### Core Features
+- 📝 **Post Management**: Create, read, update, and delete posts
+- 🔍 **Character Analysis**: Concurrent character frequency analysis with visualization
+- 🌐 **External API Integration**: Fetch posts from JSONPlaceholder API
+- 💾 **Flexible Storage**: Support for both file-based and PostgreSQL storage
+
+### Production Features
+- 🔒 **Security**: Input validation, XSS protection, security headers, rate limiting
+- 📊 **Observability**: Structured logging, Prometheus metrics, health checks
+- 🚀 **Performance**: Graceful shutdown, request timeouts, connection pooling
+- 🔄 **DevOps**: Docker support, CI/CD pipeline, automated testing
+- 🛡️ **Reliability**: Panic recovery, error handling, request tracing
+- ⚙️ **Configuration**: Environment-based config with validation
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Load Balancer │
+└─────────────────────────────────────────────────────────┘
+ │
+┌─────────────────────────────────────────────────────────┐
+│ Middleware Stack │
+│ ┌──────────┬──────────┬──────────┬──────────────────┐ │
+│ │ Request │ Logging │ Recovery │ Security Headers │ │
+│ │ ID │ │ │ │ │
+│ └──────────┴──────────┴──────────┴──────────────────┘ │
+│ ┌──────────┬──────────┬──────────┬──────────────────┐ │
+│ │ CORS │ Rate │ Body │ Metrics │ │
+│ │ │ Limiting │ Limit │ │ │
+│ └──────────┴──────────┴──────────┴──────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+ │
+┌─────────────────────────────────────────────────────────┐
+│ HTTP Handlers │
+│ ┌──────────┬──────────┬──────────┬──────────────────┐ │
+│ │ Health │ Posts │ Analysis │ Metrics │ │
+│ └──────────┴──────────┴──────────┴──────────────────┘ │
+└─────────────────────────────────────────────────────────┘
+ │
+┌─────────────────────────────────────────────────────────┐
+│ Storage Layer │
+│ ┌──────────────────────┬──────────────────────────────┐│
+│ │ File Storage │ PostgreSQL Storage ││
+│ └──────────────────────┴──────────────────────────────┘│
+└─────────────────────────────────────────────────────────┘
+```
+
+## Technology Stack
+
+- **Backend**: Go 1.21+
+- **Database**: PostgreSQL 16 (with file storage fallback)
+- **Monitoring**: Prometheus + Grafana
+- **Containerization**: Docker + Docker Compose
+- **CI/CD**: GitHub Actions
+- **Template Engine**: Go html/template
+- **Metrics**: Prometheus client
+- **Testing**: Go testing + table-driven tests
+
+## Quick Start
+
+### Prerequisites
+
+- Go 1.21 or higher
+- Docker and Docker Compose (optional)
+- PostgreSQL 16 (if not using Docker)
+- Make (optional, for convenience commands)
+
+### Using Docker (Recommended)
+
+```bash
+# Clone the repository
+git clone https://github.com/hoangsonww/Post-Analyzer-Webserver.git
+cd Post-Analyzer-Webserver
+
+# Start all services
+make docker-up
+
+# Or without Make
+docker-compose up -d
+```
+
+The application will be available at:
+- **Application**: http://localhost:8080
+- **Prometheus**: http://localhost:9090
+- **Grafana**: http://localhost:3000 (admin/admin)
+
+### Local Development
+
+```bash
+# Install dependencies
+make install
+
+# Run with file storage
+go run main_new.go
+
+# Run with PostgreSQL
+export DB_TYPE=postgres
+export DB_HOST=localhost
+export DB_PASSWORD=yourpassword
+go run main_new.go
+
+# Or use Make
+make run
+```
+
+## Configuration
+
+The application is configured via environment variables. See `.env.example` for all available options:
+
+```bash
+# Copy example configuration
+cp .env.example .env
+
+# Edit configuration
+nano .env
+```
+
+### Key Configuration Options
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `PORT` | 8080 | Server port |
+| `ENVIRONMENT` | development | Environment (development/staging/production) |
+| `DB_TYPE` | file | Storage type (file/postgres) |
+| `LOG_LEVEL` | info | Logging level (debug/info/warn/error) |
+| `RATE_LIMIT_REQUESTS` | 100 | Max requests per window |
+| `ALLOWED_ORIGINS` | * | CORS allowed origins |
+
+## API Endpoints
+
+### Health & Monitoring
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/health` | GET | Health check endpoint |
+| `/readiness` | GET | Readiness probe |
+| `/metrics` | GET | Prometheus metrics |
+
+### Application
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/` | GET | Home page with all posts |
+| `/fetch` | GET | Fetch posts from external API |
+| `/add` | GET/POST | Add new post form/submit |
+| `/analyze` | GET | Character frequency analysis |
+
+See [api-docs.yaml](api-docs.yaml) for complete OpenAPI documentation.
+
+## Development
+
+### Running Tests
+
+```bash
+# Run all tests
+make test
+
+# Run with coverage
+make test-coverage
+
+# Run benchmarks
+make benchmark
+```
+
+### Code Quality
+
+```bash
+# Format code
+make format
+
+# Run linter
+make lint
+
+# Run security checks
+make security
+
+# Run all checks
+make check
+```
+
+### Database Management
+
+```bash
+# Connect to database shell
+make db-shell
+
+# Run migrations (automatic on startup)
+make migrate
+```
+
+## Deployment
+
+### Docker Deployment
+
+```bash
+# Build Docker image
+make docker-build
+
+# Deploy with Docker Compose
+make docker-up
+```
+
+### Production Deployment
+
+1. **Build for production**:
+ ```bash
+ make prod-build
+ ```
+
+2. **Set environment variables**:
+ ```bash
+ export ENVIRONMENT=production
+ export DB_TYPE=postgres
+ export DB_HOST=your-db-host
+ export DB_PASSWORD=your-db-password
+ ```
+
+3. **Run the application**:
+ ```bash
+ ./post-analyzer
+ ```
+
+### Cloud Platforms
+
+#### Render.com
+1. Create a new Web Service
+2. Connect your GitHub repository
+3. Set build command: `go build -o app main_new.go`
+4. Set start command: `./app`
+5. Add environment variables
+
+#### Heroku
+```bash
+heroku create your-app-name
+heroku addons:create heroku-postgresql:hobby-dev
+git push heroku main
+```
+
+#### AWS ECS/Fargate
+```bash
+# Build and push Docker image
+docker build -t post-analyzer .
+docker tag post-analyzer:latest YOUR_ECR_REPO/post-analyzer:latest
+docker push YOUR_ECR_REPO/post-analyzer:latest
+
+# Deploy using ECS/Fargate
+aws ecs update-service --cluster your-cluster --service post-analyzer --force-new-deployment
+```
+
+## Monitoring & Observability
+
+### Metrics
+
+The application exposes Prometheus metrics at `/metrics`:
+
+- **HTTP Metrics**: Request count, duration, size
+- **Application Metrics**: Posts count, operations
+- **Database Metrics**: Query duration, connection pool
+- **Analysis Metrics**: Analysis operations and duration
+
+### Logging
+
+Structured JSON logging with contextual information:
+
+```json
+{
+ "time": "2025-01-16T10:30:00Z",
+ "level": "INFO",
+ "msg": "request completed",
+ "request_id": "550e8400-e29b-41d4-a716-446655440000",
+ "method": "GET",
+ "path": "/",
+ "status": 200,
+ "duration_ms": 45
+}
+```
+
+### Grafana Dashboards
+
+Access Grafana at http://localhost:3000 (when using Docker Compose):
+- Username: `admin`
+- Password: `admin`
+
+Import the included dashboard for visualization of:
+- Request rate and latency
+- Error rates
+- Database performance
+- Resource utilization
+
+## Security
+
+### Implemented Security Measures
+
+- ✅ Input validation and sanitization
+- ✅ XSS protection
+- ✅ Security headers (CSP, X-Frame-Options, etc.)
+- ✅ Rate limiting
+- ✅ CORS configuration
+- ✅ Panic recovery
+- ✅ Request timeouts
+- ✅ Body size limits
+- ✅ SQL injection prevention (prepared statements)
+
+### Security Best Practices
+
+1. **Never commit secrets**: Use environment variables
+2. **Update dependencies regularly**: `make deps-update`
+3. **Run security scans**: `make security`
+4. **Use HTTPS in production**: Configure reverse proxy
+5. **Review logs regularly**: Monitor for suspicious activity
+
+## Performance
+
+### Optimizations
+
+- **Concurrent processing**: Character analysis uses goroutines
+- **Connection pooling**: Database connection reuse
+- **Graceful shutdown**: No request interruption
+- **Request timeouts**: Prevent resource exhaustion
+- **Efficient JSON parsing**: Streaming decoder
+
+### Benchmarks
+
+Run benchmarks to measure performance:
+
+```bash
+make benchmark
+```
+
+## CI/CD Pipeline
+
+GitHub Actions workflow includes:
+
+1. **Lint**: Code quality checks
+2. **Test**: Unit and integration tests with coverage
+3. **Build**: Binary compilation
+4. **Security**: Vulnerability scanning
+5. **Docker**: Image building and pushing
+6. **Deploy**: Automated deployment (configurable)
+
+## Project Structure
+
+```
+Post-Analyzer-Webserver/
+├── config/ # Configuration management
+│ └── config.go
+├── internal/ # Internal packages
+│ ├── handlers/ # HTTP handlers
+│ ├── logger/ # Structured logging
+│ ├── metrics/ # Prometheus metrics
+│ ├── middleware/ # HTTP middleware
+│ └── storage/ # Storage layer (file, postgres)
+├── .github/
+│ └── workflows/ # CI/CD pipelines
+├── assets/ # Static assets
+├── main_new.go # Application entry point (production)
+├── main.go # Original simple version
+├── home.html # HTML template
+├── Dockerfile # Docker image definition
+├── docker-compose.yml # Multi-container setup
+├── Makefile # Development commands
+├── api-docs.yaml # OpenAPI specification
+└── README.md # This file
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Application won't start**
+```bash
+# Check logs
+docker-compose logs app
+
+# Verify configuration
+go run main_new.go # Will show config validation errors
+```
+
+**Database connection failed**
+```bash
+# Check PostgreSQL is running
+docker-compose ps
+
+# Test connection
+make db-shell
+```
+
+**Rate limit errors**
+```bash
+# Increase rate limit
+export RATE_LIMIT_REQUESTS=1000
+```
+
+## Contributing
+
+We welcome contributions! Please follow these steps:
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. Make your changes
+4. Run tests (`make check`)
+5. Commit your changes (`git commit -m 'Add amazing feature'`)
+6. Push to the branch (`git push origin feature/amazing-feature`)
+7. Open a Pull Request
+
+### Development Guidelines
+
+- Follow Go best practices and idioms
+- Write tests for new features
+- Update documentation
+- Ensure all checks pass (`make check`)
+- Use conventional commits
+
+## Roadmap
+
+- [ ] REST API for programmatic access
+- [ ] GraphQL API support
+- [ ] User authentication and authorization
+- [ ] Multi-user support
+- [ ] Advanced analytics dashboard
+- [ ] Export functionality (CSV, PDF)
+- [ ] Real-time updates with WebSockets
+- [ ] Mobile app (React Native)
+- [ ] Kubernetes deployment manifests
+- [ ] Terraform infrastructure as code
+
+## License
+
+Distributed under the MIT License. See `LICENSE` for more information.
+
+## Acknowledgements
+
+- [Go](https://golang.org/)
+- [JSONPlaceholder](https://jsonplaceholder.typicode.com/)
+- [Prometheus](https://prometheus.io/)
+- [PostgreSQL](https://www.postgresql.org/)
+- [Docker](https://www.docker.com/)
+
+## Support
+
+For support, please:
+- 📧 Open an issue on GitHub
+- 💬 Start a discussion
+- 📖 Check the documentation
+
+## Contact
+
+Son Nguyen - [@hoangsonww](https://github.com/hoangsonww)
+
+Project Link: [https://github.com/hoangsonww/Post-Analyzer-Webserver](https://github.com/hoangsonww/Post-Analyzer-Webserver)
+
+Live Demo: [https://post-analyzer-webserver.onrender.com](https://post-analyzer-webserver.onrender.com)
+
+---
+
+Created with ❤️ by [Son Nguyen](https://github.com/hoangsonww) in 2024-2025.
diff --git a/api-docs.yaml b/api-docs.yaml
new file mode 100644
index 0000000..55a7cf3
--- /dev/null
+++ b/api-docs.yaml
@@ -0,0 +1,336 @@
+openapi: 3.0.3
+info:
+ title: Post Analyzer Webserver API
+ description: |
+ A production-ready web application for analyzing and managing posts.
+
+ Features:
+ - Fetch posts from external APIs
+ - Store and manage posts
+ - Character frequency analysis
+ - Prometheus metrics
+ - Health checks
+ version: 2.0.0
+ contact:
+ name: Post Analyzer Team
+ url: https://github.com/hoangsonww/Post-Analyzer-Webserver
+ license:
+ name: MIT
+ url: https://opensource.org/licenses/MIT
+
+servers:
+ - url: http://localhost:8080
+ description: Development server
+ - url: https://post-analyzer-webserver.onrender.com
+ description: Production server
+
+tags:
+ - name: Health
+ description: Health and readiness checks
+ - name: Posts
+ description: Post management operations
+ - name: Analysis
+ description: Character analysis operations
+ - name: Metrics
+ description: Prometheus metrics
+
+paths:
+ /health:
+ get:
+ tags:
+ - Health
+ summary: Health check
+ description: Returns the health status of the application
+ operationId: healthCheck
+ responses:
+ '200':
+ description: Application is healthy
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ example: healthy
+ timestamp:
+ type: string
+ format: date-time
+
+ /readiness:
+ get:
+ tags:
+ - Health
+ summary: Readiness check
+ description: Returns whether the application is ready to serve traffic
+ operationId: readinessCheck
+ responses:
+ '200':
+ description: Application is ready
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ example: ready
+ timestamp:
+ type: string
+ format: date-time
+ '503':
+ description: Application is not ready
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /metrics:
+ get:
+ tags:
+ - Metrics
+ summary: Prometheus metrics
+ description: Returns Prometheus metrics for monitoring
+ operationId: getMetrics
+ responses:
+ '200':
+ description: Metrics in Prometheus format
+ content:
+ text/plain:
+ schema:
+ type: string
+
+ /:
+ get:
+ tags:
+ - Posts
+ summary: Home page
+ description: Displays the home page with all posts
+ operationId: getHome
+ responses:
+ '200':
+ description: HTML page with posts
+ content:
+ text/html:
+ schema:
+ type: string
+
+ /fetch:
+ get:
+ tags:
+ - Posts
+ summary: Fetch posts from external API
+ description: Fetches posts from JSONPlaceholder API and stores them
+ operationId: fetchPosts
+ responses:
+ '200':
+ description: Posts fetched successfully
+ content:
+ text/html:
+ schema:
+ type: string
+ '500':
+ description: Failed to fetch or store posts
+ content:
+ text/html:
+ schema:
+ type: string
+
+ /add:
+ get:
+ tags:
+ - Posts
+ summary: Display add post form
+ description: Shows the form to add a new post
+ operationId: showAddPostForm
+ responses:
+ '200':
+ description: Add post form
+ content:
+ text/html:
+ schema:
+ type: string
+
+ post:
+ tags:
+ - Posts
+ summary: Add a new post
+ description: Creates a new post with the provided title and body
+ operationId: addPost
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ required:
+ - title
+ - body
+ properties:
+ title:
+ type: string
+ maxLength: 500
+ description: Post title
+ example: My First Post
+ body:
+ type: string
+ maxLength: 10000
+ description: Post body
+ example: This is the content of my first post
+ responses:
+ '200':
+ description: Post added successfully
+ content:
+ text/html:
+ schema:
+ type: string
+ '400':
+ description: Invalid input
+ content:
+ text/html:
+ schema:
+ type: string
+ '500':
+ description: Failed to create post
+ content:
+ text/html:
+ schema:
+ type: string
+
+ /analyze:
+ get:
+ tags:
+ - Analysis
+ summary: Analyze character frequency
+ description: Performs character frequency analysis on all posts
+ operationId: analyzePosts
+ responses:
+ '200':
+ description: Analysis results
+ content:
+ text/html:
+ schema:
+ type: string
+ '500':
+ description: Analysis failed
+ content:
+ text/html:
+ schema:
+ type: string
+
+components:
+ schemas:
+ Post:
+ type: object
+ required:
+ - userId
+ - id
+ - title
+ - body
+ properties:
+ userId:
+ type: integer
+ description: ID of the user who created the post
+ example: 1
+ id:
+ type: integer
+ description: Unique post identifier
+ example: 1
+ title:
+ type: string
+ maxLength: 500
+ description: Post title
+ example: Sample Post Title
+ body:
+ type: string
+ maxLength: 10000
+ description: Post content
+ example: This is the body of the post with some interesting content.
+ createdAt:
+ type: string
+ format: date-time
+ description: Timestamp when post was created
+ updatedAt:
+ type: string
+ format: date-time
+ description: Timestamp when post was last updated
+
+ Error:
+ type: object
+ properties:
+ error:
+ type: string
+ description: Error message
+ example: Internal server error
+ timestamp:
+ type: string
+ format: date-time
+ description: When the error occurred
+
+ HealthStatus:
+ type: object
+ properties:
+ status:
+ type: string
+ enum: [healthy, unhealthy, ready, not ready]
+ description: Status of the service
+ timestamp:
+ type: string
+ format: date-time
+ description: Timestamp of the status check
+
+ securitySchemes:
+ ApiKey:
+ type: apiKey
+ in: header
+ name: X-API-Key
+ description: API key for authentication (not currently implemented)
+
+ parameters:
+ RequestID:
+ name: X-Request-ID
+ in: header
+ description: Unique request identifier for tracing
+ schema:
+ type: string
+ format: uuid
+
+ responses:
+ NotFound:
+ description: Resource not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ BadRequest:
+ description: Invalid request parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ InternalError:
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ TooManyRequests:
+ description: Rate limit exceeded
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ example: Rate limit exceeded. Please try again later.
+
+x-readme:
+ samples-languages:
+ - curl
+ - javascript
+ - python
+ - go
diff --git a/assets/post-analyzer.go b/assets/post-analyzer.go
index 02c7079..9e92582 100644
--- a/assets/post-analyzer.go
+++ b/assets/post-analyzer.go
@@ -45,7 +45,7 @@ func main() {
http.HandleFunc("/add", AddPostHandler)
fmt.Println("Server starting at http://localhost:8080/")
- http.ListenAndServe(":8080", nil)
+ _ = http.ListenAndServe(":8080", nil)
}
// HomeHandler serves the home page
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..218c171
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,227 @@
+package config
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "time"
+)
+
+// Config holds all configuration for the application
+type Config struct {
+ Server ServerConfig
+ Database DatabaseConfig
+ Security SecurityConfig
+ Logging LoggingConfig
+ External ExternalConfig
+}
+
+// ServerConfig contains server-related configuration
+type ServerConfig struct {
+ Port string
+ Host string
+ ReadTimeout time.Duration
+ WriteTimeout time.Duration
+ IdleTimeout time.Duration
+ ShutdownTimeout time.Duration
+ Environment string
+}
+
+// DatabaseConfig contains database-related configuration
+type DatabaseConfig struct {
+ Type string // "file" or "postgres"
+ FilePath string
+ Host string
+ Port string
+ User string
+ Password string
+ DBName string
+ SSLMode string
+ MaxConns int
+ MinConns int
+}
+
+// SecurityConfig contains security-related configuration
+type SecurityConfig struct {
+ RateLimitRequests int
+ RateLimitWindow time.Duration
+ MaxBodySize int64
+ AllowedOrigins []string
+ TrustedProxies []string
+}
+
+// LoggingConfig contains logging-related configuration
+type LoggingConfig struct {
+ Level string
+ Format string // "json" or "text"
+ Output string // "stdout" or file path
+ TimeFormat string
+}
+
+// ExternalConfig contains external service configuration
+type ExternalConfig struct {
+ JSONPlaceholderURL string
+ HTTPTimeout time.Duration
+}
+
+// Load reads configuration from environment variables with sensible defaults
+func Load() (*Config, error) {
+ cfg := &Config{
+ Server: ServerConfig{
+ Port: getEnv("PORT", "8080"),
+ Host: getEnv("HOST", "0.0.0.0"),
+ ReadTimeout: getDurationEnv("READ_TIMEOUT", 15*time.Second),
+ WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 15*time.Second),
+ IdleTimeout: getDurationEnv("IDLE_TIMEOUT", 60*time.Second),
+ ShutdownTimeout: getDurationEnv("SHUTDOWN_TIMEOUT", 30*time.Second),
+ Environment: getEnv("ENVIRONMENT", "development"),
+ },
+ Database: DatabaseConfig{
+ Type: getEnv("DB_TYPE", "file"),
+ FilePath: getEnv("DB_FILE_PATH", "posts.json"),
+ Host: getEnv("DB_HOST", "localhost"),
+ Port: getEnv("DB_PORT", "5432"),
+ User: getEnv("DB_USER", "postgres"),
+ Password: getEnv("DB_PASSWORD", ""),
+ DBName: getEnv("DB_NAME", "postanalyzer"),
+ SSLMode: getEnv("DB_SSL_MODE", "disable"),
+ MaxConns: getIntEnv("DB_MAX_CONNS", 25),
+ MinConns: getIntEnv("DB_MIN_CONNS", 5),
+ },
+ Security: SecurityConfig{
+ RateLimitRequests: getIntEnv("RATE_LIMIT_REQUESTS", 100),
+ RateLimitWindow: getDurationEnv("RATE_LIMIT_WINDOW", 1*time.Minute),
+ MaxBodySize: getInt64Env("MAX_BODY_SIZE", 1*1024*1024), // 1MB
+ AllowedOrigins: getSliceEnv("ALLOWED_ORIGINS", []string{"*"}),
+ TrustedProxies: getSliceEnv("TRUSTED_PROXIES", []string{}),
+ },
+ Logging: LoggingConfig{
+ Level: getEnv("LOG_LEVEL", "info"),
+ Format: getEnv("LOG_FORMAT", "json"),
+ Output: getEnv("LOG_OUTPUT", "stdout"),
+ TimeFormat: getEnv("LOG_TIME_FORMAT", time.RFC3339),
+ },
+ External: ExternalConfig{
+ JSONPlaceholderURL: getEnv("JSONPLACEHOLDER_URL", "https://jsonplaceholder.typicode.com/posts"),
+ HTTPTimeout: getDurationEnv("HTTP_TIMEOUT", 30*time.Second),
+ },
+ }
+
+ if err := cfg.Validate(); err != nil {
+ return nil, fmt.Errorf("invalid configuration: %w", err)
+ }
+
+ return cfg, nil
+}
+
+// Validate checks if the configuration is valid
+func (c *Config) Validate() error {
+ // Validate server config
+ if c.Server.Port == "" {
+ return fmt.Errorf("server port cannot be empty")
+ }
+ if c.Server.Environment != "development" && c.Server.Environment != "staging" && c.Server.Environment != "production" {
+ return fmt.Errorf("environment must be one of: development, staging, production")
+ }
+
+ // Validate database config
+ if c.Database.Type != "file" && c.Database.Type != "postgres" {
+ return fmt.Errorf("database type must be 'file' or 'postgres'")
+ }
+ if c.Database.Type == "file" && c.Database.FilePath == "" {
+ return fmt.Errorf("database file path cannot be empty when using file storage")
+ }
+ if c.Database.Type == "postgres" {
+ if c.Database.Host == "" || c.Database.DBName == "" {
+ return fmt.Errorf("database host and name are required for postgres")
+ }
+ }
+
+ // Validate security config
+ if c.Security.RateLimitRequests <= 0 {
+ return fmt.Errorf("rate limit requests must be positive")
+ }
+ if c.Security.MaxBodySize <= 0 {
+ return fmt.Errorf("max body size must be positive")
+ }
+
+ // Validate logging config
+ validLogLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
+ if !validLogLevels[c.Logging.Level] {
+ return fmt.Errorf("log level must be one of: debug, info, warn, error")
+ }
+ if c.Logging.Format != "json" && c.Logging.Format != "text" {
+ return fmt.Errorf("log format must be 'json' or 'text'")
+ }
+
+ return nil
+}
+
+// IsDevelopment returns true if running in development mode
+func (c *Config) IsDevelopment() bool {
+ return c.Server.Environment == "development"
+}
+
+// IsProduction returns true if running in production mode
+func (c *Config) IsProduction() bool {
+ return c.Server.Environment == "production"
+}
+
+// Helper functions for reading environment variables
+
+func getEnv(key, defaultValue string) string {
+ if value := os.Getenv(key); value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+func getIntEnv(key string, defaultValue int) int {
+ if value := os.Getenv(key); value != "" {
+ if intVal, err := strconv.Atoi(value); err == nil {
+ return intVal
+ }
+ }
+ return defaultValue
+}
+
+func getInt64Env(key string, defaultValue int64) int64 {
+ if value := os.Getenv(key); value != "" {
+ if intVal, err := strconv.ParseInt(value, 10, 64); err == nil {
+ return intVal
+ }
+ }
+ return defaultValue
+}
+
+func getDurationEnv(key string, defaultValue time.Duration) time.Duration {
+ if value := os.Getenv(key); value != "" {
+ if duration, err := time.ParseDuration(value); err == nil {
+ return duration
+ }
+ }
+ return defaultValue
+}
+
+func getSliceEnv(key string, defaultValue []string) []string {
+ if value := os.Getenv(key); value != "" {
+ // Simple comma-separated parsing
+ result := []string{}
+ current := ""
+ for _, char := range value {
+ if char == ',' {
+ if current != "" {
+ result = append(result, current)
+ current = ""
+ }
+ } else {
+ current += string(char)
+ }
+ }
+ if current != "" {
+ result = append(result, current)
+ }
+ return result
+ }
+ return defaultValue
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..6db7124
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,123 @@
+version: '3.8'
+
+services:
+ # PostgreSQL database
+ postgres:
+ image: postgres:16-alpine
+ container_name: post-analyzer-db
+ environment:
+ POSTGRES_DB: postanalyzer
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ networks:
+ - app-network
+
+ # Post Analyzer application
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: post-analyzer-app
+ environment:
+ # Server configuration
+ PORT: 8080
+ HOST: 0.0.0.0
+ ENVIRONMENT: production
+ READ_TIMEOUT: 15s
+ WRITE_TIMEOUT: 15s
+ SHUTDOWN_TIMEOUT: 30s
+
+ # Database configuration
+ DB_TYPE: postgres
+ DB_HOST: postgres
+ DB_PORT: 5432
+ DB_USER: postgres
+ DB_PASSWORD: postgres
+ DB_NAME: postanalyzer
+ DB_SSL_MODE: disable
+ DB_MAX_CONNS: 25
+ DB_MIN_CONNS: 5
+
+ # Security configuration
+ RATE_LIMIT_REQUESTS: 100
+ RATE_LIMIT_WINDOW: 1m
+ MAX_BODY_SIZE: 1048576
+ ALLOWED_ORIGINS: "*"
+
+ # Logging configuration
+ LOG_LEVEL: info
+ LOG_FORMAT: json
+ LOG_OUTPUT: stdout
+
+ # External API configuration
+ JSONPLACEHOLDER_URL: https://jsonplaceholder.typicode.com/posts
+ HTTP_TIMEOUT: 30s
+ ports:
+ - "8080:8080"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ restart: unless-stopped
+ networks:
+ - app-network
+ healthcheck:
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 10s
+
+ # Prometheus for metrics
+ prometheus:
+ image: prom/prometheus:latest
+ container_name: post-analyzer-prometheus
+ command:
+ - '--config.file=/etc/prometheus/prometheus.yml'
+ - '--storage.tsdb.path=/prometheus'
+ - '--web.console.libraries=/usr/share/prometheus/console_libraries'
+ - '--web.console.templates=/usr/share/prometheus/consoles'
+ ports:
+ - "9090:9090"
+ volumes:
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
+ - prometheus_data:/prometheus
+ depends_on:
+ - app
+ restart: unless-stopped
+ networks:
+ - app-network
+
+ # Grafana for visualization
+ grafana:
+ image: grafana/grafana:latest
+ container_name: post-analyzer-grafana
+ environment:
+ - GF_SECURITY_ADMIN_PASSWORD=admin
+ - GF_USERS_ALLOW_SIGN_UP=false
+ ports:
+ - "3000:3000"
+ volumes:
+ - grafana_data:/var/lib/grafana
+ depends_on:
+ - prometheus
+ restart: unless-stopped
+ networks:
+ - app-network
+
+volumes:
+ postgres_data:
+ prometheus_data:
+ grafana_data:
+
+networks:
+ app-network:
+ driver: bridge
diff --git a/go.mod b/go.mod
index be801a6..4f328e8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,19 @@
module Post_Analyzer_Webserver
go 1.19
+
+require (
+ github.com/google/uuid v1.6.0
+ github.com/lib/pq v1.10.9
+ github.com/prometheus/client_golang v1.19.0
+)
+
+require (
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.48.0 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
+ golang.org/x/sys v0.16.0 // indirect
+ google.golang.org/protobuf v1.32.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f7751bd
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,22 @@
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
diff --git a/home.html b/home.html
index 511c5df..2b00392 100644
--- a/home.html
+++ b/home.html
@@ -32,6 +32,9 @@
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: background-color 0.3s, box-shadow 0.3s;
+ cursor: pointer;
+ border: none;
+ font-size: 14px;
}
.button:hover {
background-color: #0056b3;
@@ -91,6 +94,44 @@
max-height: 400px;
margin: 20px auto;
}
+ #analysisSection {
+ display: none;
+ }
+ .stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 15px;
+ margin: 20px 0;
+ }
+ .stat-card {
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 8px;
+ border-left: 4px solid #007bff;
+ }
+ .stat-label {
+ font-size: 12px;
+ color: #666;
+ margin-bottom: 5px;
+ }
+ .stat-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #0056b3;
+ }
+ .top-chars {
+ max-height: 300px;
+ overflow-y: auto;
+ }
+ .char-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 8px;
+ border-bottom: 1px solid #eee;
+ }
+ .char-item:hover {
+ background: #f8f9fa;
+ }
@@ -101,7 +142,7 @@ Post Viewer and Analyzer
@@ -113,12 +154,12 @@
Welcome to the Post Viewer and Analyzer!
This application allows you to:
- Fetch posts from an external API and view them.
- - Analyze character frequency in the posts.
+ - Analyze character frequency in the posts (calculated in your browser).
- Add new posts to the existing list.
{{if .HasPosts}}
Recent Posts:
-
+
{{range .Posts}}
{{.Title}}
@@ -142,7 +183,7 @@
Recent Posts:
{{else if .HasPosts}}
-
+
Fetched Posts:
{{range .Posts}}
@@ -152,72 +193,225 @@
Fetched Posts:
{{end}}
{{end}}
- {{if .HasAnalysis}}
-
-
Character Frequency Analysis:
+
+
+
+
Character Frequency Analysis (Client-Side)
+
+
+
+
+
+
Unique Characters
+
0
+
+
+
Average Post Length
+
0
+
+
+
-
-
-
Character Frequencies:
-
- {{range $key, $value := .CharFreq}}
-
'{{printf "%q" $key}}': {{$value}}
- {{end}}
-
-
-
-
+ }
+ }
+ });
+
+ // Scroll to analysis
+ document.getElementById('analysisSection').scrollIntoView({ behavior: 'smooth' });
+ }
+
+ // If on analyze page with backend data, still use it
+ {{if .HasAnalysis}}
+ window.addEventListener('DOMContentLoaded', function() {
+ const charFreq = {{.CharFreq | toJSON}};
+ const ctx = document.getElementById('charFreqChart').getContext('2d');
+ const labels = Object.keys(charFreq).map(key => String.fromCharCode(key));
+ const data = Object.values(charFreq);
+
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: labels,
+ datasets: [{
+ label: 'Character Frequency',
+ data: data,
+ backgroundColor: 'rgba(54, 162, 235, 0.5)',
+ borderColor: 'rgba(54, 162, 235, 1)',
+ borderWidth: 1
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: 'Characters'
+ },
+ grid: {
+ display: false
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Frequency'
+ },
+ beginAtZero: true,
+ grid: {
+ color: 'rgba(200, 200, 200, 0.2)'
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top'
+ }
+ }
+ }
+ });
+ });
{{end}}
-
+