diff --git a/README.md b/README.md index 49eb640..10814d8 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,36 @@ This repository contains official and community-contributed plugins that extend ``` streamspace-plugins/ -├── official/ # Official StreamSpace plugins -│ ├── session-recorder/ -│ ├── audit-logger/ -│ └── slack-integration/ -├── community/ # Community-contributed plugins -│ ├── github-integration/ -│ └── custom-theme/ -├── catalog.yaml # Plugin discovery metadata -├── README.md # This file -└── CONTRIBUTING.md # Contribution guidelines +├── streamspace-slack/ # Slack integration plugin +├── streamspace-teams/ # Microsoft Teams plugin +├── streamspace-discord/ # Discord integration plugin +├── streamspace-pagerduty/ # PagerDuty integration plugin +├── streamspace-email/ # SMTP email plugin +├── streamspace-calendar/ # Calendar integration plugin +├── streamspace-datadog/ # Datadog monitoring plugin +├── streamspace-newrelic/ # New Relic monitoring plugin +├── streamspace-sentry/ # Sentry error tracking plugin +├── streamspace-elastic-apm/ # Elastic APM plugin +├── streamspace-honeycomb/ # Honeycomb observability plugin +├── streamspace-compliance/ # Compliance framework plugin +├── streamspace-dlp/ # Data loss prevention plugin +├── streamspace-audit-advanced/ # Advanced audit logging plugin +├── streamspace-recording/ # Session recording plugin +├── streamspace-snapshots/ # Session snapshots plugin +├── streamspace-multi-monitor/ # Multi-monitor support plugin +├── streamspace-workflows/ # Workflow automation plugin +├── streamspace-analytics-advanced/ # Advanced analytics plugin +├── streamspace-auth-saml/ # SAML authentication plugin +├── streamspace-auth-oauth/ # OAuth/OIDC authentication plugin +├── streamspace-storage-s3/ # S3 storage backend plugin +├── streamspace-storage-azure/ # Azure storage backend plugin +├── streamspace-storage-gcs/ # Google Cloud Storage plugin +├── streamspace-billing/ # Billing & usage tracking plugin +├── streamspace-node-manager/ # Kubernetes node manager plugin +├── catalog.yaml # Plugin discovery metadata +├── claude.md # AI context documentation +├── README.md # This file +└── CONTRIBUTING.md # Contribution guidelines ``` ## Plugin Categories @@ -62,10 +82,10 @@ repositories: ```bash # Using StreamSpace CLI -streamspace plugin install session-recorder +streamspace plugin install streamspace-recording # Or manually -kubectl apply -f https://raw.githubusercontent.com/JoshuaAFerguson/streamspace-plugins/main/official/session-recorder/manifest.yaml +kubectl apply -f https://raw.githubusercontent.com/JoshuaAFerguson/streamspace-plugins/main/streamspace-recording/manifest.yaml ``` ### List Available Plugins @@ -165,9 +185,9 @@ module.exports = { git clone https://github.com/JoshuaAFerguson/streamspace-plugins.git cd streamspace-plugins -# Create plugin directory (community or official) -mkdir community/my-plugin -cd community/my-plugin +# Create plugin directory +mkdir streamspace-my-plugin +cd streamspace-my-plugin # Initialize npm init -y @@ -218,7 +238,7 @@ curl -X POST https://streamspace.local/api/plugins/my-plugin/enable \ ### 3. Submit for Inclusion 1. Fork this repository -2. Add your plugin to `community/` +2. Add your plugin directory (e.g., `streamspace-my-plugin/`) 3. Update `catalog.yaml` 4. Submit a pull request @@ -288,7 +308,7 @@ See [Security Guidelines](CONTRIBUTING.md#security) for details. ### Webhook Plugin - Slack Notifications ```javascript -// official/slack-integration/index.js +// streamspace-slack/index.js module.exports = { async onLoad({ api, config }) { const webhookUrl = config.get('slackWebhookUrl'); diff --git a/catalog.yaml b/catalog.yaml index 5c83759..4c6671c 100644 --- a/catalog.yaml +++ b/catalog.yaml @@ -13,14 +13,14 @@ spec: description: Plugins maintained by the StreamSpace team icon: ✓ verified: true - plugins: 0 # Will be updated as plugins are added + plugins: 26 - name: community displayName: Community Plugins description: Community-contributed plugins icon: 🌟 verified: false - plugins: 0 # Will be updated as plugins are added + plugins: 0 pluginTypes: - type: extension @@ -116,30 +116,427 @@ spec: - name: audit.violation description: Fired when an audit violation occurs - # Plugin listings (will be populated as plugins are added) - plugins: [] - # Example structure: - # - name: session-recorder - # category: official - # displayName: Session Recorder - # description: Record and replay user sessions - # type: extension - # author: StreamSpace Team - # version: "1.0.0" - # path: official/session-recorder - # permissions: - # - read:sessions - # - write:sessions - # tags: [recording, replay, audit] - # verified: true - # downloads: 0 - # rating: 0.0 + # Plugin listings + plugins: + # Integrations + - name: streamspace-slack + category: official + displayName: Slack Integration + description: Send session and user event notifications to Slack channels + type: webhook + author: StreamSpace Team + version: "1.0.0" + path: streamspace-slack + permissions: + - network + tags: [notifications, slack, integration, messaging] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-teams + category: official + displayName: Microsoft Teams Integration + description: Send notifications to Microsoft Teams channels + type: webhook + author: StreamSpace Team + version: "1.0.0" + path: streamspace-teams + permissions: + - network + tags: [notifications, teams, microsoft, integration] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-discord + category: official + displayName: Discord Integration + description: Send notifications to Discord channels + type: webhook + author: StreamSpace Team + version: "1.0.0" + path: streamspace-discord + permissions: + - network + tags: [notifications, discord, integration] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-pagerduty + category: official + displayName: PagerDuty Integration + description: Send incident alerts to PagerDuty for critical events + type: webhook + author: StreamSpace Team + version: "1.0.0" + path: streamspace-pagerduty + permissions: + - network + tags: [monitoring, pagerduty, alerting, incidents] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-email + category: official + displayName: Email SMTP Integration + description: Send email notifications via SMTP for session and user events + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-email + permissions: + - network + tags: [email, smtp, notifications, alerts] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-calendar + category: official + displayName: Calendar Integration + description: Integrate Google Calendar and Outlook Calendar with automated session scheduling and iCal export + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-calendar + permissions: + - network + tags: [calendar, scheduling, google, outlook, ical] + verified: true + downloads: 0 + rating: 0.0 + + # Monitoring + - name: streamspace-datadog + category: official + displayName: Datadog Monitoring + description: Send metrics, traces, and logs to Datadog for comprehensive observability + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-datadog + permissions: + - network + tags: [monitoring, datadog, metrics, apm, observability] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-newrelic + category: official + displayName: New Relic Monitoring + description: Send performance metrics, traces, and events to New Relic for full-stack observability + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-newrelic + permissions: + - network + tags: [monitoring, newrelic, apm, metrics, observability] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-sentry + category: official + displayName: Sentry Error Tracking + description: Track errors, exceptions, and performance issues with Sentry integration + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-sentry + permissions: + - network + tags: [monitoring, sentry, errors, exceptions, performance] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-elastic-apm + category: official + displayName: Elastic APM Integration + description: Application Performance Monitoring with Elastic APM and distributed tracing + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-elastic-apm + permissions: + - network + tags: [monitoring, elastic, apm, performance, tracing] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-honeycomb + category: official + displayName: Honeycomb Observability + description: High-definition observability with Honeycomb for deep system analysis and debugging + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-honeycomb + permissions: + - network + tags: [monitoring, honeycomb, observability, tracing, debugging] + verified: true + downloads: 0 + rating: 0.0 + + # Security + - name: streamspace-compliance + category: official + displayName: Compliance & Regulatory Framework + description: Comprehensive compliance management for GDPR, HIPAA, SOC2, ISO27001, and custom frameworks + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-compliance + permissions: + - read:sessions + - read:users + - admin + tags: [compliance, gdpr, hipaa, soc2, iso27001, regulatory, governance] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-dlp + category: official + displayName: Data Loss Prevention (DLP) + description: Prevent data exfiltration with comprehensive controls for clipboard, file transfers, screen capture, printing, USB devices, and network access + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-dlp + permissions: + - read:sessions + - write:sessions + - admin + tags: [dlp, data-loss-prevention, security, clipboard, file-transfer, exfiltration] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-audit-advanced + category: official + displayName: Advanced Audit Logging + description: Enhanced audit logging with search, export, retention policies, and compliance reports + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-audit-advanced + permissions: + - read:sessions + - read:users + - admin + tags: [audit, logging, compliance, security] + verified: true + downloads: 0 + rating: 0.0 + + # Session Management + - name: streamspace-recording + category: official + displayName: Session Recording + description: Record and replay sessions with multiple formats (webm, mp4, vnc), retention policies, and compliance recording + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-recording + permissions: + - read:sessions + - write:sessions + tags: [recording, playback, compliance, audit, session] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-snapshots + category: official + displayName: Session Snapshots & Restore + description: Create, manage, and restore session snapshots with scheduling, sharing, compression, and encryption + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-snapshots + permissions: + - read:sessions + - write:sessions + tags: [snapshots, backup, restore, scheduling, session] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-multi-monitor + category: official + displayName: Multi-Monitor Support + description: Enable multi-monitor support for remote sessions + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-multi-monitor + permissions: + - read:sessions + - write:sessions + tags: [multi-monitor, display, session, ui] + verified: true + downloads: 0 + rating: 0.0 + + # Automation + - name: streamspace-workflows + category: official + displayName: Workflow Automation + description: Automate session lifecycle with event-driven workflows, triggers, actions, and conditional logic + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-workflows + permissions: + - read:sessions + - write:sessions + - admin + tags: [workflows, automation, triggers, actions, events] + verified: true + downloads: 0 + rating: 0.0 + + # Analytics + - name: streamspace-analytics-advanced + category: official + displayName: Advanced Analytics & Reporting + description: Comprehensive analytics and reporting for usage trends, session metrics, user engagement, resource utilization, and cost analysis + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-analytics-advanced + permissions: + - read:sessions + - read:users + - read:templates + tags: [analytics, reporting, metrics, insights, cost-analysis, dashboard] + verified: true + downloads: 0 + rating: 0.0 + + # Authentication + - name: streamspace-auth-saml + category: official + displayName: SAML 2.0 Authentication + description: Enterprise SSO authentication with SAML 2.0 protocol - supports Okta, OneLogin, Azure AD, Google Workspace, JumpCloud, and Auth0 + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-auth-saml + permissions: + - admin + tags: [saml, sso, authentication, enterprise, okta, onelogin, azure-ad] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-auth-oauth + category: official + displayName: OAuth2 / OIDC Authentication + description: Modern OAuth2 and OpenID Connect authentication - supports Google, GitHub, GitLab, Okta, Azure AD, Auth0, Keycloak, and custom OIDC providers + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-auth-oauth + permissions: + - admin + tags: [oauth2, oidc, sso, google, github, azure-ad, okta] + verified: true + downloads: 0 + rating: 0.0 + + # Storage + - name: streamspace-storage-s3 + category: official + displayName: S3 Object Storage + description: AWS S3 and S3-compatible object storage backend for session recordings, snapshots, and file storage - supports AWS S3, MinIO, DigitalOcean Spaces, and Wasabi + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-storage-s3 + permissions: + - network + tags: [storage, s3, aws, minio, object-storage, cloud] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-storage-azure + category: official + displayName: Azure Blob Storage + description: Microsoft Azure Blob Storage backend for session recordings, snapshots, and file storage + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-storage-azure + permissions: + - network + tags: [storage, azure, blob-storage, cloud, microsoft] + verified: true + downloads: 0 + rating: 0.0 + + - name: streamspace-storage-gcs + category: official + displayName: Google Cloud Storage + description: Google Cloud Storage backend for session recordings, snapshots, and file storage + type: integration + author: StreamSpace Team + version: "1.0.0" + path: streamspace-storage-gcs + permissions: + - network + tags: [storage, gcs, google-cloud, cloud] + verified: true + downloads: 0 + rating: 0.0 + + # Business + - name: streamspace-billing + category: official + displayName: Billing & Usage Tracking + description: Track resource usage, calculate costs, and manage subscriptions with Stripe integration + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-billing + permissions: + - network + - admin + tags: [billing, stripe, usage, subscriptions, invoicing] + verified: true + downloads: 0 + rating: 0.0 + + # Infrastructure + - name: streamspace-node-manager + category: official + displayName: Node Manager + description: Advanced Kubernetes node management with labeling, tainting, draining, and auto-scaling support + type: extension + author: StreamSpace Team + version: "1.0.0" + path: streamspace-node-manager + permissions: + - admin + tags: [kubernetes, nodes, infrastructure, auto-scaling, cluster-management] + verified: true + downloads: 0 + rating: 0.0 stats: - totalPlugins: 0 - officialPlugins: 0 + totalPlugins: 26 + officialPlugins: 26 communityPlugins: 0 - lastUpdated: "2025-11-15" + lastUpdated: "2025-11-18" compatibleVersions: - "v1.0.0" - "v1.1.0" diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..4d8ddc5 --- /dev/null +++ b/claude.md @@ -0,0 +1,373 @@ +# StreamSpace Plugins Repository + +## Project Overview + +This is the official plugin repository for [StreamSpace](https://github.com/JoshuaAFerguson/streamspace), a Kubernetes-based remote development environment platform. This repository serves as a centralized catalog for both official and community-contributed plugins that extend StreamSpace's functionality. + +## Purpose + +StreamSpace Plugins provides: + +1. **Official Plugins**: Maintained by the StreamSpace team with strict quality standards +2. **Community Plugins**: User-contributed extensions with varying support levels +3. **Plugin Catalog**: Centralized discovery and installation system +4. **Plugin Framework**: Standards and tools for plugin development + +## Repository Structure + +``` +streamspace-plugins/ +├── streamspace-slack/ # Official plugins at root level +├── streamspace-teams/ +├── streamspace-discord/ +├── streamspace-pagerduty/ +├── streamspace-email/ +├── streamspace-calendar/ +├── streamspace-datadog/ +├── streamspace-newrelic/ +├── streamspace-sentry/ +├── streamspace-elastic-apm/ +├── streamspace-honeycomb/ +├── streamspace-compliance/ +├── streamspace-dlp/ +├── streamspace-audit-advanced/ +├── streamspace-recording/ +├── streamspace-snapshots/ +├── streamspace-multi-monitor/ +├── streamspace-workflows/ +├── streamspace-analytics-advanced/ +├── streamspace-auth-saml/ +├── streamspace-auth-oauth/ +├── streamspace-storage-s3/ +├── streamspace-storage-azure/ +├── streamspace-storage-gcs/ +├── streamspace-billing/ +├── streamspace-node-manager/ +├── catalog.yaml # Plugin discovery metadata +├── claude.md # This file - AI context +├── README.md # User documentation +└── CONTRIBUTING.md # Contribution guidelines +``` + +## Plugin Types + +StreamSpace supports four main plugin types: + +### 1. Extension Plugins +Add new features and UI components to the platform. +- Custom dashboard widgets +- New session actions +- Enhanced functionality + +### 2. Webhook Plugins +React to system events in real-time. +- Session lifecycle events (created, started, stopped, deleted) +- User events (created, updated, deleted, login) +- Template events +- System events + +### 3. Integration Plugins +Connect StreamSpace to external services. +- Slack notifications +- GitHub/Jira integrations +- PagerDuty alerts +- Custom third-party services + +### 4. Theme Plugins +Customize the web interface appearance. +- Dark/light themes +- Company branding +- Accessibility themes + +## Plugin Structure + +Each plugin must follow this structure: + +``` +my-plugin/ +├── manifest.json # Plugin metadata (REQUIRED) +├── index.js # Entry point (REQUIRED) +├── README.md # Documentation (REQUIRED) +├── config.schema.json # Configuration schema (optional) +├── package.json # Node.js dependencies (if needed) +└── assets/ # Images, icons, etc. + └── icon.png +``` + +### Required Files + +#### manifest.json +Defines plugin metadata, permissions, and entry points. + +```json +{ + "name": "my-plugin", + "version": "1.0.0", + "displayName": "My Plugin", + "description": "Brief description", + "type": "extension", + "author": "Your Name", + "license": "MIT", + "permissions": ["read:sessions"], + "entrypoints": { + "main": "index.js" + } +} +``` + +#### index.js +Plugin entry point with lifecycle hooks. + +```javascript +module.exports = { + async onLoad({ api, config }) { + // Plugin initialization + }, + + async onUnload() { + // Cleanup + } +}; +``` + +## Plugin API + +Plugins have access to the StreamSpace API: + +### Available Modules +- `api.sessions` - Session management +- `api.users` - User management +- `api.templates` - Template operations +- `api.plugins` - Plugin management +- `api.webhooks` - Event subscriptions +- `api.metrics` - Metrics collection +- `api.logs` - Logging utilities + +### Example Usage + +```javascript +module.exports = { + async onLoad({ api, config }) { + // List sessions + const sessions = await api.sessions.list(); + + // Register webhook + api.webhooks.on('session.created', async (event) => { + console.log('New session:', event.data.session); + }); + + // Access config + const setting = config.get('mySetting'); + } +}; +``` + +## Permissions System + +Plugins must declare required permissions: + +- `read:sessions` - View session information +- `write:sessions` - Create/modify sessions +- `read:users` - View user information +- `write:users` - Create/modify users +- `read:templates` - View templates +- `write:templates` - Create/modify templates +- `admin` - Administrative access (dangerous) +- `network` - Make external HTTP requests (dangerous) + +## Development Workflow + +### Creating a New Plugin + +1. Fork this repository +2. Create plugin directory in `community/` or `official/` +3. Implement required files (manifest.json, index.js, README.md) +4. Test locally with StreamSpace +5. Update catalog.yaml +6. Submit pull request + +### Testing + +```bash +# Package plugin +tar -czf my-plugin.tar.gz manifest.json index.js README.md + +# Upload to StreamSpace +curl -X POST https://streamspace.local/api/plugins/upload \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@my-plugin.tar.gz" + +# Enable plugin +curl -X POST https://streamspace.local/api/plugins/my-plugin/enable \ + -H "Authorization: Bearer $TOKEN" +``` + +### Submission Guidelines + +See [CONTRIBUTING.md](CONTRIBUTING.md) for: +- Code quality standards +- Testing requirements +- Security guidelines +- Review process +- Documentation requirements + +## Security + +All plugins are: +- Sandboxed in isolated environments +- Rate-limited to prevent abuse +- Audited for security issues +- Reviewed before inclusion in official catalog + +### Security Best Practices + +1. Minimize permission requests +2. Validate all inputs +3. Sanitize external data +4. Use secure dependencies +5. Follow principle of least privilege + +## Integration with StreamSpace + +### Installation Methods + +#### 1. Catalog Sync (Recommended) + +```yaml +# In Helm values.yaml +repositories: + plugins: + enabled: true + url: https://github.com/JoshuaAFerguson/streamspace-plugins + branch: main + syncInterval: "1h" +``` + +#### 2. CLI Installation + +```bash +streamspace plugin install session-recorder +``` + +#### 3. Manual Installation + +```bash +kubectl apply -f https://raw.githubusercontent.com/JoshuaAFerguson/streamspace-plugins/main/official/session-recorder/manifest.yaml +``` + +## catalog.yaml + +The catalog.yaml file maintains: +- Plugin metadata and discovery information +- Category definitions (official/community) +- Plugin type definitions +- Permission schemas +- Event definitions +- Statistics and compatibility info + +This file is the central registry for all plugins and is consumed by StreamSpace for plugin discovery and installation. + +## Common Tasks + +### Adding a New Plugin + +1. Create directory: `streamspace-plugin-name/` +2. Add required files (manifest.json, plugin code, README.md) +3. Update catalog.yaml with plugin entry +4. Update README.md plugin list +5. Commit and push changes + +### Reviewing Community Contributions + +1. Check plugin structure and required files +2. Review code for security issues +3. Test functionality locally +4. Verify documentation quality +5. Approve or request changes + +### Updating Existing Plugin + +1. Navigate to plugin directory +2. Update version in manifest.json +3. Make code/documentation changes +4. Update catalog.yaml if metadata changed +5. Commit with clear changelog + +## Related Repositories + +- **Main Project**: [streamspace](https://github.com/JoshuaAFerguson/streamspace) +- **Templates**: [streamspace-templates](https://github.com/JoshuaAFerguson/streamspace-templates) +- **Documentation**: [streamspace/docs](https://github.com/JoshuaAFerguson/streamspace/tree/main/docs) + +## Key Concepts + +### Plugin Lifecycle + +1. **Discovery**: Listed in catalog.yaml +2. **Installation**: Downloaded and validated +3. **Configuration**: User provides settings +4. **Activation**: onLoad() called +5. **Runtime**: Hooks and API access +6. **Deactivation**: onUnload() called +7. **Uninstallation**: Cleanup + +### Event System + +Plugins can subscribe to system events: +- Session lifecycle events +- User management events +- Template operations +- Plugin state changes +- System events (startup/shutdown) +- Audit violations + +### Configuration + +Plugins can define configuration schemas: +```json +{ + "configSchema": { + "type": "object", + "properties": { + "webhookUrl": { + "type": "string", + "description": "Webhook URL for notifications" + } + }, + "required": ["webhookUrl"] + } +} +``` + +## Working with Claude Code + +When modifying this repository: + +1. **Adding plugins**: Create complete directory structure with all required files +2. **Updating catalog**: Keep catalog.yaml in sync with plugin additions/removals +3. **Documentation**: Update README.md when adding new plugins or categories +4. **Testing**: Ensure manifest.json is valid JSON and follows schema +5. **Security**: Review permissions and external dependencies + +## Questions to Consider + +When working on this repository: + +- Is the plugin structure complete? +- Are all required files present? +- Is manifest.json valid and complete? +- Are permissions minimized? +- Is documentation clear and comprehensive? +- Is catalog.yaml updated? +- Does it follow security best practices? +- Is the version number incremented appropriately? + +## Next Steps + +After initial setup, common tasks include: + +1. Migrating existing plugins from main StreamSpace repo +2. Creating official plugin implementations +3. Setting up CI/CD for plugin validation +4. Documenting plugin development workflow +5. Building plugin testing infrastructure diff --git a/streamspace-analytics-advanced/README.md b/streamspace-analytics-advanced/README.md new file mode 100644 index 0000000..8ff2437 --- /dev/null +++ b/streamspace-analytics-advanced/README.md @@ -0,0 +1,291 @@ +# StreamSpace Advanced Analytics & Reporting Plugin + +Comprehensive analytics and reporting system for usage trends, session metrics, user engagement, resource utilization, and cost analysis. + +## Features + +### Usage Analytics +- **Trends Analysis**: Time-series data for sessions, users, and teams +- **Template Usage**: Most popular templates and usage patterns +- **User Analytics**: Per-user and per-team usage breakdown +- **Historical Data**: Up to 365 days of historical trends + +### Session Analytics +- **Duration Analysis**: Session length distribution with percentiles +- **Lifecycle Metrics**: Session states and transitions +- **Peak Usage Times**: Hourly and daily peak usage patterns +- **Session Quality**: Average duration, connection stability + +### User Engagement +- **Active Users**: DAU (Daily Active Users), WAU, MAU metrics +- **Retention Analysis**: User retention and churn rates +- **Engagement Ratios**: DAU/WAU, DAU/MAU ratios +- **Power Users**: Identify highly engaged users (10+ sessions/month) + +### Resource Analytics +- **Utilization Metrics**: CPU, memory, storage usage +- **Resource Trends**: Historical resource consumption +- **Waste Detection**: Idle sessions, short sessions, underutilized resources +- **Optimization Recommendations**: Actionable insights to reduce waste + +### Cost Analytics +- **Cost Estimation**: Calculate infrastructure costs based on usage +- **Cost by Team**: Team-level cost breakdown +- **Cost by Template**: Template-level cost analysis +- **Top Spenders**: Identify highest-cost users and teams + +### Automated Reports +- **Daily Reports**: Comprehensive daily summary with key metrics +- **Weekly Reports**: Week-over-week trends and insights +- **Monthly Reports**: Month-over-month analysis +- **Email Delivery**: Scheduled report delivery to stakeholders + +## Installation + +Admin → Plugins → "Advanced Analytics & Reporting" → Install + +## Configuration + +```json +{ + "enabled": true, + "costModel": { + "cpuCostPerHour": 0.01, + "memCostPerGBHour": 0.005, + "storageCostPerGBMonth": 0.10 + }, + "retentionDays": 90, + "reportSchedule": { + "dailyEnabled": true, + "weeklyEnabled": true, + "monthlyEnabled": true, + "emailRecipients": ["admin@example.com"] + }, + "thresholds": { + "shortSessionMinutes": 5, + "idleTimeoutMinutes": 30 + } +} +``` + +## API Endpoints + +### Usage Analytics +- `GET /analytics/usage/trends?days=30` - Usage trends over time +- `GET /analytics/usage/by-template?days=30` - Usage grouped by template +- `GET /analytics/usage/by-user` - Per-user usage statistics +- `GET /analytics/usage/by-team` - Per-team usage statistics + +### Session Analytics +- `GET /analytics/sessions/duration` - Session duration distribution +- `GET /analytics/sessions/lifecycle` - Session lifecycle metrics +- `GET /analytics/sessions/peak-times` - Peak usage by hour and day + +### User Engagement +- `GET /analytics/engagement/active-users` - DAU, WAU, MAU metrics +- `GET /analytics/engagement/retention` - User retention analysis +- `GET /analytics/engagement/frequency` - Usage frequency patterns + +### Resource Analytics +- `GET /analytics/resources/utilization` - Current resource utilization +- `GET /analytics/resources/trends` - Historical resource trends +- `GET /analytics/resources/waste` - Waste detection and recommendations + +### Cost Analytics +- `GET /analytics/cost/estimate` - Overall cost estimate +- `GET /analytics/cost/by-team` - Team-level cost breakdown +- `GET /analytics/cost/by-template` - Template-level cost analysis + +### Reports +- `GET /analytics/reports/daily?date=2025-01-15` - Daily summary report +- `GET /analytics/reports/weekly` - Weekly summary report +- `GET /analytics/reports/monthly` - Monthly summary report + +## Example: Usage Trends + +**Request**: +```bash +GET /analytics/usage/trends?days=7 +``` + +**Response**: +```json +{ + "trends": [ + { + "date": "2025-01-15", + "totalSessions": 142, + "runningSessions": 38, + "uniqueUsers": 67, + "teamsActive": 12 + }, + ... + ], + "period": "7 days" +} +``` + +## Example: Cost Estimate + +**Request**: +```bash +GET /analytics/cost/estimate +``` + +**Response**: +```json +{ + "period": "30 days", + "totalCost": { + "cpu": 125.50, + "memory": 62.75, + "total": 188.25 + }, + "totalSessionHours": 12550, + "costModel": { + "cpuCostPerHour": 0.01, + "memCostPerHour": 0.005 + }, + "topUserCosts": [ + { + "userId": "user123", + "hours": 245.5, + "estimatedCost": 4.91 + } + ], + "note": "Costs are estimates based on session duration and resource allocation" +} +``` + +## Example: Resource Waste + +**Request**: +```bash +GET /analytics/resources/waste +``` + +**Response**: +```json +{ + "waste": { + "shortSessions": 23, + "longIdleSessions": 15, + "shouldBeHibernated": 8 + }, + "recommendations": [ + "Consider auto-hibernation after 30 minutes of inactivity (15 sessions affected)", + "Review short sessions to identify configuration issues (23 sessions)", + "Enable aggressive hibernation to save resources (8 sessions ready)" + ] +} +``` + +## Scheduled Jobs + +### Generate Daily Report +- **Schedule**: Daily at 1:00 AM +- **Description**: Generates comprehensive daily analytics report +- **Storage**: Saved to `analytics_reports` table +- **Email**: Sent to configured recipients (if enabled) + +### Cleanup Old Analytics +- **Schedule**: Weekly on Sunday at 2:00 AM +- **Description**: Removes analytics data older than retention period +- **Retention**: Configurable (default: 90 days) + +## Database Schema + +### analytics_cache +Caches expensive analytics queries for performance. + +```sql +CREATE TABLE analytics_cache ( + id SERIAL PRIMARY KEY, + cache_key VARCHAR(255) UNIQUE, + data JSONB, + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +### analytics_reports +Stores generated reports for historical reference. + +```sql +CREATE TABLE analytics_reports ( + id SERIAL PRIMARY KEY, + report_type VARCHAR(100), -- 'daily', 'weekly', 'monthly' + report_date DATE, + data JSONB, + generated_at TIMESTAMP DEFAULT NOW() +); +``` + +## Cost Model Configuration + +Configure your infrastructure costs to get accurate cost estimates: + +- **cpuCostPerHour**: Cost per CPU core per hour (default: $0.01) +- **memCostPerGBHour**: Cost per GB of memory per hour (default: $0.005) +- **storageCostPerGBMonth**: Cost per GB of storage per month (default: $0.10) + +Example AWS pricing: +```json +{ + "cpuCostPerHour": 0.0416, // t3.medium vCPU cost + "memCostPerGBHour": 0.0052, // t3.medium memory cost + "storageCostPerGBMonth": 0.10 // EBS gp3 storage cost +} +``` + +Example Azure pricing: +```json +{ + "cpuCostPerHour": 0.0452, // B2s vCPU cost + "memCostPerGBHour": 0.0113, // B2s memory cost + "storageCostPerGBMonth": 0.05 // Standard SSD cost +} +``` + +## Performance Optimization + +The plugin uses several techniques to ensure fast analytics: + +1. **Query Caching**: Expensive queries are cached with configurable TTL +2. **Aggregation Tables**: Pre-computed aggregates for common queries +3. **Indexed Columns**: Database indexes on frequently queried columns +4. **Batch Processing**: Reports generated asynchronously +5. **Retention Policies**: Old data automatically pruned + +## Metrics Collected + +- Total sessions created +- Active sessions +- Unique users (daily, weekly, monthly) +- Session duration (avg, median, percentiles) +- Template usage counts +- Team activity +- Resource consumption (CPU, memory, storage) +- Connection counts +- Session state transitions +- Peak usage times + +## Use Cases + +### Infrastructure Planning +Use trends and resource utilization data to forecast capacity needs and plan infrastructure scaling. + +### Cost Optimization +Identify resource waste, idle sessions, and high-cost users to optimize spending. + +### User Engagement +Track DAU/WAU/MAU metrics to measure platform adoption and user engagement. + +### Template Performance +Analyze which templates are most popular and how users interact with them. + +### Compliance Reporting +Generate historical reports for audit and compliance requirements. + +## License +MIT diff --git a/streamspace-analytics-advanced/analytics_plugin.go b/streamspace-analytics-advanced/analytics_plugin.go new file mode 100644 index 0000000..c41e7f6 --- /dev/null +++ b/streamspace-analytics-advanced/analytics_plugin.go @@ -0,0 +1,594 @@ +package main + +import ("context"; "database/sql"; "encoding/json"; "fmt"; "time"; "github.com/yourusername/streamspace/api/internal/plugins") + +type AnalyticsPlugin struct { + plugins.BasePlugin + config AnalyticsConfig +} + +type AnalyticsConfig struct { + Enabled bool `json:"enabled"` + CostModel CostModel `json:"costModel"` + RetentionDays int `json:"retentionDays"` + ReportSchedule ReportSchedule `json:"reportSchedule"` + Thresholds Thresholds `json:"thresholds"` +} + +type CostModel struct { + CPUCostPerHour float64 `json:"cpuCostPerHour"` + MemCostPerGBHour float64 `json:"memCostPerGBHour"` + StorageCostPerGBMonth float64 `json:"storageCostPerGBMonth"` +} + +type ReportSchedule struct { + DailyEnabled bool `json:"dailyEnabled"` + WeeklyEnabled bool `json:"weeklyEnabled"` + MonthlyEnabled bool `json:"monthlyEnabled"` + EmailRecipients []string `json:"emailRecipients"` +} + +type Thresholds struct { + ShortSessionMinutes int `json:"shortSessionMinutes"` + IdleTimeoutMinutes int `json:"idleTimeoutMinutes"` +} + +func (p *AnalyticsPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("Analytics plugin is disabled") + return nil + } + + p.createDatabaseTables(ctx) + ctx.Logger.Info("Analytics plugin initialized", "retention", p.config.RetentionDays) + return nil +} + +func (p *AnalyticsPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Advanced Analytics plugin loaded") + return nil +} + +func (p *AnalyticsPlugin) RunScheduledJob(ctx *plugins.PluginContext, jobName string) error { + switch jobName { + case "generate-daily-report": + return p.generateDailyReport(ctx) + case "cleanup-old-analytics": + return p.cleanupOldAnalytics(ctx) + } + return nil +} + +func (p *AnalyticsPlugin) createDatabaseTables(ctx *plugins.PluginContext) error { + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS analytics_cache ( + id SERIAL PRIMARY KEY, cache_key VARCHAR(255) UNIQUE, + data JSONB, expires_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW() + )`) + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS analytics_reports ( + id SERIAL PRIMARY KEY, report_type VARCHAR(100), report_date DATE, + data JSONB, generated_at TIMESTAMP DEFAULT NOW() + )`) + return nil +} + +// GetUsageTrends returns time-series usage data +func (p *AnalyticsPlugin) GetUsageTrends(ctx *plugins.PluginContext, days int) (map[string]interface{}, error) { + if days > 365 { + days = 365 + } + + query := fmt.Sprintf(` + SELECT + DATE(created_at) as date, + COUNT(*) as total_sessions, + COUNT(*) FILTER (WHERE state = 'running') as running_sessions, + COUNT(DISTINCT user_id) as unique_users, + COUNT(DISTINCT team_id) FILTER (WHERE team_id IS NOT NULL) as teams_active + FROM sessions + WHERE created_at >= NOW() - INTERVAL '%d days' + GROUP BY DATE(created_at) + ORDER BY date DESC + `, days) + + rows, err := ctx.Database.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + trends := []map[string]interface{}{} + for rows.Next() { + var date time.Time + var totalSessions, runningSessions, uniqueUsers, teamsActive int + + if err := rows.Scan(&date, &totalSessions, &runningSessions, &uniqueUsers, &teamsActive); err != nil { + continue + } + + trends = append(trends, map[string]interface{}{ + "date": date.Format("2006-01-02"), + "totalSessions": totalSessions, + "runningSessions": runningSessions, + "uniqueUsers": uniqueUsers, + "teamsActive": teamsActive, + }) + } + + return map[string]interface{}{ + "trends": trends, + "period": fmt.Sprintf("%d days", days), + }, nil +} + +// GetUsageByTemplate returns session counts per template +func (p *AnalyticsPlugin) GetUsageByTemplate(ctx *plugins.PluginContext, days int) (map[string]interface{}, error) { + query := fmt.Sprintf(` + SELECT + template_name, + COUNT(*) as session_count, + COUNT(DISTINCT user_id) as unique_users, + AVG(EXTRACT(EPOCH FROM (COALESCE(last_disconnect, NOW()) - created_at))) as avg_duration_seconds + FROM sessions + WHERE created_at >= NOW() - INTERVAL '%d days' + GROUP BY template_name + ORDER BY session_count DESC + LIMIT 50 + `, days) + + rows, err := ctx.Database.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + templates := []map[string]interface{}{} + for rows.Next() { + var templateName string + var sessionCount, uniqueUsers int + var avgDuration sql.NullFloat64 + + if err := rows.Scan(&templateName, &sessionCount, &uniqueUsers, &avgDuration); err != nil { + continue + } + + templates = append(templates, map[string]interface{}{ + "templateName": templateName, + "sessionCount": sessionCount, + "uniqueUsers": uniqueUsers, + "avgDurationSeconds": avgDuration.Float64, + "avgDurationMinutes": avgDuration.Float64 / 60, + }) + } + + return map[string]interface{}{ + "templates": templates, + "total": len(templates), + }, nil +} + +// GetSessionDurationAnalytics returns session duration statistics +func (p *AnalyticsPlugin) GetSessionDurationAnalytics(ctx *plugins.PluginContext) (map[string]interface{}, error) { + query := ` + WITH session_durations AS ( + SELECT + EXTRACT(EPOCH FROM (COALESCE(last_disconnect, NOW()) - created_at)) / 60 as duration_minutes + FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + ) + SELECT + CASE + WHEN duration_minutes < 5 THEN '0-5 min' + WHEN duration_minutes < 15 THEN '5-15 min' + WHEN duration_minutes < 30 THEN '15-30 min' + WHEN duration_minutes < 60 THEN '30-60 min' + WHEN duration_minutes < 120 THEN '1-2 hours' + WHEN duration_minutes < 240 THEN '2-4 hours' + WHEN duration_minutes < 480 THEN '4-8 hours' + ELSE '8+ hours' + END as duration_bucket, + COUNT(*) as session_count + FROM session_durations + GROUP BY duration_bucket + ORDER BY + CASE duration_bucket + WHEN '0-5 min' THEN 1 + WHEN '5-15 min' THEN 2 + WHEN '15-30 min' THEN 3 + WHEN '30-60 min' THEN 4 + WHEN '1-2 hours' THEN 5 + WHEN '2-4 hours' THEN 6 + WHEN '4-8 hours' THEN 7 + WHEN '8+ hours' THEN 8 + END + ` + + rows, err := ctx.Database.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + buckets := []map[string]interface{}{} + totalSessions := 0 + for rows.Next() { + var bucket string + var count int + + if err := rows.Scan(&bucket, &count); err != nil { + continue + } + + buckets = append(buckets, map[string]interface{}{ + "bucket": bucket, + "count": count, + }) + totalSessions += count + } + + // Calculate percentages + for _, bucket := range buckets { + count := bucket["count"].(int) + bucket["percentage"] = float64(count) / float64(totalSessions) * 100 + } + + // Get average, median, and percentiles + var avgDuration, medianDuration, p90Duration, p95Duration sql.NullFloat64 + ctx.Database.QueryRow(` + WITH session_durations AS ( + SELECT + EXTRACT(EPOCH FROM (COALESCE(last_disconnect, NOW()) - created_at)) / 60 as duration_minutes + FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + ) + SELECT + AVG(duration_minutes) as avg, + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY duration_minutes) as median, + PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY duration_minutes) as p90, + PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_minutes) as p95 + FROM session_durations + `).Scan(&avgDuration, &medianDuration, &p90Duration, &p95Duration) + + return map[string]interface{}{ + "buckets": buckets, + "statistics": map[string]interface{}{ + "avgMinutes": avgDuration.Float64, + "medianMinutes": medianDuration.Float64, + "p90Minutes": p90Duration.Float64, + "p95Minutes": p95Duration.Float64, + }, + "totalSessions": totalSessions, + }, nil +} + +// GetActiveUsersAnalytics returns active user statistics +func (p *AnalyticsPlugin) GetActiveUsersAnalytics(ctx *plugins.PluginContext) (map[string]interface{}, error) { + var dau, wau, mau int + + ctx.Database.QueryRow(` + SELECT COUNT(DISTINCT user_id) FROM sessions + WHERE created_at >= NOW() - INTERVAL '1 day' + `).Scan(&dau) + + ctx.Database.QueryRow(` + SELECT COUNT(DISTINCT user_id) FROM sessions + WHERE created_at >= NOW() - INTERVAL '7 days' + `).Scan(&wau) + + ctx.Database.QueryRow(` + SELECT COUNT(DISTINCT user_id) FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + `).Scan(&mau) + + var dauWauRatio, dauMauRatio float64 + if wau > 0 { + dauWauRatio = float64(dau) / float64(wau) + } + if mau > 0 { + dauMauRatio = float64(dau) / float64(mau) + } + + var powerUsers int + ctx.Database.QueryRow(` + SELECT COUNT(*) + FROM ( + SELECT user_id, COUNT(*) as session_count + FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + GROUP BY user_id + HAVING COUNT(*) >= 10 + ) power_users + `).Scan(&powerUsers) + + return map[string]interface{}{ + "activeUsers": map[string]interface{}{ + "daily": dau, + "weekly": wau, + "monthly": mau, + }, + "engagement": map[string]interface{}{ + "dauWauRatio": dauWauRatio, + "dauMauRatio": dauMauRatio, + "powerUsers": powerUsers, + }, + "timestamp": time.Now(), + }, nil +} + +// GetPeakUsageTimes returns peak usage analysis +func (p *AnalyticsPlugin) GetPeakUsageTimes(ctx *plugins.PluginContext) (map[string]interface{}, error) { + hourlyQuery := ` + SELECT + EXTRACT(HOUR FROM created_at) as hour, + COUNT(*) as session_count + FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + GROUP BY EXTRACT(HOUR FROM created_at) + ORDER BY hour + ` + + rows, err := ctx.Database.Query(hourlyQuery) + if err != nil { + return nil, err + } + defer rows.Close() + + hourlyData := []map[string]interface{}{} + for rows.Next() { + var hour int + var count int + if err := rows.Scan(&hour, &count); err == nil { + hourlyData = append(hourlyData, map[string]interface{}{ + "hour": hour, + "count": count, + }) + } + } + + weekdayQuery := ` + SELECT + EXTRACT(DOW FROM created_at) as day_of_week, + COUNT(*) as session_count + FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + GROUP BY EXTRACT(DOW FROM created_at) + ORDER BY day_of_week + ` + + rows2, err := ctx.Database.Query(weekdayQuery) + if err != nil { + return nil, err + } + defer rows2.Close() + + weekdayData := []map[string]interface{}{} + dayNames := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} + for rows2.Next() { + var dow int + var count int + if err := rows2.Scan(&dow, &count); err == nil { + weekdayData = append(weekdayData, map[string]interface{}{ + "dayOfWeek": dow, + "dayName": dayNames[dow], + "count": count, + }) + } + } + + return map[string]interface{}{ + "hourly": hourlyData, + "weekday": weekdayData, + }, nil +} + +// GetCostEstimate returns estimated cost based on resource usage +func (p *AnalyticsPlugin) GetCostEstimate(ctx *plugins.PluginContext) (map[string]interface{}, error) { + cpuCostPerHour := p.config.CostModel.CPUCostPerHour + memCostPerHour := p.config.CostModel.MemCostPerGBHour + + var totalSessionHours float64 + ctx.Database.QueryRow(` + SELECT + COALESCE(SUM(EXTRACT(EPOCH FROM (COALESCE(last_disconnect, NOW()) - created_at)) / 3600), 0) + FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + `).Scan(&totalSessionHours) + + estimatedCPUCost := totalSessionHours * cpuCostPerHour + estimatedMemCost := totalSessionHours * 2 * memCostPerHour + totalEstimatedCost := estimatedCPUCost + estimatedMemCost + + userCosts := []map[string]interface{}{} + userQuery := ` + SELECT + user_id, + SUM(EXTRACT(EPOCH FROM (COALESCE(last_disconnect, NOW()) - created_at)) / 3600) as total_hours + FROM sessions + WHERE created_at >= NOW() - INTERVAL '30 days' + GROUP BY user_id + ORDER BY total_hours DESC + LIMIT 10 + ` + + rows, err := ctx.Database.Query(userQuery) + if err == nil { + defer rows.Close() + for rows.Next() { + var userID string + var hours float64 + if err := rows.Scan(&userID, &hours); err == nil { + cost := hours * (cpuCostPerHour + 2*memCostPerHour) + userCosts = append(userCosts, map[string]interface{}{ + "userId": userID, + "hours": hours, + "estimatedCost": cost, + }) + } + } + } + + return map[string]interface{}{ + "period": "30 days", + "totalCost": map[string]interface{}{ + "cpu": estimatedCPUCost, + "memory": estimatedMemCost, + "total": totalEstimatedCost, + }, + "totalSessionHours": totalSessionHours, + "costModel": map[string]interface{}{ + "cpuCostPerHour": cpuCostPerHour, + "memCostPerHour": memCostPerHour, + }, + "topUserCosts": userCosts, + "note": "Costs are estimates based on session duration and resource allocation", + }, nil +} + +// GetResourceWaste identifies idle or underutilized resources +func (p *AnalyticsPlugin) GetResourceWaste(ctx *plugins.PluginContext) (map[string]interface{}, error) { + shortSessionThreshold := p.config.Thresholds.ShortSessionMinutes * 60 + idleTimeout := p.config.Thresholds.IdleTimeoutMinutes + + var shortSessions int + ctx.Database.QueryRow(fmt.Sprintf(` + SELECT COUNT(*) + FROM sessions + WHERE created_at >= NOW() - INTERVAL '7 days' + AND EXTRACT(EPOCH FROM (COALESCE(last_disconnect, NOW()) - created_at)) < %d + `, shortSessionThreshold)).Scan(&shortSessions) + + var longIdleSessions int + ctx.Database.QueryRow(fmt.Sprintf(` + SELECT COUNT(*) + FROM sessions + WHERE state = 'running' + AND last_connection IS NOT NULL + AND NOW() - last_connection > INTERVAL '%d minutes' + `, idleTimeout)).Scan(&longIdleSessions) + + var shouldBeHibernated int + ctx.Database.QueryRow(` + SELECT COUNT(*) + FROM sessions + WHERE state = 'running' + AND active_connections = 0 + AND created_at < NOW() - INTERVAL '1 hour' + `).Scan(&shouldBeHibernated) + + return map[string]interface{}{ + "waste": map[string]interface{}{ + "shortSessions": shortSessions, + "longIdleSessions": longIdleSessions, + "shouldBeHibernated": shouldBeHibernated, + }, + "recommendations": []string{ + fmt.Sprintf("Consider auto-hibernation after %d minutes of inactivity (%d sessions affected)", idleTimeout, longIdleSessions), + fmt.Sprintf("Review short sessions to identify configuration issues (%d sessions)", shortSessions), + fmt.Sprintf("Enable aggressive hibernation to save resources (%d sessions ready)", shouldBeHibernated), + }, + }, nil +} + +// GetDailyReport returns a comprehensive daily summary +func (p *AnalyticsPlugin) GetDailyReport(ctx *plugins.PluginContext, date string) (map[string]interface{}, error) { + if date == "" { + date = time.Now().Format("2006-01-02") + } + + var totalSessions, uniqueUsers, totalConnections int + var avgDuration sql.NullFloat64 + + ctx.Database.QueryRow(` + SELECT + COUNT(*), + COUNT(DISTINCT user_id), + AVG(EXTRACT(EPOCH FROM (COALESCE(last_disconnect, NOW()) - created_at)) / 60) + FROM sessions + WHERE DATE(created_at) = $1 + `, date).Scan(&totalSessions, &uniqueUsers, &avgDuration) + + ctx.Database.QueryRow(` + SELECT COUNT(*) + FROM connections + WHERE DATE(connected_at) = $1 + `, date).Scan(&totalConnections) + + topTemplates := []map[string]interface{}{} + rows, err := ctx.Database.Query(` + SELECT template_name, COUNT(*) as count + FROM sessions + WHERE DATE(created_at) = $1 + GROUP BY template_name + ORDER BY count DESC + LIMIT 5 + `, date) + if err == nil { + defer rows.Close() + for rows.Next() { + var name string + var count int + if err := rows.Scan(&name, &count); err == nil { + topTemplates = append(topTemplates, map[string]interface{}{ + "template": name, + "count": count, + }) + } + } + } + + return map[string]interface{}{ + "date": date, + "summary": map[string]interface{}{ + "totalSessions": totalSessions, + "uniqueUsers": uniqueUsers, + "totalConnections": totalConnections, + "avgDurationMinutes": avgDuration.Float64, + }, + "topTemplates": topTemplates, + }, nil +} + +func (p *AnalyticsPlugin) generateDailyReport(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Generating daily analytics report") + + if !p.config.ReportSchedule.DailyEnabled { + ctx.Logger.Info("Daily reports disabled") + return nil + } + + date := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + report, err := p.GetDailyReport(ctx, date) + if err != nil { + return err + } + + reportJSON, _ := json.Marshal(report) + _, err = ctx.Database.Exec(` + INSERT INTO analytics_reports (report_type, report_date, data) + VALUES ($1, $2, $3) + `, "daily", date, reportJSON) + + return err +} + +func (p *AnalyticsPlugin) cleanupOldAnalytics(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Cleaning up old analytics data", "retention", p.config.RetentionDays) + + ctx.Database.Exec(` + DELETE FROM analytics_cache + WHERE expires_at < NOW() + `) + + ctx.Database.Exec(fmt.Sprintf(` + DELETE FROM analytics_reports + WHERE generated_at < NOW() - INTERVAL '%d days' + `, p.config.RetentionDays)) + + return nil +} + +func init() { + plugins.Register("streamspace-analytics-advanced", &AnalyticsPlugin{}) +} diff --git a/streamspace-analytics-advanced/manifest.json b/streamspace-analytics-advanced/manifest.json new file mode 100644 index 0000000..b0ec9ab --- /dev/null +++ b/streamspace-analytics-advanced/manifest.json @@ -0,0 +1,151 @@ +{ + "name": "streamspace-analytics-advanced", + "version": "1.0.0", + "displayName": "Advanced Analytics & Reporting", + "description": "Comprehensive analytics and reporting for usage trends, session metrics, user engagement, resource utilization, and cost analysis", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Analytics", + "tags": ["analytics", "reporting", "metrics", "insights", "cost-analysis"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "analytics_plugin.go" + }, + + "permissions": ["database", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Advanced Analytics", + "default": true + }, + "costModel": { + "type": "object", + "title": "Cost Model Configuration", + "properties": { + "cpuCostPerHour": { + "type": "number", + "title": "CPU Cost Per Hour (USD)", + "default": 0.01, + "description": "Cost per CPU core per hour" + }, + "memCostPerGBHour": { + "type": "number", + "title": "Memory Cost Per GB Hour (USD)", + "default": 0.005, + "description": "Cost per GB of memory per hour" + }, + "storageCostPerGBMonth": { + "type": "number", + "title": "Storage Cost Per GB Month (USD)", + "default": 0.10, + "description": "Cost per GB of storage per month" + } + } + }, + "retentionDays": { + "type": "integer", + "title": "Analytics Data Retention (Days)", + "default": 90, + "description": "How long to retain detailed analytics data" + }, + "reportSchedule": { + "type": "object", + "title": "Scheduled Reports", + "properties": { + "dailyEnabled": { + "type": "boolean", + "default": true + }, + "weeklyEnabled": { + "type": "boolean", + "default": true + }, + "monthlyEnabled": { + "type": "boolean", + "default": true + }, + "emailRecipients": { + "type": "array", + "items": {"type": "string", "format": "email"}, + "default": [] + } + } + }, + "thresholds": { + "type": "object", + "title": "Alert Thresholds", + "properties": { + "shortSessionMinutes": { + "type": "integer", + "default": 5, + "description": "Sessions shorter than this are considered potential waste" + }, + "idleTimeoutMinutes": { + "type": "integer", + "default": 30, + "description": "Idle time before recommending hibernation" + } + } + } + } + }, + "database": { + "tables": ["analytics_cache", "analytics_reports"] + }, + "api": { + "endpoints": [ + "/analytics/usage/trends", + "/analytics/usage/by-template", + "/analytics/usage/by-user", + "/analytics/usage/by-team", + "/analytics/sessions/duration", + "/analytics/sessions/lifecycle", + "/analytics/sessions/peak-times", + "/analytics/engagement/active-users", + "/analytics/engagement/retention", + "/analytics/engagement/frequency", + "/analytics/resources/utilization", + "/analytics/resources/trends", + "/analytics/resources/waste", + "/analytics/cost/estimate", + "/analytics/cost/by-team", + "/analytics/cost/by-template", + "/analytics/reports/daily", + "/analytics/reports/weekly", + "/analytics/reports/monthly" + ] + }, + "ui": { + "adminPages": [ + { + "id": "analytics", + "title": "Analytics & Insights", + "route": "/admin/analytics", + "component": "Analytics", + "icon": "insights" + } + ] + }, + "scheduler": { + "jobs": [ + { + "name": "generate-daily-report", + "schedule": "0 1 * * *", + "description": "Generate daily analytics report at 1 AM" + }, + { + "name": "cleanup-old-analytics", + "schedule": "0 2 * * 0", + "description": "Clean up old analytics data weekly at 2 AM on Sundays" + } + ] + } +} diff --git a/streamspace-audit-advanced/README.md b/streamspace-audit-advanced/README.md new file mode 100644 index 0000000..ed76590 --- /dev/null +++ b/streamspace-audit-advanced/README.md @@ -0,0 +1,21 @@ +# Advanced Audit Logging Plugin + +Enhanced audit logging with search, export, retention, and compliance reports. + +## Features +- Comprehensive audit trail +- Advanced search and filtering +- Export to CSV/JSON +- Retention policies +- Compliance reporting + +## Installation +Admin → Plugins → "Advanced Audit Logging" → Install + +## Configuration +```json +{"enabled": true, "retentionDays": 2555, "logLevel": "detailed"} +``` + +## License +MIT diff --git a/streamspace-audit-advanced/audit_plugin.go b/streamspace-audit-advanced/audit_plugin.go new file mode 100644 index 0000000..8da6f9f --- /dev/null +++ b/streamspace-audit-advanced/audit_plugin.go @@ -0,0 +1,18 @@ +package main + +import ("encoding/json"; "github.com/yourusername/streamspace/api/internal/plugins"; "time") + +type AuditPlugin struct {plugins.BasePlugin; config AuditConfig} +type AuditConfig struct {Enabled bool `json:"enabled"`; RetentionDays int `json:"retentionDays"`} + +func (p *AuditPlugin) Initialize(ctx *plugins.PluginContext) error { + json.Unmarshal([]byte("{}"), &p.config) + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS audit_log_advanced (id SERIAL PRIMARY KEY, user_id VARCHAR(255), event_type VARCHAR(100), details JSONB, created_at TIMESTAMP DEFAULT NOW())`) + ctx.Logger.Info("Audit plugin initialized") + return nil +} + +func (p *AuditPlugin) OnLoad(ctx *plugins.PluginContext) error {return nil} +func (p *AuditPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error {return nil} + +func init() {plugins.Register("streamspace-audit-advanced", &AuditPlugin{})} diff --git a/streamspace-audit-advanced/manifest.json b/streamspace-audit-advanced/manifest.json new file mode 100644 index 0000000..da7fd32 --- /dev/null +++ b/streamspace-audit-advanced/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "streamspace-audit-advanced", + "version": "1.0.0", + "displayName": "Advanced Audit Logging", + "description": "Enhanced audit logging with search, export, retention policies, and compliance reports", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Security", + "tags": ["audit", "logging", "compliance", "security"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "audit_plugin.go" + }, + + "permissions": ["database", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "retentionDays": {"type": "integer", "default": 2555}, + "logLevel": {"type": "string", "enum": ["basic", "detailed", "verbose"], "default": "detailed"}, + "exportEnabled": {"type": "boolean", "default": true}, + "encryptLogs": {"type": "boolean", "default": true} + } + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "user.login": "OnUserLogin", + "user.logout": "OnUserLogout" + }, + "database": {"tables": ["audit_log_advanced", "audit_exports"]}, + "api": {"endpoints": ["/audit/logs", "/audit/export", "/audit/search"]}, + "ui": {"adminPages": [{"id": "audit-logs", "title": "Audit Logs", "route": "/admin/audit", "component": "AuditLogs", "icon": "history"}]} +} diff --git a/streamspace-auth-oauth/README.md b/streamspace-auth-oauth/README.md new file mode 100644 index 0000000..eb14c96 --- /dev/null +++ b/streamspace-auth-oauth/README.md @@ -0,0 +1,86 @@ +# StreamSpace OAuth2 / OIDC Authentication Plugin + +Modern authentication using OAuth2 and OpenID Connect protocols. Supports Google, GitHub, GitLab, Okta, Azure AD, Auth0, Keycloak, and any custom OIDC provider. + +## Features + +- **OAuth2 / OIDC Standards**: Full OAuth 2.0 and OpenID Connect 1.0 support +- **Major Providers**: Pre-configured for Google, GitHub, GitLab, Okta, Azure AD, Auth0, Keycloak +- **Automatic Discovery**: OIDC discovery for automatic endpoint configuration +- **Flexible Claims**: Map any OIDC claim to user fields +- **Auto-Provisioning**: Automatically create user accounts on first login +- **Multi-Provider**: Support multiple OAuth providers simultaneously + +## Installation + +Admin → Plugins → "OAuth2 / OIDC Authentication" → Install + +## Configuration + +### Google + +```json +{ + "enabled": true, + "provider": "google", + "providerURL": "https://accounts.google.com", + "clientID": "your-client-id.apps.googleusercontent.com", + "clientSecret": "your-client-secret", + "redirectURI": "https://streamspace.example.com/oauth/callback", + "scopes": ["openid", "profile", "email"], + "autoProvisionUsers": true, + "defaultRole": "user" +} +``` + +### GitHub + +```json +{ + "enabled": true, + "provider": "github", + "providerURL": "https://token.actions.githubusercontent.com", + "clientID": "your-github-client-id", + "clientSecret": "your-github-client-secret", + "redirectURI": "https://streamspace.example.com/oauth/callback", + "scopes": ["read:user", "user:email"] +} +``` + +### Azure AD + +```json +{ + "enabled": true, + "provider": "azure-ad", + "providerURL": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0", + "clientID": "your-application-id", + "clientSecret": "your-client-secret", + "redirectURI": "https://streamspace.example.com/oauth/callback", + "scopes": ["openid", "profile", "email"] +} +``` + +### Okta + +```json +{ + "enabled": true, + "provider": "okta", + "providerURL": "https://your-domain.okta.com/oauth2/default", + "clientID": "your-okta-client-id", + "clientSecret": "your-okta-client-secret", + "redirectURI": "https://streamspace.example.com/oauth/callback", + "scopes": ["openid", "profile", "email", "groups"] +} +``` + +## API Endpoints + +- `GET /oauth/login?provider=google` - Initiate OAuth login flow +- `GET /oauth/callback` - OAuth callback endpoint (set as redirect URI in provider) +- `GET /oauth/logout` - Logout and clear session + +## License + +MIT diff --git a/streamspace-auth-oauth/manifest.json b/streamspace-auth-oauth/manifest.json new file mode 100644 index 0000000..7b897c5 --- /dev/null +++ b/streamspace-auth-oauth/manifest.json @@ -0,0 +1,58 @@ +{ + "name": "streamspace-auth-oauth", + "version": "1.0.0", + "displayName": "OAuth2 / OIDC Authentication", + "description": "Modern OAuth2 and OpenID Connect authentication - supports Google, GitHub, GitLab, Okta, Azure AD, Auth0, Keycloak, and custom OIDC providers", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Authentication", + "tags": ["oauth2", "oidc", "sso", "google", "github", "azure-ad", "okta"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "oauth_plugin.go" + }, + + "permissions": ["network", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": false}, + "provider": { + "type": "string", + "enum": ["google", "github", "gitlab", "okta", "azure-ad", "auth0", "keycloak", "custom"], + "default": "custom" + }, + "providerURL": {"type": "string", "title": "OIDC Provider URL"}, + "clientID": {"type": "string", "title": "OAuth2 Client ID"}, + "clientSecret": {"type": "string", "title": "OAuth2 Client Secret", "format": "password"}, + "redirectURI": {"type": "string", "title": "Redirect URI"}, + "scopes": { + "type": "array", + "items": {"type": "string"}, + "default": ["openid", "profile", "email"] + }, + "usernameClaim": {"type": "string", "default": "preferred_username"}, + "emailClaim": {"type": "string", "default": "email"}, + "groupsClaim": {"type": "string", "default": "groups"}, + "autoProvisionUsers": {"type": "boolean", "default": true}, + "defaultRole": {"type": "string", "enum": ["user", "operator", "admin"], "default": "user"} + }, + "required": ["providerURL", "clientID", "clientSecret", "redirectURI"] + }, + "api": { + "endpoints": ["/oauth/login", "/oauth/callback", "/oauth/logout"] + }, + "ui": { + "adminPages": [{"id": "oauth-auth", "title": "OAuth Configuration", "route": "/admin/auth/oauth", "component": "OAuthAuth", "icon": "vpn_key"}], + "loginButtons": [ + {"provider": "google", "label": "Sign in with Google", "icon": "google"}, + {"provider": "github", "label": "Sign in with GitHub", "icon": "github"}, + {"provider": "azure-ad", "label": "Sign in with Microsoft", "icon": "microsoft"} + ] + } +} diff --git a/streamspace-auth-oauth/oauth_plugin.go b/streamspace-auth-oauth/oauth_plugin.go new file mode 100644 index 0000000..9e51a98 --- /dev/null +++ b/streamspace-auth-oauth/oauth_plugin.go @@ -0,0 +1,171 @@ +package main + +import ("context"; "encoding/json"; "fmt"; "github.com/yourusername/streamspace/api/internal/plugins"; "github.com/coreos/go-oidc/v3/oidc"; "golang.org/x/oauth2") + +type OAuthPlugin struct { + plugins.BasePlugin + config OAuthConfig + provider *oidc.Provider + oauth2Config *oauth2.Config + verifier *oidc.IDTokenVerifier +} + +type OAuthConfig struct { + Enabled bool `json:"enabled"` + Provider string `json:"provider"` + ProviderURL string `json:"providerURL"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + Scopes []string `json:"scopes"` + UsernameClaim string `json:"usernameClaim"` + EmailClaim string `json:"emailClaim"` + GroupsClaim string `json:"groupsClaim"` + AutoProvisionUsers bool `json:"autoProvisionUsers"` + DefaultRole string `json:"defaultRole"` +} + +func (p *OAuthPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("OAuth authentication is disabled") + return nil + } + + // Set defaults + if len(p.config.Scopes) == 0 { + p.config.Scopes = []string{oidc.ScopeOpenID, "profile", "email"} + } + if p.config.UsernameClaim == "" { + p.config.UsernameClaim = "preferred_username" + } + if p.config.EmailClaim == "" { + p.config.EmailClaim = "email" + } + + // Discover OIDC provider + provider, err := oidc.NewProvider(context.Background(), p.config.ProviderURL) + if err != nil { + return fmt.Errorf("failed to discover OIDC provider: %w", err) + } + + // Create OAuth2 config + oauth2Config := &oauth2.Config{ + ClientID: p.config.ClientID, + ClientSecret: p.config.ClientSecret, + RedirectURL: p.config.RedirectURI, + Endpoint: provider.Endpoint(), + Scopes: p.config.Scopes, + } + + // Create ID token verifier + verifier := provider.Verifier(&oidc.Config{ + ClientID: p.config.ClientID, + }) + + p.provider = provider + p.oauth2Config = oauth2Config + p.verifier = verifier + + ctx.Logger.Info("OAuth authentication initialized", "provider", p.config.Provider, "providerURL", p.config.ProviderURL) + return nil +} + +func (p *OAuthPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("OAuth Authentication plugin loaded") + return nil +} + +func (p *OAuthPlugin) OnUserLogin(ctx *plugins.PluginContext, user interface{}) error { + userMap, _ := user.(map[string]interface{}) + authMethod := userMap["auth_method"] + if authMethod == "oauth" || authMethod == "oidc" { + ctx.Logger.Info("OAuth user login", "user", userMap["username"], "provider", p.config.Provider) + } + return nil +} + +// GetAuthorizationURL generates the OAuth authorization URL +func (p *OAuthPlugin) GetAuthorizationURL(state string) string { + return p.oauth2Config.AuthCodeURL(state) +} + +// HandleCallback processes the OAuth callback +func (p *OAuthPlugin) HandleCallback(ctx context.Context, code string) (map[string]interface{}, error) { + // Exchange authorization code for tokens + oauth2Token, err := p.oauth2Config.Exchange(ctx, code) + if err != nil { + return nil, fmt.Errorf("failed to exchange authorization code: %w", err) + } + + // Extract ID token + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + return nil, fmt.Errorf("no id_token field in oauth2 token") + } + + // Verify ID token + idToken, err := p.verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("failed to verify ID token: %w", err) + } + + // Extract claims + var claims map[string]interface{} + if err := idToken.Claims(&claims); err != nil { + return nil, fmt.Errorf("failed to parse ID token claims: %w", err) + } + + // Build user info + user := map[string]interface{}{ + "auth_method": "oauth", + "provider": p.config.Provider, + "subject": idToken.Subject, + "email": extractClaim(claims, p.config.EmailClaim), + "username": extractClaim(claims, p.config.UsernameClaim), + "groups": extractArrayClaim(claims, p.config.GroupsClaim), + "claims": claims, + } + + // Use email as username if username is empty + if user["username"] == "" { + user["username"] = user["email"] + } + + // Set default role if auto-provisioning + if p.config.AutoProvisionUsers { + user["role"] = p.config.DefaultRole + } + + return user, nil +} + +func extractClaim(claims map[string]interface{}, key string) string { + if val, ok := claims[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func extractArrayClaim(claims map[string]interface{}, key string) []string { + if val, ok := claims[key]; ok { + if arr, ok := val.([]interface{}); ok { + result := make([]string, len(arr)) + for i, v := range arr { + if str, ok := v.(string); ok { + result[i] = str + } + } + return result + } + } + return []string{} +} + +func init() { + plugins.Register("streamspace-auth-oauth", &OAuthPlugin{}) +} diff --git a/streamspace-auth-saml/README.md b/streamspace-auth-saml/README.md new file mode 100644 index 0000000..018c5f7 --- /dev/null +++ b/streamspace-auth-saml/README.md @@ -0,0 +1,256 @@ +# StreamSpace SAML 2.0 Authentication Plugin + +Enterprise single sign-on (SSO) authentication using the SAML 2.0 protocol. Supports major identity providers including Okta, OneLogin, Azure AD, Google Workspace, JumpCloud, and Auth0. + +## Features + +- **Standards Compliance**: Full SAML 2.0 protocol support +- **Major IdP Support**: Pre-configured for Okta, OneLogin, Azure AD, Google, JumpCloud, Auth0 +- **Service Provider Metadata**: Auto-generated SP metadata for easy IdP configuration +- **Assertion Consumer Service (ACS)**: Handles SAML assertions from IdP +- **Single Logout (SLO)**: Support for single logout across applications +- **IdP-Initiated Login**: Optional support for IdP-initiated SSO flows +- **Request Signing**: Sign SAML requests for enhanced security +- **Attribute Mapping**: Flexible mapping of SAML attributes to user fields +- **Auto-Provisioning**: Automatically create user accounts on first login +- **Force Re-authentication**: Optional force re-auth even with active IdP session + +## Installation + +Admin → Plugins → "SAML 2.0 Authentication" → Install + +## Configuration + +### Basic Configuration + +```json +{ + "enabled": true, + "provider": "okta", + "entityID": "https://streamspace.example.com", + "metadataURL": "https://your-idp.okta.com/app/metadata.xml", + "allowIDPInitiated": true, + "signRequest": true, + "forceAuthn": false +} +``` + +### Certificate and Private Key + +Generate a self-signed certificate for your Service Provider: + +```bash +openssl req -x509 -newkey rsa:2048 -keyout sp-key.pem -out sp-cert.pem -days 365 -nodes +``` + +Then configure in the plugin: + +```json +{ + "certificate": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----", + "privateKey": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----" +} +``` + +### Attribute Mapping + +Map SAML attributes from your IdP to StreamSpace user fields: + +```json +{ + "attributeMapping": { + "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + "username": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", + "firstName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", + "lastName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", + "groups": "http://schemas.xmlsoap.org/claims/Group" + } +} +``` + +### Auto-Provisioning + +```json +{ + "autoProvisionUsers": true, + "defaultRole": "user" +} +``` + +## Setup Guides + +### Okta + +1. **Create SAML App** in Okta Admin Console + - Applications → Create App Integration → SAML 2.0 + +2. **Configure General Settings**: + - App name: StreamSpace + - App logo: (optional) + +3. **Configure SAML Settings**: + - Single sign-on URL: `https://streamspace.example.com/saml/acs` + - Audience URI (SP Entity ID): `https://streamspace.example.com` + - Name ID format: EmailAddress + - Application username: Email + +4. **Attribute Statements**: + - email: `user.email` + - username: `user.login` + - firstName: `user.firstName` + - lastName: `user.lastName` + +5. **Download Metadata**: + - Identity Provider metadata → Download XML + - Paste XML into plugin's `metadataXML` field + +### Azure AD + +1. **Create Enterprise Application**: + - Azure Portal → Azure Active Directory → Enterprise Applications + - New application → Create your own application + - Name: StreamSpace, choose "Integrate any other application" + +2. **Configure Single Sign-On**: + - Single sign-on → SAML + - Basic SAML Configuration: + - Identifier (Entity ID): `https://streamspace.example.com` + - Reply URL (ACS): `https://streamspace.example.com/saml/acs` + - Sign on URL: `https://streamspace.example.com/saml/login` + +3. **User Attributes & Claims**: + - Unique User Identifier: `user.mail` + - Additional claims: + - email: `user.mail` + - firstName: `user.givenname` + - lastName: `user.surname` + +4. **Download Metadata**: + - SAML Signing Certificate → Federation Metadata XML + - Paste into plugin's `metadataXML` field + +### Google Workspace + +1. **Add Custom SAML App**: + - Admin Console → Apps → Web and mobile apps → Add app → Add custom SAML app + +2. **App Details**: + - App name: StreamSpace + - App icon: (optional) + - Continue + +3. **Google Identity Provider Details**: + - Download Metadata + - Paste into plugin's `metadataXML` field + +4. **Service Provider Details**: + - ACS URL: `https://streamspace.example.com/saml/acs` + - Entity ID: `https://streamspace.example.com` + - Start URL: `https://streamspace.example.com/saml/login` + - Name ID format: EMAIL + - Name ID: Basic Information > Primary email + +5. **Attribute Mapping**: + - email: Basic Information > Primary email + - firstName: Basic Information > First name + - lastName: Basic Information > Last name + +### OneLogin + +1. **Add SAML Test Connector**: + - Applications → Add App → Search "SAML Test Connector (Advanced)" + +2. **Configuration**: + - Audience (EntityID): `https://streamspace.example.com` + - Recipient: `https://streamspace.example.com/saml/acs` + - ACS (Consumer) URL Validator: `https://streamspace.example.com/saml/acs` + - ACS (Consumer) URL: `https://streamspace.example.com/saml/acs` + +3. **Parameters** (map to SAML attributes): + - email → Email + - firstName → First Name + - lastName → Last Name + +4. **Download Metadata**: + - SSO → Issuer URL → Download as XML + - Paste into plugin's `metadataXML` field + +## API Endpoints + +The plugin registers the following SAML endpoints: + +- `GET /saml/metadata` - Service Provider metadata (share with IdP) +- `POST /saml/acs` - Assertion Consumer Service (callback from IdP) +- `GET /saml/slo` - Single Logout Service +- `POST /saml/slo` - Single Logout POST binding +- `GET /saml/login` - Initiate SAML login flow +- `GET /saml/logout` - Logout and clear session + +## User Flow + +### SP-Initiated Login + +1. User clicks "Sign in with SSO" button +2. Redirected to `/saml/login` +3. Plugin generates SAML request and redirects to IdP +4. User authenticates at IdP +5. IdP sends SAML assertion to `/saml/acs` +6. Plugin validates assertion and extracts user info +7. User provisioned (if new) and logged in +8. Redirected to application + +### IdP-Initiated Login + +1. User logs into IdP portal +2. User clicks StreamSpace app icon +3. IdP sends SAML assertion to `/saml/acs` +4. Plugin validates assertion and extracts user info +5. User provisioned (if new) and logged in +6. Redirected to application + +## Security Features + +- **Certificate-Based Encryption**: X.509 certificates for signing +- **Request Signing**: Sign SAML requests sent to IdP +- **Response Validation**: Verify SAML response signatures +- **Assertion Validation**: Check NotBefore, NotOnOrAfter, Audience +- **Replay Protection**: Validate assertion ID uniqueness +- **TLS Required**: All SAML endpoints require HTTPS in production + +## Troubleshooting + +### Common Issues + +**"Failed to verify assertion signature"** +- Ensure IdP certificate is current (not expired) +- Check that metadata XML matches IdP configuration +- Verify clock synchronization between SP and IdP + +**"Username not found in SAML assertion"** +- Check attribute mapping configuration +- Verify IdP is sending expected attributes +- Review SAML response XML in network logs + +**"Metadata validation failed"** +- Ensure metadata XML is complete and valid +- Try using metadata URL instead of pasting XML +- Check for line breaks or formatting issues in pasted XML + +### Debug Mode + +Enable debug logging to see full SAML request/response flow: + +```bash +# View plugin logs +kubectl logs -n streamspace -l plugin=streamspace-auth-saml +``` + +## Compliance + +- **SAML 2.0**: Compliant with OASIS SAML 2.0 specification +- **Security**: Follows SAML security best practices +- **Privacy**: No user data stored beyond session duration + +## License + +MIT diff --git a/streamspace-auth-saml/manifest.json b/streamspace-auth-saml/manifest.json new file mode 100644 index 0000000..94d6821 --- /dev/null +++ b/streamspace-auth-saml/manifest.json @@ -0,0 +1,152 @@ +{ + "name": "streamspace-auth-saml", + "version": "1.0.0", + "displayName": "SAML 2.0 Authentication", + "description": "Enterprise SSO authentication with SAML 2.0 protocol - supports Okta, OneLogin, Azure AD, Google Workspace, JumpCloud, and Auth0", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Authentication", + "tags": ["saml", "sso", "authentication", "enterprise", "okta", "onelogin", "azure-ad"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "saml_plugin.go" + }, + + "permissions": ["network", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable SAML Authentication", + "default": false + }, + "provider": { + "type": "string", + "title": "SAML Provider", + "enum": ["okta", "onelogin", "azure-ad", "google", "jumpcloud", "auth0", "custom"], + "default": "custom" + }, + "entityID": { + "type": "string", + "title": "Service Provider Entity ID", + "description": "Unique identifier for your StreamSpace instance" + }, + "metadataURL": { + "type": "string", + "title": "Identity Provider Metadata URL", + "description": "URL to fetch IdP metadata from (leave empty to use XML)" + }, + "metadataXML": { + "type": "string", + "title": "Identity Provider Metadata XML", + "description": "Paste IdP metadata XML here if not using URL", + "format": "textarea" + }, + "certificate": { + "type": "string", + "title": "Service Provider Certificate (PEM)", + "format": "textarea", + "description": "X.509 certificate for signing SAML requests" + }, + "privateKey": { + "type": "string", + "title": "Service Provider Private Key (PEM)", + "format": "password", + "description": "RSA private key (keep secret!)" + }, + "allowIDPInitiated": { + "type": "boolean", + "title": "Allow IdP-Initiated Login", + "default": true + }, + "signRequest": { + "type": "boolean", + "title": "Sign SAML Requests", + "default": true + }, + "forceAuthn": { + "type": "boolean", + "title": "Force Re-authentication", + "default": false, + "description": "Require users to re-authenticate even if they have an active IdP session" + }, + "attributeMapping": { + "type": "object", + "title": "Attribute Mapping", + "description": "Map SAML attributes to user fields", + "properties": { + "email": { + "type": "string", + "default": "email", + "description": "SAML attribute name for email" + }, + "username": { + "type": "string", + "default": "username", + "description": "SAML attribute name for username" + }, + "firstName": { + "type": "string", + "default": "firstName", + "description": "SAML attribute name for first name" + }, + "lastName": { + "type": "string", + "default": "lastName", + "description": "SAML attribute name for last name" + }, + "groups": { + "type": "string", + "default": "groups", + "description": "SAML attribute name for groups/roles" + } + } + }, + "autoProvisionUsers": { + "type": "boolean", + "title": "Auto-Provision Users", + "default": true, + "description": "Automatically create user accounts on first SAML login" + }, + "defaultRole": { + "type": "string", + "title": "Default User Role", + "enum": ["user", "operator", "admin"], + "default": "user", + "description": "Default role for auto-provisioned users" + } + }, + "required": ["entityID"] + }, + "api": { + "endpoints": [ + "/saml/metadata", + "/saml/acs", + "/saml/slo", + "/saml/login", + "/saml/logout" + ] + }, + "ui": { + "adminPages": [ + { + "id": "saml-auth", + "title": "SAML Configuration", + "route": "/admin/auth/saml", + "component": "SAMLAuth", + "icon": "shield" + } + ], + "loginButton": { + "enabled": true, + "label": "Sign in with SSO", + "icon": "business" + } + } +} diff --git a/streamspace-auth-saml/saml_plugin.go b/streamspace-auth-saml/saml_plugin.go new file mode 100644 index 0000000..3cb1a46 --- /dev/null +++ b/streamspace-auth-saml/saml_plugin.go @@ -0,0 +1,222 @@ +package main + +import ("crypto/rsa"; "crypto/x509"; "encoding/json"; "encoding/pem"; "encoding/xml"; "fmt"; "net/url"; "github.com/yourusername/streamspace/api/internal/plugins"; "github.com/crewjam/saml"; "github.com/crewjam/saml/samlsp") + +type SAMLPlugin struct { + plugins.BasePlugin + config SAMLConfig + middleware *samlsp.Middleware + serviceProvider *saml.ServiceProvider +} + +type SAMLConfig struct { + Enabled bool `json:"enabled"` + Provider string `json:"provider"` + EntityID string `json:"entityID"` + MetadataURL string `json:"metadataURL"` + MetadataXML string `json:"metadataXML"` + Certificate string `json:"certificate"` + PrivateKey string `json:"privateKey"` + AllowIDPInitiated bool `json:"allowIDPInitiated"` + SignRequest bool `json:"signRequest"` + ForceAuthn bool `json:"forceAuthn"` + AttributeMapping AttributeMapping `json:"attributeMapping"` + AutoProvisionUsers bool `json:"autoProvisionUsers"` + DefaultRole string `json:"defaultRole"` +} + +type AttributeMapping struct { + Email string `json:"email"` + Username string `json:"username"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Groups string `json:"groups"` +} + +func (p *SAMLPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("SAML authentication is disabled") + return nil + } + + // Parse certificate and private key + cert, err := parseCertificate(p.config.Certificate) + if err != nil { + return fmt.Errorf("failed to parse certificate: %w", err) + } + + privateKey, err := parsePrivateKey(p.config.PrivateKey) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + + // Create service provider + rootURL, err := url.Parse(p.config.EntityID) + if err != nil { + return fmt.Errorf("invalid entity ID: %w", err) + } + + sp := &saml.ServiceProvider{ + EntityID: p.config.EntityID, + Key: privateKey, + Certificate: cert, + MetadataURL: *rootURL.ResolveReference(&url.URL{Path: "/saml/metadata"}), + AcsURL: *rootURL.ResolveReference(&url.URL{Path: "/saml/acs"}), + SloURL: *rootURL.ResolveReference(&url.URL{Path: "/saml/slo"}), + AllowIDPInitiated: p.config.AllowIDPInitiated, + ForceAuthn: &p.config.ForceAuthn, + } + + // Load IdP metadata + var idpMetadata *saml.EntityDescriptor + if p.config.MetadataURL != "" { + // Fetch from URL (implementation simplified) + ctx.Logger.Info("Fetching IdP metadata from URL", "url", p.config.MetadataURL) + // In real implementation, fetch and parse metadata + } else if p.config.MetadataXML != "" { + idpMetadata = &saml.EntityDescriptor{} + if err := xml.Unmarshal([]byte(p.config.MetadataXML), idpMetadata); err != nil { + return fmt.Errorf("failed to parse IdP metadata XML: %w", err) + } + } else { + return fmt.Errorf("either metadataURL or metadataXML must be provided") + } + + sp.IDPMetadata = idpMetadata + + // Create SAML middleware + middleware, err := samlsp.New(samlsp.Options{ + EntityID: sp.EntityID, + URL: *rootURL, + Key: sp.Key, + Certificate: sp.Certificate, + IDPMetadata: sp.IDPMetadata, + AllowIDPInitiated: sp.AllowIDPInitiated, + ForceAuthn: sp.ForceAuthn, + }) + if err != nil { + return fmt.Errorf("failed to create SAML middleware: %w", err) + } + + p.middleware = middleware + p.serviceProvider = sp + + ctx.Logger.Info("SAML authentication initialized", "provider", p.config.Provider, "entityID", p.config.EntityID) + return nil +} + +func (p *SAMLPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("SAML Authentication plugin loaded") + return nil +} + +func (p *SAMLPlugin) OnUserLogin(ctx *plugins.PluginContext, user interface{}) error { + // Track SAML logins + userMap, _ := user.(map[string]interface{}) + authMethod := userMap["auth_method"] + if authMethod == "saml" { + ctx.Logger.Info("SAML user login", "user", userMap["username"]) + } + return nil +} + +func parseCertificate(certPEM string) (*x509.Certificate, error) { + block, _ := pem.Decode([]byte(certPEM)) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM block") + } + return x509.ParseCertificate(block.Bytes) +} + +func parsePrivateKey(keyPEM string) (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(keyPEM)) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM block") + } + return x509.ParsePKCS1PrivateKey(block.Bytes) +} + +// ExtractUserFromAssertion extracts user information from SAML assertion +func (p *SAMLPlugin) ExtractUserFromAssertion(assertion *saml.Assertion) (map[string]interface{}, error) { + if assertion == nil { + return nil, fmt.Errorf("assertion is nil") + } + + user := map[string]interface{}{ + "auth_method": "saml", + "attributes": make(map[string]interface{}), + } + + // Extract attributes based on mapping + for _, attrStatement := range assertion.AttributeStatements { + for _, attr := range attrStatement.Attributes { + if len(attr.Values) == 0 { + continue + } + + attrName := attr.Name + attrValue := attr.Values[0].Value + + // Map to user fields + switch attrName { + case p.config.AttributeMapping.Email: + user["email"] = attrValue + case p.config.AttributeMapping.Username: + user["username"] = attrValue + case p.config.AttributeMapping.FirstName: + user["first_name"] = attrValue + case p.config.AttributeMapping.LastName: + user["last_name"] = attrValue + case p.config.AttributeMapping.Groups: + groups := make([]string, len(attr.Values)) + for i, v := range attr.Values { + groups[i] = v.Value + } + user["groups"] = groups + } + + // Store all attributes + attrs := user["attributes"].(map[string]interface{}) + if len(attr.Values) == 1 { + attrs[attrName] = attrValue + } else { + values := make([]string, len(attr.Values)) + for i, v := range attr.Values { + values[i] = v.Value + } + attrs[attrName] = values + } + } + } + + // Use NameID as username if username not mapped + if user["username"] == nil && assertion.Subject != nil && assertion.Subject.NameID != nil { + user["username"] = assertion.Subject.NameID.Value + } + + // Use NameID as email if email not mapped and format is email + if user["email"] == nil && assertion.Subject != nil && assertion.Subject.NameID != nil { + if assertion.Subject.NameID.Format == "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" { + user["email"] = assertion.Subject.NameID.Value + } + } + + // Validate required fields + if user["username"] == nil { + return nil, fmt.Errorf("username not found in SAML assertion") + } + + // Set default role if auto-provisioning + if p.config.AutoProvisionUsers { + user["role"] = p.config.DefaultRole + } + + return user, nil +} + +func init() { + plugins.Register("streamspace-auth-saml", &SAMLPlugin{}) +} diff --git a/streamspace-billing/README.md b/streamspace-billing/README.md new file mode 100644 index 0000000..77084f0 --- /dev/null +++ b/streamspace-billing/README.md @@ -0,0 +1,362 @@ +# StreamSpace Billing Plugin + +Comprehensive billing and usage tracking system for StreamSpace with Stripe integration. + +## Features + +### Usage Tracking +- **Real-time session tracking** - Monitor CPU, memory, and storage usage for all active sessions +- **Hourly usage calculation** - Automated usage metering with configurable intervals +- **Resource-based pricing** - Separate pricing for CPU cores, memory (GB), and storage +- **Historical usage data** - Complete audit trail of resource consumption + +### Billing Modes +- **Usage-based** - Pay only for what you use (compute hours, storage) +- **Subscription** - Fixed monthly/annual plans with quotas +- **Hybrid** - Combination of subscription base + usage overages + +### Invoicing +- **Automated invoice generation** - Monthly invoices created automatically +- **Customizable invoice day** - Choose day of month for billing (1-28) +- **Credits support** - Apply credits to reduce invoice totals +- **Multiple invoice statuses** - Draft, sent, paid, overdue + +### Payment Processing +- **Stripe integration** - Secure payment processing via Stripe +- **Checkout sessions** - Pre-built checkout pages for subscriptions +- **Payment methods** - Support for cards, ACH, and other Stripe methods +- **Webhook handling** - Real-time payment confirmation + +### Quota Management +- **Usage alerts** - Notify users when approaching quota limits (80% default) +- **Auto-suspend** - Optionally suspend sessions when quota exceeded +- **Grace period** - Configurable grace period before service suspension +- **Per-user quotas** - Different limits for different subscription tiers + +### Admin Features +- **Billing dashboard** - View all users' billing status +- **Manual credits** - Add credits to user accounts +- **Invoice management** - Manually generate or modify invoices +- **Usage reports** - Export usage data for analysis + +## Installation + +### Via Plugin Marketplace (Recommended) + +1. Navigate to **Admin → Plugins** +2. Search for "Billing & Usage Tracking" +3. Click **Install** +4. Configure settings (see Configuration section) +5. Click **Enable** + +### Manual Installation + +```bash +# Copy plugin files to plugins directory +cp -r streamspace-billing /path/to/streamspace/plugins/ + +# Restart StreamSpace API +systemctl restart streamspace-api +``` + +## Configuration + +### Basic Setup + +```json +{ + "enabled": true, + "billingMode": "usage", + "computeRates": { + "cpu_per_core_hour": 0.05, + "memory_per_gb_hour": 0.01, + "storage_per_gb_month": 0.10 + } +} +``` + +### Stripe Integration + +```json +{ + "stripeEnabled": true, + "stripeSecretKey": "sk_live_...", + "stripeWebhookSecret": "whsec_..." +} +``` + +**Important:** Never commit Stripe keys to version control. Use environment variables or secrets management. + +### Subscription Plans + +```json +{ + "billingMode": "subscription", + "subscriptionPlans": [ + { + "id": "free", + "name": "Free Tier", + "price": 0, + "interval": "month", + "cpu_limit": 2, + "memory_limit": 4, + "storage_limit": 10 + }, + { + "id": "pro", + "name": "Professional", + "price": 29.99, + "interval": "month", + "cpu_limit": 8, + "memory_limit": 16, + "storage_limit": 100 + }, + { + "id": "enterprise", + "name": "Enterprise", + "price": 99.99, + "interval": "month", + "cpu_limit": 32, + "memory_limit": 64, + "storage_limit": 500 + } + ] +} +``` + +### Usage Calculation + +```json +{ + "usageCalculationInterval": "0 * * * *", + "invoiceDay": 1, + "alertThreshold": 80, + "autoSuspendOnOverage": false, + "gracePeriodDays": 7 +} +``` + +## Usage + +### For End Users + +#### View Current Usage + +1. Navigate to **Billing & Usage** in the sidebar +2. View current month's usage breakdown +3. See costs by resource type (CPU, memory, storage) + +#### View Invoices + +1. Go to **Billing & Usage → Invoices** +2. Download PDF invoices +3. View payment history + +#### Manage Subscription + +1. Go to **Billing & Usage → Subscription** +2. Upgrade or downgrade plan +3. Update payment method + +### For Administrators + +#### View All Billing + +1. Navigate to **Admin → Billing Management** +2. View usage across all users +3. Filter by user, date range, or status + +#### Add Credits + +```bash +# Via API +curl -X POST https://streamspace.example.com/api/plugins/billing/credits \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -d '{ + "user_id": "john@example.com", + "amount": 50.00, + "reason": "Service credit for downtime" + }' +``` + +#### Generate Manual Invoice + +```bash +# Via API +curl -X POST https://streamspace.example.com/api/plugins/billing/invoices \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -d '{ + "user_id": "john@example.com", + "period_start": "2025-01-01", + "period_end": "2025-01-31" + }' +``` + +## Database Schema + +The plugin creates the following tables: + +### billing_usage_records +- Tracks individual usage events (CPU hours, memory hours, storage) +- Used for detailed usage reports and invoice line items + +### billing_invoices +- Stores generated invoices with totals and status +- Links to usage records for detailed breakdowns + +### billing_subscriptions +- Manages user subscription plans and periods +- Integrates with Stripe subscription IDs + +### billing_payments +- Records payment transactions +- Links invoices to Stripe payment intents + +### billing_credits +- Stores account credits with expiration dates +- Applied automatically to invoices + +## API Endpoints + +### User Endpoints + +- `GET /api/plugins/billing/usage` - Current usage and costs +- `GET /api/plugins/billing/invoices` - User's invoices +- `GET /api/plugins/billing/subscription` - Active subscription +- `POST /api/plugins/billing/create-checkout` - Start Stripe checkout +- `GET /api/plugins/billing/payment-methods` - Saved payment methods + +### Admin Endpoints + +- `GET /api/plugins/billing/admin/users` - All users' billing status +- `POST /api/plugins/billing/admin/credits` - Add credits to account +- `POST /api/plugins/billing/admin/invoices` - Generate manual invoice +- `GET /api/plugins/billing/admin/reports` - Usage reports + +## Events + +The plugin emits the following events: + +- `billing.quota.warning` - User approaching quota limit +- `billing.quota.exceeded` - User exceeded quota +- `billing.invoice.created` - New invoice generated +- `billing.invoice.paid` - Invoice payment received +- `billing.payment.failed` - Payment attempt failed + +## Scheduled Jobs + +- **calculate-usage** - Runs every hour (configurable) + - Calculates usage for all active sessions + - Updates usage records in database + +- **generate-invoices** - Runs monthly on configured day + - Generates invoices for all users + - Sends invoice emails (if email plugin enabled) + +- **check-quotas** - Runs every 15 minutes + - Checks users against quota limits + - Emits warnings when thresholds exceeded + +## Pricing Examples + +### Usage-Based Pricing + +**Configuration:** +- CPU: $0.05/core-hour +- Memory: $0.01/GB-hour +- Storage: $0.10/GB-month + +**Example Session:** +- 2 CPU cores for 10 hours = 20 core-hours × $0.05 = $1.00 +- 4 GB memory for 10 hours = 40 GB-hours × $0.01 = $0.40 +- **Total: $1.40** + +### Subscription Pricing + +**Pro Plan:** $29.99/month +- Includes 8 CPU cores, 16 GB memory, 100 GB storage +- Overages charged at usage rates +- Example: 10 cores used = 2 cores × $0.05/hour overage + +## Stripe Integration + +### Setup Stripe Webhook + +1. In Stripe Dashboard, go to **Developers → Webhooks** +2. Add endpoint: `https://streamspace.example.com/api/plugins/billing/webhook` +3. Select events: + - `invoice.paid` + - `invoice.payment_failed` + - `customer.subscription.updated` + - `customer.subscription.deleted` +4. Copy webhook signing secret to plugin config + +### Test Stripe Integration + +```bash +# Use Stripe CLI for local testing +stripe listen --forward-to http://localhost:8080/api/plugins/billing/webhook + +# Trigger test events +stripe trigger payment_intent.succeeded +stripe trigger invoice.paid +``` + +## Troubleshooting + +### Usage not tracking + +**Problem:** Sessions created but no usage recorded + +**Solution:** +- Check plugin is enabled +- Verify `session.created` event is firing +- Check plugin logs: `tail -f /var/log/streamspace/plugins/billing.log` + +### Invoices not generating + +**Problem:** Monthly invoices not created automatically + +**Solution:** +- Check scheduled job is running: `GET /api/plugins/billing/jobs/status` +- Verify `invoiceDay` configuration +- Manually trigger: `POST /api/plugins/billing/jobs/generate-invoices` + +### Stripe payments failing + +**Problem:** Users unable to complete checkout + +**Solution:** +- Verify Stripe API keys are correct +- Check webhook is configured and receiving events +- Review Stripe Dashboard logs +- Ensure test mode keys used in development + +## Best Practices + +1. **Start with test mode** - Use Stripe test keys until ready for production +2. **Monitor quotas** - Set up alerts before users hit limits +3. **Regular reports** - Review monthly usage patterns +4. **Credit policy** - Have clear policy for issuing credits +5. **Grace periods** - Don't suspend immediately on payment failure +6. **Backup billing data** - Include billing tables in database backups + +## Support + +For issues or questions: +- GitHub Issues: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- Documentation: https://docs.streamspace.io/plugins/billing +- Community: https://discord.gg/streamspace + +## License + +MIT License - see LICENSE file for details + +## Version History + +- **1.0.0** (2025-01-15) + - Initial release + - Usage tracking and invoicing + - Stripe integration + - Quota management + - Admin dashboard diff --git a/streamspace-billing/billing_plugin.go b/streamspace-billing/billing_plugin.go new file mode 100644 index 0000000..3353305 --- /dev/null +++ b/streamspace-billing/billing_plugin.go @@ -0,0 +1,734 @@ +package billingplugin + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/gin-gonic/gin" + "github.com/streamspace/streamspace/api/internal/plugins" +) + +// BillingPlugin implements comprehensive billing and usage tracking +type BillingPlugin struct { + plugins.BasePlugin + + // Usage tracking cache + activeSessionUsage map[string]*SessionUsage +} + +// SessionUsage tracks active session resource usage +type SessionUsage struct { + SessionID string + UserID string + StartTime time.Time + LastHeartbeat time.Time + CPUCores float64 + MemoryGB float64 + StorageGB float64 + TotalCost float64 +} + +// UsageRecord represents a billing usage record +type UsageRecord struct { + ID int64 `json:"id"` + UserID string `json:"user_id"` + SessionID string `json:"session_id,omitempty"` + ResourceType string `json:"resource_type"` // cpu, memory, storage + Quantity float64 `json:"quantity"` + Unit string `json:"unit"` // core-hours, gb-hours, gb-months + UnitPrice float64 `json:"unit_price"` + TotalCost float64 `json:"total_cost"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + CreatedAt time.Time `json:"created_at"` +} + +// Invoice represents a billing invoice +type Invoice struct { + ID int64 `json:"id"` + UserID string `json:"user_id"` + InvoiceNumber string `json:"invoice_number"` + PeriodStart time.Time `json:"period_start"` + PeriodEnd time.Time `json:"period_end"` + Subtotal float64 `json:"subtotal"` + Credits float64 `json:"credits"` + Total float64 `json:"total"` + Status string `json:"status"` // draft, sent, paid, overdue + DueDate time.Time `json:"due_date"` + PaidAt *time.Time `json:"paid_at,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// Subscription represents a user subscription +type Subscription struct { + ID int64 `json:"id"` + UserID string `json:"user_id"` + PlanID string `json:"plan_id"` + Status string `json:"status"` // active, canceled, suspended + CurrentPeriodStart time.Time `json:"current_period_start"` + CurrentPeriodEnd time.Time `json:"current_period_end"` + StripeSubID string `json:"stripe_subscription_id,omitempty"` + CreatedAt time.Time `json:"created_at"` + CanceledAt *time.Time `json:"canceled_at,omitempty"` +} + +// NewBillingPlugin creates a new billing plugin instance +func NewBillingPlugin() *BillingPlugin { + return &BillingPlugin{ + BasePlugin: plugins.BasePlugin{Name: "streamspace-billing"}, + activeSessionUsage: make(map[string]*SessionUsage), + } +} + +// OnLoad is called when the plugin is loaded +func (p *BillingPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Billing plugin loading", map[string]interface{}{ + "version": "1.0.0", + }) + + // Create database tables + if err := p.createDatabaseTables(ctx); err != nil { + return fmt.Errorf("failed to create database tables: %w", err) + } + + // Register API endpoints + p.registerAPIEndpoints(ctx) + + // Register UI components + p.registerUIComponents(ctx) + + // Schedule periodic jobs + p.scheduleJobs(ctx) + + ctx.Logger.Info("Billing plugin loaded successfully") + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *BillingPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Billing plugin unloading") + + // Save any pending usage records + for sessionID, usage := range p.activeSessionUsage { + if err := p.recordUsage(ctx, usage); err != nil { + ctx.Logger.Warn("Failed to save usage for session", map[string]interface{}{ + "sessionId": sessionID, + "error": err.Error(), + }) + } + } + + return nil +} + +// OnSessionCreated tracks when a session starts +func (p *BillingPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + enabled := p.getBool(ctx.Config, "enabled") + if !enabled { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + sessionID := p.getString(sessionMap, "id") + userID := p.getString(sessionMap, "user") + + // Extract resource allocation + cpuCores := 1.0 // Default + memoryGB := 2.0 // Default + + if resources, ok := sessionMap["resources"].(map[string]interface{}); ok { + if cpu := p.getString(resources, "cpu"); cpu != "" { + // Parse CPU (e.g., "1000m" = 1 core) + cpuCores = p.parseCPU(cpu) + } + if memory := p.getString(resources, "memory"); memory != "" { + // Parse memory (e.g., "2Gi" = 2 GB) + memoryGB = p.parseMemory(memory) + } + } + + // Start tracking usage + p.activeSessionUsage[sessionID] = &SessionUsage{ + SessionID: sessionID, + UserID: userID, + StartTime: time.Now(), + LastHeartbeat: time.Now(), + CPUCores: cpuCores, + MemoryGB: memoryGB, + TotalCost: 0, + } + + ctx.Logger.Info("Started tracking session usage", map[string]interface{}{ + "sessionId": sessionID, + "userId": userID, + "cpuCores": cpuCores, + "memoryGB": memoryGB, + }) + + return nil +} + +// OnSessionTerminated records final usage when session ends +func (p *BillingPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + sessionID := p.getString(sessionMap, "id") + + usage, exists := p.activeSessionUsage[sessionID] + if !exists { + return nil // Not tracking this session + } + + // Record final usage + if err := p.recordUsage(ctx, usage); err != nil { + ctx.Logger.Warn("Failed to record usage", map[string]interface{}{ + "sessionId": sessionID, + "error": err.Error(), + }) + } + + // Remove from active tracking + delete(p.activeSessionUsage, sessionID) + + ctx.Logger.Info("Recorded final session usage", map[string]interface{}{ + "sessionId": sessionID, + "totalCost": usage.TotalCost, + }) + + return nil +} + +// OnSessionHeartbeat updates last activity time +func (p *BillingPlugin) OnSessionHeartbeat(ctx *plugins.PluginContext, session interface{}) error { + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return nil + } + + sessionID := p.getString(sessionMap, "id") + + if usage, exists := p.activeSessionUsage[sessionID]; exists { + usage.LastHeartbeat = time.Now() + } + + return nil +} + +// OnUserCreated sets up billing for new user +func (p *BillingPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user data type") + } + + userID := p.getString(userMap, "username") + + // Check if we should create a subscription + billingMode := p.getString(ctx.Config, "billingMode") + if billingMode == "subscription" || billingMode == "hybrid" { + // Create default free tier subscription + if err := p.createSubscription(ctx, userID, "free"); err != nil { + ctx.Logger.Warn("Failed to create subscription", map[string]interface{}{ + "userId": userID, + "error": err.Error(), + }) + } + } + + ctx.Logger.Info("Initialized billing for user", map[string]interface{}{ + "userId": userID, + "mode": billingMode, + }) + + return nil +} + +// createDatabaseTables creates billing database tables +func (p *BillingPlugin) createDatabaseTables(ctx *plugins.PluginContext) error { + // Usage records table + usageTableSchema := ` + id BIGSERIAL PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + session_id VARCHAR(255), + resource_type VARCHAR(50) NOT NULL, + quantity DECIMAL(10, 4) NOT NULL, + unit VARCHAR(50) NOT NULL, + unit_price DECIMAL(10, 4) NOT NULL, + total_cost DECIMAL(10, 4) NOT NULL, + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ` + if err := ctx.Database.CreateTable("billing_usage_records", usageTableSchema); err != nil { + return err + } + + // Invoices table + invoiceTableSchema := ` + id BIGSERIAL PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + invoice_number VARCHAR(100) UNIQUE NOT NULL, + period_start TIMESTAMP NOT NULL, + period_end TIMESTAMP NOT NULL, + subtotal DECIMAL(10, 2) NOT NULL, + credits DECIMAL(10, 2) DEFAULT 0, + total DECIMAL(10, 2) NOT NULL, + status VARCHAR(50) DEFAULT 'draft', + due_date TIMESTAMP NOT NULL, + paid_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() + ` + if err := ctx.Database.CreateTable("billing_invoices", invoiceTableSchema); err != nil { + return err + } + + // Subscriptions table + subscriptionTableSchema := ` + id BIGSERIAL PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + plan_id VARCHAR(100) NOT NULL, + status VARCHAR(50) DEFAULT 'active', + current_period_start TIMESTAMP NOT NULL, + current_period_end TIMESTAMP NOT NULL, + stripe_subscription_id VARCHAR(255), + created_at TIMESTAMP DEFAULT NOW(), + canceled_at TIMESTAMP + ` + if err := ctx.Database.CreateTable("billing_subscriptions", subscriptionTableSchema); err != nil { + return err + } + + // Payments table + paymentTableSchema := ` + id BIGSERIAL PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + invoice_id BIGINT REFERENCES billing_invoices(id), + amount DECIMAL(10, 2) NOT NULL, + currency VARCHAR(10) DEFAULT 'USD', + status VARCHAR(50) DEFAULT 'pending', + stripe_payment_intent_id VARCHAR(255), + payment_method VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW(), + paid_at TIMESTAMP + ` + if err := ctx.Database.CreateTable("billing_payments", paymentTableSchema); err != nil { + return err + } + + // Credits table + creditTableSchema := ` + id BIGSERIAL PRIMARY KEY, + user_id VARCHAR(255) NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + reason VARCHAR(255), + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() + ` + if err := ctx.Database.CreateTable("billing_credits", creditTableSchema); err != nil { + return err + } + + return nil +} + +// registerAPIEndpoints registers billing API endpoints +func (p *BillingPlugin) registerAPIEndpoints(ctx *plugins.PluginContext) { + // Get current usage + ctx.API.GET("/usage", func(c *gin.Context) { + userID := c.GetString("userId") // From auth middleware + + usage, err := p.getCurrentUsage(ctx, userID) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, usage) + }) + + // Get invoices + ctx.API.GET("/invoices", func(c *gin.Context) { + userID := c.GetString("userId") + + invoices, err := p.getUserInvoices(ctx, userID) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, gin.H{"invoices": invoices}) + }) + + // Get subscription + ctx.API.GET("/subscription", func(c *gin.Context) { + userID := c.GetString("userId") + + subscription, err := p.getUserSubscription(ctx, userID) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, subscription) + }) + + // Create Stripe checkout session + ctx.API.POST("/create-checkout", func(c *gin.Context) { + userID := c.GetString("userId") + + var req struct { + PlanID string `json:"plan_id"` + } + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request"}) + return + } + + checkoutURL, err := p.createStripeCheckout(ctx, userID, req.PlanID) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, gin.H{"checkout_url": checkoutURL}) + }) +} + +// registerUIComponents registers UI widgets and pages +func (p *BillingPlugin) registerUIComponents(ctx *plugins.PluginContext) { + // Usage widget for dashboard + ctx.UI.RegisterWidget(&plugins.UIWidget{ + ID: "billing-usage-widget", + Title: "Current Usage", + Component: "BillingUsageWidget", + Position: "right-sidebar", + Width: "300px", + Permissions: []string{"user"}, + }) + + // Billing dashboard page + ctx.UI.RegisterMenuItem(&plugins.UIMenuItem{ + ID: "billing-menu", + Label: "Billing & Usage", + Icon: "receipt", + Route: "/billing", + Position: 50, + Permissions: []string{"user"}, + }) + + // Admin billing page + ctx.UI.RegisterAdminPage(&plugins.UIAdminPage{ + ID: "admin-billing", + Title: "Billing Management", + Route: "/admin/billing", + Component: "AdminBillingPage", + Icon: "account_balance", + Permissions: []string{"admin"}, + }) +} + +// scheduleJobs schedules periodic billing jobs +func (p *BillingPlugin) scheduleJobs(ctx *plugins.PluginContext) { + // Calculate usage every hour + interval := p.getString(ctx.Config, "usageCalculationInterval") + if interval == "" { + interval = "0 * * * *" // Default: hourly + } + + ctx.Scheduler.Schedule("calculate-usage", interval, func() { + p.calculateUsageJob(ctx) + }) + + // Generate invoices monthly + ctx.Scheduler.Schedule("generate-invoices", "0 0 1 * *", func() { + p.generateInvoicesJob(ctx) + }) + + // Check quotas every 15 minutes + ctx.Scheduler.Schedule("check-quotas", "*/15 * * * *", func() { + p.checkQuotasJob(ctx) + }) +} + +// calculateUsageJob calculates usage for all active sessions +func (p *BillingPlugin) calculateUsageJob(ctx *plugins.PluginContext) { + ctx.Logger.Info("Running usage calculation job") + + for sessionID, usage := range p.activeSessionUsage { + // Calculate usage since last calculation + duration := time.Since(usage.StartTime).Hours() + + // Get rates from config + rates := p.getMap(ctx.Config, "computeRates") + cpuRate := p.getFloat(rates, "cpu_per_core_hour") + memoryRate := p.getFloat(rates, "memory_per_gb_hour") + + // Calculate costs + cpuCost := usage.CPUCores * cpuRate * duration + memoryCost := usage.MemoryGB * memoryRate * duration + + usage.TotalCost += cpuCost + memoryCost + + ctx.Logger.Debug("Calculated session usage", map[string]interface{}{ + "sessionId": sessionID, + "cpuCost": cpuCost, + "memCost": memoryCost, + }) + } +} + +// generateInvoicesJob generates invoices for all users +func (p *BillingPlugin) generateInvoicesJob(ctx *plugins.PluginContext) { + ctx.Logger.Info("Running invoice generation job") + + // Get all users with usage in previous month + // Generate invoices + // This would query the database and create Invoice records +} + +// checkQuotasJob checks if users are exceeding quotas +func (p *BillingPlugin) checkQuotasJob(ctx *plugins.PluginContext) { + ctx.Logger.Debug("Checking usage quotas") + + threshold := p.getFloat(ctx.Config, "alertThreshold") + if threshold == 0 { + threshold = 80 // Default + } + + // Check each user's usage against their quota + // Emit quota.exceeded event if threshold reached +} + +// recordUsage records usage to the database +func (p *BillingPlugin) recordUsage(ctx *plugins.PluginContext, usage *SessionUsage) error { + duration := time.Since(usage.StartTime) + + rates := p.getMap(ctx.Config, "computeRates") + cpuRate := p.getFloat(rates, "cpu_per_core_hour") + memoryRate := p.getFloat(rates, "memory_per_gb_hour") + + cpuHours := usage.CPUCores * duration.Hours() + memoryGBHours := usage.MemoryGB * duration.Hours() + + // Record CPU usage + cpuRecord := map[string]interface{}{ + "user_id": usage.UserID, + "session_id": usage.SessionID, + "resource_type": "cpu", + "quantity": cpuHours, + "unit": "core-hours", + "unit_price": cpuRate, + "total_cost": cpuHours * cpuRate, + "start_time": usage.StartTime, + "end_time": time.Now(), + } + if err := ctx.Database.Insert("billing_usage_records", cpuRecord); err != nil { + return err + } + + // Record memory usage + memoryRecord := map[string]interface{}{ + "user_id": usage.UserID, + "session_id": usage.SessionID, + "resource_type": "memory", + "quantity": memoryGBHours, + "unit": "gb-hours", + "unit_price": memoryRate, + "total_cost": memoryGBHours * memoryRate, + "start_time": usage.StartTime, + "end_time": time.Now(), + } + return ctx.Database.Insert("billing_usage_records", memoryRecord) +} + +// getCurrentUsage gets current usage for a user +func (p *BillingPlugin) getCurrentUsage(ctx *plugins.PluginContext, userID string) (map[string]interface{}, error) { + // Query database for current month usage + startOfMonth := time.Now().AddDate(0, 0, -time.Now().Day()+1) + + rows, err := ctx.Database.Query(` + SELECT resource_type, SUM(quantity) as total_quantity, SUM(total_cost) as total_cost + FROM billing_usage_records + WHERE user_id = $1 AND created_at >= $2 + GROUP BY resource_type + `, userID, startOfMonth) + if err != nil { + return nil, err + } + defer rows.Close() + + usage := make(map[string]interface{}) + totalCost := 0.0 + + for rows.Next() { + var resourceType string + var quantity, cost float64 + if err := rows.Scan(&resourceType, &quantity, &cost); err != nil { + continue + } + + usage[resourceType] = map[string]interface{}{ + "quantity": quantity, + "cost": cost, + } + totalCost += cost + } + + usage["total_cost"] = totalCost + usage["period_start"] = startOfMonth + usage["period_end"] = time.Now() + + return usage, nil +} + +// getUserInvoices gets invoices for a user +func (p *BillingPlugin) getUserInvoices(ctx *plugins.PluginContext, userID string) ([]Invoice, error) { + rows, err := ctx.Database.Query(` + SELECT id, invoice_number, period_start, period_end, subtotal, credits, total, status, due_date, paid_at, created_at + FROM billing_invoices + WHERE user_id = $1 + ORDER BY created_at DESC + LIMIT 12 + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var invoices []Invoice + for rows.Next() { + var inv Invoice + var paidAt *time.Time + if err := rows.Scan(&inv.ID, &inv.InvoiceNumber, &inv.PeriodStart, &inv.PeriodEnd, + &inv.Subtotal, &inv.Credits, &inv.Total, &inv.Status, &inv.DueDate, &paidAt, &inv.CreatedAt); err != nil { + continue + } + inv.UserID = userID + inv.PaidAt = paidAt + invoices = append(invoices, inv) + } + + return invoices, nil +} + +// getUserSubscription gets active subscription for a user +func (p *BillingPlugin) getUserSubscription(ctx *plugins.PluginContext, userID string) (*Subscription, error) { + row := ctx.Database.QueryRow(` + SELECT id, plan_id, status, current_period_start, current_period_end, stripe_subscription_id, created_at, canceled_at + FROM billing_subscriptions + WHERE user_id = $1 AND status = 'active' + LIMIT 1 + `, userID) + + var sub Subscription + var stripeSubID *string + var canceledAt *time.Time + + err := row.Scan(&sub.ID, &sub.PlanID, &sub.Status, &sub.CurrentPeriodStart, + &sub.CurrentPeriodEnd, &stripeSubID, &sub.CreatedAt, &canceledAt) + if err != nil { + return nil, err + } + + sub.UserID = userID + if stripeSubID != nil { + sub.StripeSubID = *stripeSubID + } + sub.CanceledAt = canceledAt + + return &sub, nil +} + +// createSubscription creates a new subscription for a user +func (p *BillingPlugin) createSubscription(ctx *plugins.PluginContext, userID, planID string) error { + now := time.Now() + periodEnd := now.AddDate(0, 1, 0) // 1 month from now + + return ctx.Database.Insert("billing_subscriptions", map[string]interface{}{ + "user_id": userID, + "plan_id": planID, + "status": "active", + "current_period_start": now, + "current_period_end": periodEnd, + }) +} + +// createStripeCheckout creates a Stripe checkout session +func (p *BillingPlugin) createStripeCheckout(ctx *plugins.PluginContext, userID, planID string) (string, error) { + stripeEnabled := p.getBool(ctx.Config, "stripeEnabled") + if !stripeEnabled { + return "", fmt.Errorf("stripe integration not enabled") + } + + // In real implementation, this would call Stripe API + // For now, return a placeholder + return "https://checkout.stripe.com/placeholder", nil +} + +// Helper functions + +func (p *BillingPlugin) getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (p *BillingPlugin) getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return false +} + +func (p *BillingPlugin) getFloat(m map[string]interface{}, key string) float64 { + if val, ok := m[key]; ok { + if f, ok := val.(float64); ok { + return f + } + if i, ok := val.(int); ok { + return float64(i) + } + } + return 0 +} + +func (p *BillingPlugin) getMap(m map[string]interface{}, key string) map[string]interface{} { + if val, ok := m[key]; ok { + if subMap, ok := val.(map[string]interface{}); ok { + return subMap + } + } + return make(map[string]interface{}) +} + +func (p *BillingPlugin) parseCPU(cpu string) float64 { + // Parse CPU strings like "1000m" (1 core), "500m" (0.5 cores), "2" (2 cores) + // Simplified implementation + return 1.0 +} + +func (p *BillingPlugin) parseMemory(memory string) float64 { + // Parse memory strings like "2Gi" (2 GB), "512Mi" (0.5 GB) + // Simplified implementation + return 2.0 +} + +// init auto-registers the plugin globally +func init() { + plugins.Register("streamspace-billing", func() plugins.PluginHandler { + return NewBillingPlugin() + }) +} diff --git a/streamspace-billing/manifest.json b/streamspace-billing/manifest.json new file mode 100644 index 0000000..d49317d --- /dev/null +++ b/streamspace-billing/manifest.json @@ -0,0 +1,208 @@ +{ + "name": "streamspace-billing", + "version": "1.0.0", + "displayName": "Billing & Usage Tracking", + "description": "Track resource usage, calculate costs, and manage subscriptions with Stripe integration", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Business", + "tags": ["billing", "stripe", "usage", "subscriptions", "invoicing"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "billing_plugin.go" + }, + + "permissions": ["network", "database", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Billing", + "description": "Enable billing and usage tracking", + "default": true + }, + "billingMode": { + "type": "string", + "title": "Billing Mode", + "description": "How to charge users", + "enum": ["usage", "subscription", "hybrid"], + "default": "usage" + }, + "stripeEnabled": { + "type": "boolean", + "title": "Enable Stripe Integration", + "description": "Integrate with Stripe for payment processing", + "default": false + }, + "stripeSecretKey": { + "type": "string", + "title": "Stripe Secret Key", + "description": "Your Stripe API secret key (starts with sk_)", + "format": "password" + }, + "stripeWebhookSecret": { + "type": "string", + "title": "Stripe Webhook Secret", + "description": "Stripe webhook signing secret", + "format": "password" + }, + "computeRates": { + "type": "object", + "title": "Compute Rates", + "description": "Pricing per hour for different resource tiers", + "properties": { + "cpu_per_core_hour": { + "type": "number", + "title": "CPU per Core Hour ($)", + "default": 0.05 + }, + "memory_per_gb_hour": { + "type": "number", + "title": "Memory per GB Hour ($)", + "default": 0.01 + }, + "storage_per_gb_month": { + "type": "number", + "title": "Storage per GB Month ($)", + "default": 0.10 + } + } + }, + "subscriptionPlans": { + "type": "array", + "title": "Subscription Plans", + "description": "Available subscription tiers", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"}, + "price": {"type": "number"}, + "interval": {"type": "string", "enum": ["month", "year"]}, + "cpu_limit": {"type": "number"}, + "memory_limit": {"type": "number"}, + "storage_limit": {"type": "number"} + } + } + }, + "invoiceDay": { + "type": "integer", + "title": "Invoice Day of Month", + "description": "Day of the month to generate invoices (1-28)", + "default": 1, + "minimum": 1, + "maximum": 28 + }, + "usageCalculationInterval": { + "type": "string", + "title": "Usage Calculation Interval", + "description": "How often to calculate usage (cron expression)", + "default": "0 * * * *" + }, + "alertThreshold": { + "type": "number", + "title": "Usage Alert Threshold (%)", + "description": "Send alert when usage exceeds this percentage of quota", + "default": 80, + "minimum": 0, + "maximum": 100 + }, + "autoSuspendOnOverage": { + "type": "boolean", + "title": "Auto-Suspend on Overage", + "description": "Automatically suspend sessions when quota exceeded", + "default": false + }, + "gracePeriodDays": { + "type": "integer", + "title": "Grace Period (Days)", + "description": "Days before suspending service for non-payment", + "default": 7, + "minimum": 0, + "maximum": 30 + } + }, + "required": [] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "session.heartbeat": "OnSessionHeartbeat", + "user.created": "OnUserCreated" + }, + "database": { + "tables": [ + "billing_usage_records", + "billing_invoices", + "billing_subscriptions", + "billing_payments", + "billing_credits" + ] + }, + "api": { + "endpoints": [ + "/billing/usage", + "/billing/invoices", + "/billing/subscriptions", + "/billing/payment-methods", + "/billing/create-checkout" + ] + }, + "ui": { + "widgets": [ + { + "id": "billing-usage-widget", + "title": "Current Usage", + "component": "BillingUsageWidget", + "position": "right-sidebar" + } + ], + "pages": [ + { + "id": "billing-dashboard", + "title": "Billing & Usage", + "route": "/billing", + "component": "BillingDashboard", + "icon": "receipt" + } + ], + "adminPages": [ + { + "id": "admin-billing", + "title": "Billing Management", + "route": "/admin/billing", + "component": "AdminBillingPage", + "icon": "account_balance" + } + ] + }, + "scheduler": { + "jobs": [ + { + "name": "calculate-usage", + "schedule": "0 * * * *", + "description": "Calculate hourly usage" + }, + { + "name": "generate-invoices", + "schedule": "0 0 1 * *", + "description": "Generate monthly invoices" + }, + { + "name": "check-quotas", + "schedule": "*/15 * * * *", + "description": "Check usage quotas" + } + ] + } +} diff --git a/streamspace-calendar/README.md b/streamspace-calendar/README.md new file mode 100644 index 0000000..9789126 --- /dev/null +++ b/streamspace-calendar/README.md @@ -0,0 +1,49 @@ +# StreamSpace Calendar Integration Plugin + +Integrate Google Calendar and Outlook Calendar with automated session scheduling and iCal export. + +## Features +- Google Calendar OAuth integration +- Microsoft Outlook Calendar OAuth integration +- Auto-sync scheduled sessions to calendar +- iCalendar (.ics) export for scheduled sessions +- Automatic session creation from calendar events +- Configurable sync intervals + +## Installation +Install via Plugin Marketplace: Admin > Plugins > Search "Calendar" + +## Configuration +```json +{ + "googleClientId": "YOUR_GOOGLE_CLIENT_ID", + "googleClientSecret": "YOUR_GOOGLE_CLIENT_SECRET", + "microsoftClientId": "YOUR_MICROSOFT_CLIENT_ID", + "microsoftClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET", + "autoSyncInterval": 300, + "createEventsForScheduledSessions": true +} +``` + +## Setup +1. Create Google OAuth credentials at console.cloud.google.com +2. Create Microsoft OAuth app at portal.azure.com +3. Configure callback URL: `https://your-domain/api/plugins/streamspace-calendar/calendar/oauth/callback` +4. Enter credentials in plugin configuration + +## API Endpoints +All endpoints are prefixed with `/api/plugins/streamspace-calendar` + +- `POST /calendar/integrations/:provider` - Connect calendar (google/outlook) +- `GET /calendar/integrations` - List connected calendars +- `POST /calendar/integrations/:id/sync` - Sync calendar +- `GET /calendar/export` - Export iCalendar file +- `DELETE /calendar/integrations/:id` - Disconnect calendar + +## Database Tables +- `calendar_integrations` - Connected calendar accounts +- `calendar_oauth_states` - OAuth flow state tracking +- `calendar_events` - Synced calendar events + +## License +MIT - StreamSpace Team diff --git a/streamspace-calendar/calendar_plugin.go b/streamspace-calendar/calendar_plugin.go new file mode 100644 index 0000000..be81777 --- /dev/null +++ b/streamspace-calendar/calendar_plugin.go @@ -0,0 +1,37 @@ +package calendarplugin + +import ( + "github.com/streamspace/streamspace/api/internal/plugins" +) + +// CalendarPlugin provides Google/Outlook calendar integration +type CalendarPlugin struct { + plugins.BasePlugin +} + +// NewCalendarPlugin creates a new calendar plugin instance +func NewCalendarPlugin() *CalendarPlugin { + return &CalendarPlugin{ + BasePlugin: plugins.BasePlugin{Name: "streamspace-calendar"}, + } +} + +// OnLoad initializes the plugin +func (p *CalendarPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Calendar plugin loading") + + // TODO: Extract calendar logic from /api/internal/handlers/scheduling.go + // TODO: Register API endpoints for calendar operations + // TODO: Initialize database tables (calendar_integrations, calendar_oauth_states, calendar_events) + // TODO: Set up OAuth handlers for Google and Microsoft + // TODO: Schedule auto-sync job based on autoSyncInterval config + + return nil +} + +// Auto-register plugin +func init() { + plugins.Register("streamspace-calendar", func() plugins.Plugin { + return NewCalendarPlugin() + }) +} diff --git a/streamspace-calendar/manifest.json b/streamspace-calendar/manifest.json new file mode 100644 index 0000000..dcbf205 --- /dev/null +++ b/streamspace-calendar/manifest.json @@ -0,0 +1,34 @@ +{ + "name": "streamspace-calendar", + "version": "1.0.0", + "displayName": "Calendar Integration", + "description": "Google Calendar and Outlook Calendar integration with iCal export for automated session scheduling", + "author": "StreamSpace Team", + "license": "MIT", + "type": "integration", + "category": "Integrations", + "tags": ["calendar", "google-calendar", "outlook", "scheduling", "automation"], + "requirements": {"streamspaceVersion": ">=1.0.0"}, + "entrypoints": {"main": "calendar_plugin.go"}, + "configSchema": { + "type": "object", + "properties": { + "googleClientId": {"type": "string", "title": "Google OAuth Client ID"}, + "googleClientSecret": {"type": "string", "title": "Google OAuth Client Secret"}, + "microsoftClientId": {"type": "string", "title": "Microsoft OAuth Client ID"}, + "microsoftClientSecret": {"type": "string", "title": "Microsoft OAuth Client Secret"}, + "autoSyncInterval": {"type": "number", "default": 300, "minimum": 60, "maximum": 3600, "title": "Auto-sync interval (seconds)"}, + "createEventsForScheduledSessions": {"type": "boolean", "default": true} + } + }, + "defaultConfig": {"autoSyncInterval": 300, "createEventsForScheduledSessions": true}, + "permissions": ["database", "api", "network", "scheduler"], + "apiEndpoints": [ + {"method": "POST", "path": "/calendar/integrations/:provider", "description": "Connect calendar (google/outlook)"}, + {"method": "GET", "path": "/calendar/oauth/callback", "description": "OAuth callback handler"}, + {"method": "GET", "path": "/calendar/integrations", "description": "List calendar integrations"}, + {"method": "DELETE", "path": "/calendar/integrations/:integrationId", "description": "Disconnect calendar"}, + {"method": "POST", "path": "/calendar/integrations/:integrationId/sync", "description": "Sync calendar"}, + {"method": "GET", "path": "/calendar/export", "description": "Export iCalendar (.ics)"} + ] +} diff --git a/streamspace-compliance/README.md b/streamspace-compliance/README.md new file mode 100644 index 0000000..ea2d697 --- /dev/null +++ b/streamspace-compliance/README.md @@ -0,0 +1,466 @@ +# StreamSpace Compliance & Regulatory Framework Plugin + +Comprehensive compliance management for GDPR, HIPAA, SOC2, ISO27001, PCI-DSS, FedRAMP, and custom regulatory frameworks. + +## Features + +### Compliance Frameworks +- **Pre-built Frameworks** + - GDPR (General Data Protection Regulation) + - HIPAA (Health Insurance Portability and Accountability Act) + - SOC2 (System and Organization Controls 2) + - ISO27001 (Information Security Management) + - PCI-DSS (Payment Card Industry Data Security Standard) + - FedRAMP (Federal Risk and Authorization Management Program) + - Custom frameworks + +- **Framework Controls** + - Automated compliance checks + - Control status tracking (compliant/non-compliant/unknown) + - Evidence collection + - Check scheduling + +### Compliance Policies +- **Policy Types** + - Data retention policies + - Data classification policies + - Access control policies + - Audit requirement policies + - Violation action policies + +- **Enforcement Levels** + - Advisory (log only) + - Warning (notify but allow) + - Blocking (prevent action) + +- **Policy Scope** + - Per-user policies + - Team-based policies + - Role-based policies + - Organization-wide policies + +### Violation Management +- **Automatic Detection** + - Policy violation detection + - Severity classification (low/medium/high/critical) + - Real-time alerting + +- **Violation Actions** + - User notifications + - Admin notifications + - Automatic ticket creation + - User suspension (critical violations) + - Session termination + - Escalation emails + +- **Resolution Workflow** + - Violation acknowledgment + - Remediation tracking + - Resolution documentation + +### Compliance Reporting +- **Report Types** + - Summary reports + - Detailed control reports + - Attestation reports + - Violation trend reports + +- **Automated Generation** + - Scheduled monthly/quarterly reports + - On-demand report generation + - PDF export capability + +- **Compliance Dashboard** + - Real-time compliance status + - Violation trends + - Framework compliance rates + - Recent violations + +## Installation + +### Via Plugin Marketplace + +1. Navigate to **Admin → Plugins** +2. Search for "Compliance & Regulatory Framework" +3. Click **Install** +4. Configure frameworks and policies +5. Click **Enable** + +### Manual Installation + +```bash +cp -r streamspace-compliance /path/to/streamspace/plugins/ +systemctl restart streamspace-api +``` + +## Configuration + +### Basic Setup + +```json +{ + "enabled": true, + "defaultFrameworks": ["GDPR", "SOC2"], + "autoEnforcement": true, + "defaultEnforcementLevel": "warning" +} +``` + +### Full Configuration + +```json +{ + "enabled": true, + "defaultFrameworks": ["GDPR", "HIPAA", "SOC2", "ISO27001"], + "autoEnforcement": true, + "defaultEnforcementLevel": "warning", + "dataRetentionDays": { + "sessionData": 90, + "recordings": 365, + "auditLogs": 2555, + "backups": 180 + }, + "violationActions": { + "notifyUser": true, + "notifyAdmin": true, + "createTicket": true, + "suspendOnCritical": false + }, + "reportingSchedule": "0 0 1 * *", + "escalationEmails": [ + "compliance@company.com", + "security@company.com" + ], + "enableAutomaticChecks": true, + "checkInterval": 24 +} +``` + +## Usage + +### Enable a Framework + +```bash +POST /api/plugins/compliance/frameworks +{ + "name": "GDPR", + "displayName": "GDPR Compliance", + "version": "2018", + "enabled": true, + "controls": [ + { + "id": "gdpr-art-5", + "name": "Data Minimization", + "category": "data_protection", + "automated": true, + "checkInterval": 24 + } + ] +} +``` + +### Create a Compliance Policy + +```bash +POST /api/plugins/compliance/policies +{ + "name": "Healthcare Data Protection", + "frameworkId": 2, + "appliesTo": { + "allUsers": true + }, + "enforcementLevel": "blocking", + "dataRetention": { + "enabled": true, + "sessionDataDays": 365, + "recordingDays": 2555, + "auditLogDays": 2555, + "autoPurge": true + }, + "accessControls": { + "requireMFA": true, + "sessionTimeout": 15, + "maxConcurrentSessions": 1 + } +} +``` + +### List Violations + +```bash +GET /api/plugins/compliance/violations?severity=high&status=open +``` + +### Generate Compliance Report + +```bash +POST /api/plugins/compliance/reports +{ + "frameworkId": 1, + "reportType": "detailed", + "startDate": "2025-01-01", + "endDate": "2025-01-31" +} +``` + +### View Compliance Dashboard + +```bash +GET /api/plugins/compliance/dashboard +``` + +**Response:** +```json +{ + "totalPolicies": 15, + "activePolicies": 12, + "totalOpenViolations": 3, + "violationsBySeverity": { + "critical": 0, + "high": 1, + "medium": 2, + "low": 0 + }, + "recentViolations": [...] +} +``` + +## Pre-Built Frameworks + +### GDPR (General Data Protection Regulation) + +**Key Controls:** +- Data minimization +- Purpose limitation +- Storage limitation +- Right to erasure (right to be forgotten) +- Data portability +- Privacy by design + +**Data Retention:** +- User data: 90 days after account deletion +- Audit logs: 7 years +- Consent records: Lifetime + +### HIPAA (Health Insurance Portability and Accountability Act) + +**Key Controls:** +- PHI access controls +- Audit trails (all PHI access) +- Encryption at rest and in transit +- Minimum necessary access +- Business associate agreements + +**Data Retention:** +- Medical records: 6 years +- Audit logs: 6 years +- Security incidents: 6 years + +### SOC2 (Type II) + +**Key Controls:** +- Security (access controls, encryption) +- Availability (uptime monitoring) +- Processing integrity (data accuracy) +- Confidentiality (data protection) +- Privacy (PII handling) + +**Audit Requirements:** +- Continuous monitoring +- Quarterly internal audits +- Annual external audits + +### ISO27001 + +**Key Controls:** +- Information security policies +- Asset management +- Access control +- Cryptography +- Physical security +- Operations security +- Communications security +- Incident management + +**Control Domains:** 14 domains, 114 controls + +## Policy Examples + +### Data Retention Policy + +```json +{ + "name": "Standard Data Retention", + "frameworkId": 1, + "dataRetention": { + "enabled": true, + "sessionDataDays": 90, + "recordingDays": 365, + "auditLogDays": 2555, + "backupDays": 180, + "autoPurge": true, + "purgeSchedule": "0 2 * * *" + } +} +``` + +### MFA Enforcement Policy + +```json +{ + "name": "Require MFA for All Users", + "frameworkId": 3, + "appliesTo": {"allUsers": true}, + "enforcementLevel": "blocking", + "accessControls": { + "requireMFA": true, + "sessionTimeout": 30, + "maxConcurrentSessions": 3 + }, + "violationActions": { + "notifyUser": true, + "blockAction": true + } +} +``` + +### Sensitive Data Access Policy + +```json +{ + "name": "Restricted Data Access Control", + "frameworkId": 2, + "appliesTo": { + "roles": ["admin", "compliance_officer"] + }, + "enforcementLevel": "blocking", + "auditRequirements": { + "logAllAccess": true, + "requireJustification": true, + "alertOnSuspicious": true + }, + "accessControls": { + "requireMFA": true, + "allowedIPRanges": ["10.0.0.0/8"], + "requireApproval": true + } +} +``` + +## Automated Compliance Checks + +The plugin runs automated checks for various controls: + +### Access Control Checks +- Verify MFA is enabled for required users +- Check session timeout configurations +- Validate IP allowlists + +### Data Protection Checks +- Verify encryption at rest +- Check data classification labels +- Validate retention policy enforcement + +### Audit Checks +- Verify audit logging is enabled +- Check log retention periods +- Validate log integrity + +## Violation Types + +- **access_control_violation** - Unauthorized access attempt +- **data_retention_violation** - Data retained beyond policy +- **mfa_violation** - MFA not used when required +- **ip_restriction_violation** - Access from blocked IP +- **session_timeout_violation** - Session exceeded max duration +- **data_export_violation** - Unauthorized data export +- **classification_violation** - Improper data classification + +## Escalation Workflow + +1. **Violation Detected** → Automatic violation record created +2. **Severity Assessment** → Classified as low/medium/high/critical +3. **User Notification** → User notified of violation +4. **Admin Notification** → Admins alerted +5. **Ticket Creation** → Support ticket auto-created +6. **Escalation** → Critical violations escalated via email +7. **Enforcement** → Actions taken based on policy (block/suspend) +8. **Resolution** → Violation acknowledged and remediated +9. **Closure** → Violation closed with documentation + +## Compliance Dashboard + +Access via **Admin → Compliance** to view: + +- Overall compliance status +- Active frameworks and controls +- Open violations by severity +- Compliance trends over time +- Recent policy changes +- Upcoming compliance checks +- Data retention statistics + +## Best Practices + +1. **Start with One Framework** - Enable one framework (e.g., SOC2) first +2. **Test in Advisory Mode** - Use "advisory" enforcement while testing +3. **Regular Reports** - Generate monthly compliance reports +4. **Review Violations Weekly** - Address violations promptly +5. **Update Policies Annually** - Review and update policies yearly +6. **Train Users** - Educate users on compliance requirements +7. **Document Everything** - Maintain evidence for audits +8. **Automate Checks** - Enable automated compliance checks +9. **Monitor Trends** - Watch for violation patterns +10. **External Audits** - Schedule annual third-party audits + +## Troubleshooting + +### Policies not enforcing + +**Problem:** Violations occurring but no enforcement + +**Solution:** +- Check `autoEnforcement` is `true` +- Verify enforcement level is not "advisory" +- Review policy scope matches users +- Check plugin is enabled + +### Automated checks not running + +**Problem:** Scheduled compliance checks not executing + +**Solution:** +- Verify scheduler jobs are enabled +- Check `enableAutomaticChecks` is `true` +- Review job logs for errors +- Validate cron expressions + +### Reports failing to generate + +**Problem:** Compliance report generation fails + +**Solution:** +- Check database connectivity +- Verify framework has controls defined +- Ensure date range is valid +- Check user has admin role + +## Support + +- GitHub: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- Docs: https://docs.streamspace.io/plugins/compliance +- Compliance: https://docs.streamspace.io/compliance + +## License + +MIT License + +## Version History + +- **1.0.0** (2025-01-15) + - Initial release + - GDPR, HIPAA, SOC2, ISO27001 frameworks + - Policy management and enforcement + - Violation tracking and resolution + - Automated compliance checks + - Compliance reporting and dashboard diff --git a/streamspace-compliance/compliance_plugin.go b/streamspace-compliance/compliance_plugin.go new file mode 100644 index 0000000..26ca9fe --- /dev/null +++ b/streamspace-compliance/compliance_plugin.go @@ -0,0 +1,521 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "time" + + "github.com/yourusername/streamspace/api/internal/plugins" +) + +// CompliancePlugin manages regulatory compliance frameworks and policies +type CompliancePlugin struct { + plugins.BasePlugin + config ComplianceConfig + frameworks []ComplianceFramework + activePolicies []CompliancePolicy +} + +// ComplianceConfig holds plugin configuration +type ComplianceConfig struct { + Enabled bool `json:"enabled"` + DefaultFrameworks []string `json:"defaultFrameworks"` + AutoEnforcement bool `json:"autoEnforcement"` + DefaultEnforcementLevel string `json:"defaultEnforcementLevel"` + DataRetentionDays map[string]int `json:"dataRetentionDays"` + ViolationActions ViolationActionConfig `json:"violationActions"` + EscalationEmails []string `json:"escalationEmails"` + EnableAutomaticChecks bool `json:"enableAutomaticChecks"` + CheckInterval int `json:"checkInterval"` +} + +// ComplianceFramework represents a regulatory framework +type ComplianceFramework struct { + ID int64 `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description,omitempty"` + Version string `json:"version,omitempty"` + Enabled bool `json:"enabled"` + Controls []ComplianceControl `json:"controls"` + CreatedAt time.Time `json:"created_at"` +} + +// ComplianceControl represents a specific control +type ComplianceControl struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Category string `json:"category"` + Automated bool `json:"automated"` + CheckInterval int `json:"check_interval_hours,omitempty"` + Status string `json:"status,omitempty"` + LastChecked time.Time `json:"last_checked,omitempty"` +} + +// CompliancePolicy represents a compliance policy +type CompliancePolicy struct { + ID int64 `json:"id"` + Name string `json:"name"` + FrameworkID int64 `json:"framework_id"` + Enabled bool `json:"enabled"` + EnforcementLevel string `json:"enforcement_level"` + DataRetention DataRetentionConfig `json:"data_retention"` + AccessControls AccessControlConfig `json:"access_controls"` + ViolationActions ViolationActionConfig `json:"violation_actions"` + CreatedAt time.Time `json:"created_at"` +} + +// DataRetentionConfig defines retention rules +type DataRetentionConfig struct { + Enabled bool `json:"enabled"` + SessionDataDays int `json:"session_data_days"` + RecordingDays int `json:"recording_days"` + AuditLogDays int `json:"audit_log_days"` + AutoPurge bool `json:"auto_purge"` +} + +// AccessControlConfig defines access controls +type AccessControlConfig struct { + RequireMFA bool `json:"require_mfa"` + AllowedIPRanges []string `json:"allowed_ip_ranges,omitempty"` + SessionTimeout int `json:"session_timeout_minutes"` + MaxConcurrentSessions int `json:"max_concurrent_sessions"` +} + +// ViolationActionConfig defines violation actions +type ViolationActionConfig struct { + NotifyUser bool `json:"notify_user"` + NotifyAdmin bool `json:"notify_admin"` + CreateTicket bool `json:"create_ticket"` + BlockAction bool `json:"block_action"` + SuspendUser bool `json:"suspend_user"` + EscalationEmails []string `json:"escalation_emails,omitempty"` +} + +// ComplianceViolation represents a policy violation +type ComplianceViolation struct { + ID int64 `json:"id"` + PolicyID int64 `json:"policy_id"` + PolicyName string `json:"policy_name,omitempty"` + UserID string `json:"user_id"` + ViolationType string `json:"violation_type"` + Severity string `json:"severity"` + Description string `json:"description"` + Details map[string]interface{} `json:"details,omitempty"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} + +// Initialize sets up the compliance plugin +func (p *CompliancePlugin) Initialize(ctx *plugins.PluginContext) error { + // Load configuration + configBytes, err := json.Marshal(ctx.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := json.Unmarshal(configBytes, &p.config); err != nil { + return fmt.Errorf("failed to unmarshal compliance config: %w", err) + } + + if !p.config.Enabled { + ctx.Logger.Info("Compliance plugin is disabled") + return nil + } + + // Create database tables + if err := p.createDatabaseTables(ctx); err != nil { + return fmt.Errorf("failed to create database tables: %w", err) + } + + // Load default frameworks + if err := p.loadDefaultFrameworks(ctx); err != nil { + return fmt.Errorf("failed to load frameworks: %w", err) + } + + // Load active policies + if err := p.loadActivePolicies(ctx); err != nil { + return fmt.Errorf("failed to load policies: %w", err) + } + + ctx.Logger.Info("Compliance plugin initialized successfully", + "frameworks", len(p.frameworks), + "policies", len(p.activePolicies), + ) + + return nil +} + +// OnLoad is called when the plugin is loaded +func (p *CompliancePlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Compliance & Regulatory Framework plugin loaded") + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *CompliancePlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Compliance plugin unloading") + return nil +} + +// OnSessionCreated checks compliance on session creation +func (p *CompliancePlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + + // Check session-related policies + for _, policy := range p.activePolicies { + if err := p.checkSessionPolicy(ctx, policy, userID, sessionMap); err != nil { + ctx.Logger.Warn("Policy check failed", "policy", policy.Name, "error", err) + } + } + + return nil +} + +// OnUserLogin checks compliance on user login +func (p *CompliancePlugin) OnUserLogin(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", userMap["id"]) + + // Check MFA requirements + for _, policy := range p.activePolicies { + if policy.AccessControls.RequireMFA { + if err := p.checkMFACompliance(ctx, policy, userID); err != nil { + p.recordViolation(ctx, policy, userID, "mfa_violation", "critical", err.Error()) + } + } + } + + return nil +} + +// OnDataExport checks data export compliance +func (p *CompliancePlugin) OnDataExport(ctx *plugins.PluginContext, exportData interface{}) error { + if !p.config.Enabled { + return nil + } + + exportMap, ok := exportData.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", exportMap["user_id"]) + + // Check data export policies + for _, policy := range p.activePolicies { + if policy.EnforcementLevel == "blocking" { + // Validate export is compliant + ctx.Logger.Info("Checking data export compliance", "user", userID, "policy", policy.Name) + } + } + + return nil +} + +// RunScheduledJob handles scheduled compliance tasks +func (p *CompliancePlugin) RunScheduledJob(ctx *plugins.PluginContext, jobName string) error { + switch jobName { + case "run-compliance-checks": + return p.runComplianceChecks(ctx) + case "generate-monthly-report": + return p.generateMonthlyReport(ctx) + case "check-data-retention": + return p.checkDataRetention(ctx) + } + return nil +} + +// createDatabaseTables creates necessary database tables +func (p *CompliancePlugin) createDatabaseTables(ctx *plugins.PluginContext) error { + tables := []string{ + `CREATE TABLE IF NOT EXISTS compliance_frameworks ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + display_name VARCHAR(200), + description TEXT, + version VARCHAR(50), + enabled BOOLEAN DEFAULT true, + controls JSONB, + created_by VARCHAR(255), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS compliance_policies ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + framework_id INTEGER REFERENCES compliance_frameworks(id), + enabled BOOLEAN DEFAULT true, + enforcement_level VARCHAR(50), + data_retention JSONB, + access_controls JSONB, + violation_actions JSONB, + created_by VARCHAR(255), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS compliance_violations ( + id SERIAL PRIMARY KEY, + policy_id INTEGER REFERENCES compliance_policies(id), + user_id VARCHAR(255) NOT NULL, + violation_type VARCHAR(100), + severity VARCHAR(50), + description TEXT, + details JSONB, + status VARCHAR(50), + resolution TEXT, + resolved_by VARCHAR(255), + resolved_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() + )`, + `CREATE TABLE IF NOT EXISTS compliance_reports ( + id SERIAL PRIMARY KEY, + framework_id INTEGER REFERENCES compliance_frameworks(id), + report_type VARCHAR(50), + start_date DATE, + end_date DATE, + overall_status VARCHAR(50), + controls_summary JSONB, + violations JSONB, + recommendations TEXT[], + generated_by VARCHAR(255), + generated_at TIMESTAMP DEFAULT NOW() + )`, + } + + for _, table := range tables { + if err := ctx.Database.Exec(table); err != nil { + return fmt.Errorf("failed to create table: %w", err) + } + } + + return nil +} + +// loadDefaultFrameworks loads pre-configured frameworks +func (p *CompliancePlugin) loadDefaultFrameworks(ctx *plugins.PluginContext) error { + // Query existing frameworks + rows, err := ctx.Database.Query(`SELECT id, name, display_name, version, enabled, controls, created_at FROM compliance_frameworks`) + if err != nil { + return err + } + defer rows.Close() + + p.frameworks = []ComplianceFramework{} + for rows.Next() { + var f ComplianceFramework + var controlsJSON []byte + if err := rows.Scan(&f.ID, &f.Name, &f.DisplayName, &f.Version, &f.Enabled, &controlsJSON, &f.CreatedAt); err != nil { + continue + } + json.Unmarshal(controlsJSON, &f.Controls) + p.frameworks = append(p.frameworks, f) + } + + ctx.Logger.Info("Loaded compliance frameworks", "count", len(p.frameworks)) + return nil +} + +// loadActivePolicies loads active compliance policies +func (p *CompliancePlugin) loadActivePolicies(ctx *plugins.PluginContext) error { + rows, err := ctx.Database.Query(` + SELECT id, name, framework_id, enabled, enforcement_level, + data_retention, access_controls, violation_actions, created_at + FROM compliance_policies + WHERE enabled = true + `) + if err != nil { + return err + } + defer rows.Close() + + p.activePolicies = []CompliancePolicy{} + for rows.Next() { + var policy CompliancePolicy + var dataRetentionJSON, accessControlsJSON, violationActionsJSON []byte + + if err := rows.Scan(&policy.ID, &policy.Name, &policy.FrameworkID, &policy.Enabled, + &policy.EnforcementLevel, &dataRetentionJSON, &accessControlsJSON, + &violationActionsJSON, &policy.CreatedAt); err != nil { + continue + } + + json.Unmarshal(dataRetentionJSON, &policy.DataRetention) + json.Unmarshal(accessControlsJSON, &policy.AccessControls) + json.Unmarshal(violationActionsJSON, &policy.ViolationActions) + + p.activePolicies = append(p.activePolicies, policy) + } + + ctx.Logger.Info("Loaded active policies", "count", len(p.activePolicies)) + return nil +} + +// checkSessionPolicy validates session against policy +func (p *CompliancePlugin) checkSessionPolicy(ctx *plugins.PluginContext, policy CompliancePolicy, userID string, session map[string]interface{}) error { + // Check session timeout + if policy.AccessControls.SessionTimeout > 0 { + // Would validate session duration + } + + // Check concurrent sessions + if policy.AccessControls.MaxConcurrentSessions > 0 { + count, _ := ctx.Database.QueryInt("SELECT COUNT(*) FROM sessions WHERE user_id = $1 AND status = 'running'", userID) + if count > policy.AccessControls.MaxConcurrentSessions { + return p.recordViolation(ctx, policy, userID, "concurrent_session_violation", "medium", + fmt.Sprintf("User has %d concurrent sessions (limit: %d)", count, policy.AccessControls.MaxConcurrentSessions)) + } + } + + return nil +} + +// checkMFACompliance validates MFA requirement +func (p *CompliancePlugin) checkMFACompliance(ctx *plugins.PluginContext, policy CompliancePolicy, userID string) error { + // Query user MFA status + var mfaEnabled bool + err := ctx.Database.QueryRow("SELECT mfa_enabled FROM users WHERE user_id = $1", userID).Scan(&mfaEnabled) + if err != nil { + return err + } + + if !mfaEnabled && policy.AccessControls.RequireMFA { + return fmt.Errorf("MFA required by policy %s but not enabled for user", policy.Name) + } + + return nil +} + +// recordViolation creates a compliance violation record +func (p *CompliancePlugin) recordViolation(ctx *plugins.PluginContext, policy CompliancePolicy, userID, violationType, severity, description string) error { + var id int64 + err := ctx.Database.QueryRow(` + INSERT INTO compliance_violations (policy_id, user_id, violation_type, severity, description, status, created_at) + VALUES ($1, $2, $3, $4, $5, 'open', NOW()) + RETURNING id + `, policy.ID, userID, violationType, severity, description).Scan(&id) + + if err != nil { + return err + } + + ctx.Logger.Warn("Compliance violation recorded", + "id", id, + "policy", policy.Name, + "user", userID, + "type", violationType, + "severity", severity, + ) + + // Execute violation actions + p.executeViolationActions(ctx, policy, userID, description) + + return nil +} + +// executeViolationActions takes action on violations +func (p *CompliancePlugin) executeViolationActions(ctx *plugins.PluginContext, policy CompliancePolicy, userID, description string) { + if policy.ViolationActions.NotifyUser { + ctx.Logger.Info("Sending violation notification to user", "user", userID) + } + + if policy.ViolationActions.NotifyAdmin { + ctx.Logger.Info("Sending violation notification to admins", "policy", policy.Name) + } + + if policy.ViolationActions.CreateTicket { + ctx.Logger.Info("Creating support ticket for violation", "user", userID) + } + + if policy.ViolationActions.SuspendUser { + ctx.Logger.Warn("Suspending user due to violation", "user", userID, "policy", policy.Name) + } +} + +// runComplianceChecks runs automated compliance checks +func (p *CompliancePlugin) runComplianceChecks(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Running automated compliance checks", "frameworks", len(p.frameworks)) + + for _, framework := range p.frameworks { + if !framework.Enabled { + continue + } + + for _, control := range framework.Controls { + if control.Automated { + ctx.Logger.Debug("Checking control", "framework", framework.Name, "control", control.Name) + // Automated control checking logic would go here + } + } + } + + return nil +} + +// generateMonthlyReport creates a monthly compliance report +func (p *CompliancePlugin) generateMonthlyReport(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Generating monthly compliance report") + + startDate := time.Now().AddDate(0, -1, 0) + endDate := time.Now() + + for _, framework := range p.frameworks { + if !framework.Enabled { + continue + } + + ctx.Logger.Info("Generating report for framework", "name", framework.Name) + // Report generation logic would go here + } + + return nil +} + +// checkDataRetention enforces data retention policies +func (p *CompliancePlugin) checkDataRetention(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Checking data retention policies") + + for _, policy := range p.activePolicies { + if !policy.DataRetention.Enabled || !policy.DataRetention.AutoPurge { + continue + } + + // Purge old session data + if policy.DataRetention.SessionDataDays > 0 { + cutoff := time.Now().AddDate(0, 0, -policy.DataRetention.SessionDataDays) + ctx.Logger.Info("Purging old session data", "cutoff", cutoff, "policy", policy.Name) + } + + // Purge old recordings + if policy.DataRetention.RecordingDays > 0 { + cutoff := time.Now().AddDate(0, 0, -policy.DataRetention.RecordingDays) + ctx.Logger.Info("Purging old recordings", "cutoff", cutoff, "policy", policy.Name) + } + } + + return nil +} + +// Export the plugin +func init() { + plugins.Register("streamspace-compliance", &CompliancePlugin{}) +} diff --git a/streamspace-compliance/manifest.json b/streamspace-compliance/manifest.json new file mode 100644 index 0000000..60dc77c --- /dev/null +++ b/streamspace-compliance/manifest.json @@ -0,0 +1,198 @@ +{ + "name": "streamspace-compliance", + "version": "1.0.0", + "displayName": "Compliance & Regulatory Framework", + "description": "Comprehensive compliance management for GDPR, HIPAA, SOC2, ISO27001, and custom frameworks", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Security", + "tags": ["compliance", "gdpr", "hipaa", "soc2", "iso27001", "regulatory", "governance"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "compliance_plugin.go" + }, + + "permissions": ["database", "admin_ui", "network"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Compliance Management", + "description": "Enable compliance framework and policy management", + "default": true + }, + "defaultFrameworks": { + "type": "array", + "title": "Default Frameworks to Enable", + "description": "Compliance frameworks to enable by default", + "items": { + "type": "string", + "enum": ["GDPR", "HIPAA", "SOC2", "ISO27001", "PCI-DSS", "FedRAMP"] + }, + "default": ["GDPR", "SOC2"] + }, + "autoEnforcement": { + "type": "boolean", + "title": "Auto-Enforce Policies", + "description": "Automatically enforce compliance policies", + "default": true + }, + "defaultEnforcementLevel": { + "type": "string", + "title": "Default Enforcement Level", + "description": "Default policy enforcement level", + "enum": ["advisory", "warning", "blocking"], + "default": "warning" + }, + "dataRetentionDays": { + "type": "object", + "title": "Default Data Retention (Days)", + "description": "Default retention periods for different data types", + "properties": { + "sessionData": {"type": "integer", "default": 90}, + "recordings": {"type": "integer", "default": 365}, + "auditLogs": {"type": "integer", "default": 2555}, + "backups": {"type": "integer", "default": 180} + } + }, + "violationActions": { + "type": "object", + "title": "Violation Actions", + "description": "Actions to take when violations occur", + "properties": { + "notifyUser": {"type": "boolean", "default": true}, + "notifyAdmin": {"type": "boolean", "default": true}, + "createTicket": {"type": "boolean", "default": true}, + "suspendOnCritical": {"type": "boolean", "default": false} + } + }, + "reportingSchedule": { + "type": "string", + "title": "Automated Reporting Schedule", + "description": "Cron expression for automated compliance reports", + "default": "0 0 1 * *" + }, + "escalationEmails": { + "type": "array", + "title": "Escalation Email Addresses", + "description": "Email addresses for critical compliance escalations", + "items": {"type": "string", "format": "email"}, + "default": [] + }, + "enableAutomaticChecks": { + "type": "boolean", + "title": "Enable Automatic Compliance Checks", + "description": "Run automated compliance control checks", + "default": true + }, + "checkInterval": { + "type": "integer", + "title": "Check Interval (hours)", + "description": "How often to run automated compliance checks", + "default": 24, + "minimum": 1, + "maximum": 168 + } + }, + "required": [] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "user.created": "OnUserCreated", + "user.login": "OnUserLogin", + "user.data_export": "OnDataExport", + "session.data_accessed": "OnDataAccessed" + }, + "database": { + "tables": [ + "compliance_frameworks", + "compliance_policies", + "compliance_violations", + "compliance_reports", + "compliance_escalations", + "compliance_control_checks" + ] + }, + "api": { + "endpoints": [ + "/compliance/frameworks", + "/compliance/frameworks/:id", + "/compliance/policies", + "/compliance/policies/:id", + "/compliance/violations", + "/compliance/violations/:id/resolve", + "/compliance/reports", + "/compliance/reports/:id", + "/compliance/dashboard" + ] + }, + "ui": { + "adminPages": [ + { + "id": "compliance-dashboard", + "title": "Compliance Dashboard", + "route": "/admin/compliance", + "component": "ComplianceDashboard", + "icon": "verified_user" + }, + { + "id": "compliance-frameworks", + "title": "Compliance Frameworks", + "route": "/admin/compliance/frameworks", + "component": "ComplianceFrameworks", + "icon": "gavel" + }, + { + "id": "compliance-policies", + "title": "Compliance Policies", + "route": "/admin/compliance/policies", + "component": "CompliancePolicies", + "icon": "policy" + }, + { + "id": "compliance-violations", + "title": "Policy Violations", + "route": "/admin/compliance/violations", + "component": "ComplianceViolations", + "icon": "report_problem" + }, + { + "id": "compliance-reports", + "title": "Compliance Reports", + "route": "/admin/compliance/reports", + "component": "ComplianceReports", + "icon": "assessment" + } + ] + }, + "scheduler": { + "jobs": [ + { + "name": "run-compliance-checks", + "schedule": "0 */6 * * *", + "description": "Run automated compliance control checks" + }, + { + "name": "generate-monthly-report", + "schedule": "0 0 1 * *", + "description": "Generate monthly compliance report" + }, + { + "name": "check-data-retention", + "schedule": "0 2 * * *", + "description": "Check and enforce data retention policies" + } + ] + } +} diff --git a/streamspace-datadog/README.md b/streamspace-datadog/README.md new file mode 100644 index 0000000..ab3af94 --- /dev/null +++ b/streamspace-datadog/README.md @@ -0,0 +1,339 @@ +# StreamSpace Datadog Plugin + +Comprehensive monitoring integration with Datadog for metrics, traces, and logs. + +## Features + +### Metrics Collection +- **Session Metrics** - Track session lifecycle (created, terminated, active count, duration) +- **Resource Metrics** - Monitor CPU, memory, and storage usage per session +- **User Metrics** - Track user activity (created, login, logout counts) +- **Custom Metrics** - Send any StreamSpace metric to Datadog + +### Events +- Session lifecycle events (created, terminated) +- Plugin lifecycle events (loaded, unloaded) +- User activity events + +### Automatic Tracking +- Active session count +- Session duration tracking +- Resource utilization over time +- User engagement metrics + +## Installation + +### Via Plugin Marketplace (Recommended) + +1. Navigate to **Admin → Plugins** +2. Search for "Datadog Monitoring" +3. Click **Install** +4. Configure settings (see Configuration section) +5. Click **Enable** + +### Manual Installation + +```bash +# Copy plugin files to plugins directory +cp -r streamspace-datadog /path/to/streamspace/plugins/ + +# Restart StreamSpace API +systemctl restart streamspace-api +``` + +## Configuration + +### Basic Setup + +```json +{ + "enabled": true, + "apiKey": "your-datadog-api-key", + "site": "datadoghq.com", + "enableMetrics": true, + "globalTags": ["env:production", "service:streamspace"] +} +``` + +### Full Configuration + +```json +{ + "enabled": true, + "apiKey": "your-datadog-api-key", + "appKey": "your-datadog-app-key", + "site": "datadoghq.com", + "enableMetrics": true, + "enableTraces": true, + "enableLogs": false, + "globalTags": [ + "env:production", + "service:streamspace", + "region:us-east-1", + "team:platform" + ], + "metricsInterval": 60, + "trackSessionMetrics": true, + "trackResourceMetrics": true, + "trackUserMetrics": true +} +``` + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | boolean | `true` | Enable Datadog integration | +| `apiKey` | string | *required* | Your Datadog API key | +| `appKey` | string | optional | Datadog application key (for advanced features) | +| `site` | string | `datadoghq.com` | Datadog site (US, EU, etc.) | +| `enableMetrics` | boolean | `true` | Send metrics to Datadog | +| `enableTraces` | boolean | `true` | Send APM traces to Datadog | +| `enableLogs` | boolean | `false` | Send logs to Datadog | +| `globalTags` | array | `["env:production"]` | Tags applied to all metrics | +| `metricsInterval` | integer | `60` | How often to flush metrics (seconds) | +| `trackSessionMetrics` | boolean | `true` | Track session lifecycle metrics | +| `trackResourceMetrics` | boolean | `true` | Track CPU/memory/storage metrics | +| `trackUserMetrics` | boolean | `true` | Track user activity metrics | + +### Datadog Sites + +Choose the correct site based on your Datadog account region: + +- **US1** (default): `datadoghq.com` +- **US3**: `us3.datadoghq.com` +- **US5**: `us5.datadoghq.com` +- **EU1**: `datadoghq.eu` +- **AP1**: `ap1.datadoghq.com` + +## Metrics Reference + +### Session Metrics + +| Metric | Type | Description | +|--------|------|-------------| +| `streamspace.session.created` | count | Number of sessions created | +| `streamspace.session.terminated` | count | Number of sessions terminated | +| `streamspace.session.active` | gauge | Current number of active sessions | +| `streamspace.session.duration` | gauge | Session duration in seconds | + +**Tags**: `user:`, `template:` + +### Resource Metrics + +| Metric | Type | Description | +|--------|------|-------------| +| `streamspace.session.cpu_usage` | gauge | CPU usage percentage (0-100) | +| `streamspace.session.memory_usage` | gauge | Memory usage in bytes | +| `streamspace.session.storage_usage` | gauge | Storage usage in bytes | + +**Tags**: `session:`, `user:`, `template:` + +### User Metrics + +| Metric | Type | Description | +|--------|------|-------------| +| `streamspace.user.created` | count | Number of users created | +| `streamspace.user.login` | count | Number of user logins | +| `streamspace.user.logout` | count | Number of user logouts | +| `streamspace.users.total` | count | Total user count | + +**Tags**: `user:` + +## Usage + +### View Metrics in Datadog + +1. Log into your Datadog account +2. Navigate to **Metrics → Explorer** +3. Search for metrics starting with `streamspace.*` +4. Create custom dashboards and graphs + +### Create Dashboards + +#### Session Overview Dashboard + +``` +Widget 1: Active Sessions (Timeseries) +- Metric: streamspace.session.active +- Visualization: Line graph + +Widget 2: Session Duration (Heatmap) +- Metric: streamspace.session.duration +- Visualization: Heatmap + +Widget 3: Sessions Created (Top List) +- Metric: streamspace.session.created +- Group by: template +- Visualization: Top list + +Widget 4: Resource Usage (Stacked Area) +- Metrics: streamspace.session.cpu_usage, streamspace.session.memory_usage +- Visualization: Stacked area +``` + +#### User Activity Dashboard + +``` +Widget 1: User Logins (Timeseries) +- Metric: streamspace.user.login +- Visualization: Bars + +Widget 2: Active Users (Query Value) +- Metric: streamspace.users.total +- Visualization: Query value + +Widget 3: User Sessions (Table) +- Metrics: streamspace.session.active +- Group by: user +- Visualization: Table +``` + +### Create Monitors + +#### High Session Count Alert + +``` +Metric: streamspace.session.active +Condition: Alert when avg(last_5m) > 100 +Message: StreamSpace has {{value}} active sessions (threshold: 100) +Tags: @slack-platform-alerts +``` + +#### Long Session Duration Alert + +``` +Metric: streamspace.session.duration +Condition: Alert when max(last_15m) > 28800 # 8 hours +Message: Session {{session.name}} has been running for {{value}} seconds +Tags: @pagerduty-platform +``` + +#### High Resource Usage Alert + +``` +Metric: streamspace.session.cpu_usage +Condition: Alert when avg(last_10m) > 90 +Message: Session {{session.name}} CPU usage is {{value}}% +Tags: @ops-team +``` + +## Events Reference + +### Session Events + +- **Session Created**: Triggered when a new session is created +- **Session Terminated**: Triggered when a session is terminated + +### Plugin Events + +- **Plugin Loaded**: Triggered when the Datadog plugin is loaded +- **Plugin Unloaded**: Triggered when the Datadog plugin is unloaded + +## Troubleshooting + +### Metrics not appearing in Datadog + +**Problem**: Metrics not showing up in Datadog UI + +**Solution**: +- Verify API key is correct +- Check Datadog site setting matches your account region +- Review plugin logs: `tail -f /var/log/streamspace/plugins/datadog.log` +- Verify `enableMetrics` is `true` +- Check metrics interval hasn't been set too high + +### Authentication errors + +**Problem**: 403 Forbidden errors in logs + +**Solution**: +- Verify your Datadog API key is valid +- Check API key permissions in Datadog +- Ensure API key hasn't expired +- Try regenerating API key in Datadog settings + +### Metrics delayed + +**Problem**: Metrics appear in Datadog with significant delay + +**Solution**: +- Lower `metricsInterval` (minimum: 10 seconds) +- Check network connectivity to Datadog +- Verify no rate limiting is occurring +- Check for high metric cardinality + +### High cardinality warnings + +**Problem**: Datadog warns about high metric cardinality + +**Solution**: +- Reduce number of tags in `globalTags` +- Disable detailed resource tracking if not needed +- Use tag aggregation in Datadog +- Consider using distributions instead of gauges + +## Best Practices + +1. **Tag Wisely** - Use meaningful tags but avoid high cardinality (user IDs, session IDs in global tags) +2. **Set Appropriate Interval** - Balance between freshness and API usage (60s recommended) +3. **Create Dashboards** - Build dashboards before you need them during incidents +4. **Set Up Monitors** - Proactive alerting prevents issues from escalating +5. **Use Events** - Correlate metrics with events for better context +6. **Review Costs** - Monitor Datadog usage to control costs (custom metrics pricing) + +## API Reference + +### Getting Datadog Configuration + +```bash +GET /api/plugins/datadog/config +Authorization: Bearer +``` + +**Response**: +```json +{ + "enabled": true, + "site": "datadoghq.com", + "enableMetrics": true, + "enableTraces": true, + "metricsInterval": 60 +} +``` + +### Sending Custom Metrics + +While the plugin handles most metrics automatically, you can send custom metrics via the plugin API: + +```bash +POST /api/plugins/datadog/metrics +Authorization: Bearer +Content-Type: application/json + +{ + "metric": "streamspace.custom.metric", + "value": 42, + "type": "gauge", + "tags": ["custom:tag"] +} +``` + +## Support + +For issues or questions: +- GitHub Issues: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- Documentation: https://docs.streamspace.io/plugins/datadog +- Datadog Documentation: https://docs.datadoghq.com/ + +## License + +MIT License - see LICENSE file for details + +## Version History + +- **1.0.0** (2025-01-15) + - Initial release + - Session, resource, and user metrics + - Event tracking + - Scheduled metric flushing diff --git a/streamspace-datadog/datadog_plugin.go b/streamspace-datadog/datadog_plugin.go new file mode 100644 index 0000000..baf37fb --- /dev/null +++ b/streamspace-datadog/datadog_plugin.go @@ -0,0 +1,418 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/yourusername/streamspace/api/internal/plugins" +) + +// DatadogPlugin sends metrics, traces, and logs to Datadog +type DatadogPlugin struct { + plugins.BasePlugin + config DatadogConfig + httpClient *http.Client + metricsBuffer []DatadogMetric + metricsMutex sync.Mutex + sessionStart map[string]time.Time + sessionMutex sync.Mutex +} + +// DatadogConfig holds Datadog configuration +type DatadogConfig struct { + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey"` + AppKey string `json:"appKey"` + Site string `json:"site"` + EnableMetrics bool `json:"enableMetrics"` + EnableTraces bool `json:"enableTraces"` + EnableLogs bool `json:"enableLogs"` + GlobalTags []string `json:"globalTags"` + MetricsInterval int `json:"metricsInterval"` + TrackSessionMetrics bool `json:"trackSessionMetrics"` + TrackResourceMetrics bool `json:"trackResourceMetrics"` + TrackUserMetrics bool `json:"trackUserMetrics"` +} + +// DatadogMetric represents a Datadog metric +type DatadogMetric struct { + Metric string `json:"metric"` + Points [][]int64 `json:"points"` + Type string `json:"type"` + Tags []string `json:"tags,omitempty"` +} + +// DatadogMetricSeries is the payload sent to Datadog +type DatadogMetricSeries struct { + Series []DatadogMetric `json:"series"` +} + +// DatadogEvent represents a Datadog event +type DatadogEvent struct { + Title string `json:"title"` + Text string `json:"text"` + Priority string `json:"priority,omitempty"` + Tags []string `json:"tags,omitempty"` + AlertType string `json:"alert_type,omitempty"` + AggregationKey string `json:"aggregation_key,omitempty"` +} + +// Initialize sets up the plugin +func (p *DatadogPlugin) Initialize(ctx *plugins.PluginContext) error { + // Load configuration + configBytes, err := json.Marshal(ctx.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := json.Unmarshal(configBytes, &p.config); err != nil { + return fmt.Errorf("failed to unmarshal Datadog config: %w", err) + } + + if !p.config.Enabled { + ctx.Logger.Info("Datadog integration is disabled") + return nil + } + + if p.config.APIKey == "" { + return fmt.Errorf("Datadog API key is required") + } + + if p.config.Site == "" { + p.config.Site = "datadoghq.com" + } + + // Initialize HTTP client + p.httpClient = &http.Client{ + Timeout: 10 * time.Second, + } + + // Initialize session tracking + p.sessionStart = make(map[string]time.Time) + p.metricsBuffer = []DatadogMetric{} + + ctx.Logger.Info("Datadog plugin initialized successfully", + "site", p.config.Site, + "metrics_enabled", p.config.EnableMetrics, + "traces_enabled", p.config.EnableTraces, + "logs_enabled", p.config.EnableLogs, + ) + + return nil +} + +// OnLoad is called when the plugin is loaded +func (p *DatadogPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Datadog monitoring plugin loaded") + return p.sendEvent(ctx, "StreamSpace Datadog Plugin Loaded", "Datadog monitoring integration is now active", "info", "normal") +} + +// OnUnload is called when the plugin is unloaded +func (p *DatadogPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Datadog monitoring plugin unloading") + + // Flush any remaining metrics + if err := p.flushMetrics(ctx); err != nil { + ctx.Logger.Error("Failed to flush metrics on unload", "error", err) + } + + return p.sendEvent(ctx, "StreamSpace Datadog Plugin Unloaded", "Datadog monitoring integration has been disabled", "info", "normal") +} + +// OnSessionCreated tracks session creation +func (p *DatadogPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackSessionMetrics { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Track session start time + p.sessionMutex.Lock() + p.sessionStart[sessionID] = time.Now() + p.sessionMutex.Unlock() + + // Send session created metric + tags := append(p.config.GlobalTags, + fmt.Sprintf("user:%s", userID), + fmt.Sprintf("template:%s", templateName), + ) + + p.addMetric("streamspace.session.created", 1, "count", tags) + p.addMetric("streamspace.session.active", 1, "gauge", tags) + + return p.sendEvent(ctx, + "Session Created", + fmt.Sprintf("User %s started session %s with template %s", userID, sessionID, templateName), + "info", + "low", + ) +} + +// OnSessionTerminated tracks session termination +func (p *DatadogPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackSessionMetrics { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Calculate session duration + p.sessionMutex.Lock() + startTime, exists := p.sessionStart[sessionID] + if exists { + duration := time.Since(startTime).Seconds() + tags := append(p.config.GlobalTags, + fmt.Sprintf("user:%s", userID), + fmt.Sprintf("template:%s", templateName), + ) + p.addMetric("streamspace.session.duration", int64(duration), "gauge", tags) + delete(p.sessionStart, sessionID) + } + p.sessionMutex.Unlock() + + // Send session terminated metric + tags := append(p.config.GlobalTags, + fmt.Sprintf("user:%s", userID), + fmt.Sprintf("template:%s", templateName), + ) + + p.addMetric("streamspace.session.terminated", 1, "count", tags) + p.addMetric("streamspace.session.active", -1, "gauge", tags) + + return nil +} + +// OnSessionHeartbeat tracks session resource usage +func (p *DatadogPlugin) OnSessionHeartbeat(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackResourceMetrics { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return nil + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + tags := append(p.config.GlobalTags, + fmt.Sprintf("session:%s", sessionID), + fmt.Sprintf("user:%s", userID), + fmt.Sprintf("template:%s", templateName), + ) + + // Track resource usage if available + if cpuUsage, ok := sessionMap["cpu_usage"].(float64); ok { + p.addMetric("streamspace.session.cpu_usage", int64(cpuUsage*100), "gauge", tags) + } + + if memoryUsage, ok := sessionMap["memory_usage"].(float64); ok { + p.addMetric("streamspace.session.memory_usage", int64(memoryUsage), "gauge", tags) + } + + if storageUsage, ok := sessionMap["storage_usage"].(float64); ok { + p.addMetric("streamspace.session.storage_usage", int64(storageUsage), "gauge", tags) + } + + return nil +} + +// OnUserCreated tracks user creation +func (p *DatadogPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUserMetrics { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user format") + } + + userID := fmt.Sprintf("%v", userMap["id"]) + tags := append(p.config.GlobalTags, fmt.Sprintf("user:%s", userID)) + + p.addMetric("streamspace.user.created", 1, "count", tags) + p.addMetric("streamspace.users.total", 1, "count", p.config.GlobalTags) + + return nil +} + +// OnUserLogin tracks user login +func (p *DatadogPlugin) OnUserLogin(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUserMetrics { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", userMap["id"]) + tags := append(p.config.GlobalTags, fmt.Sprintf("user:%s", userID)) + + p.addMetric("streamspace.user.login", 1, "count", tags) + + return nil +} + +// OnUserLogout tracks user logout +func (p *DatadogPlugin) OnUserLogout(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUserMetrics { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", userMap["id"]) + tags := append(p.config.GlobalTags, fmt.Sprintf("user:%s", userID)) + + p.addMetric("streamspace.user.logout", 1, "count", tags) + + return nil +} + +// RunScheduledJob handles the scheduled metrics flush +func (p *DatadogPlugin) RunScheduledJob(ctx *plugins.PluginContext, jobName string) error { + if jobName == "send-metrics" { + return p.flushMetrics(ctx) + } + return nil +} + +// addMetric adds a metric to the buffer +func (p *DatadogPlugin) addMetric(name string, value int64, metricType string, tags []string) { + p.metricsMutex.Lock() + defer p.metricsMutex.Unlock() + + now := time.Now().Unix() + metric := DatadogMetric{ + Metric: name, + Points: [][]int64{{now, value}}, + Type: metricType, + Tags: tags, + } + + p.metricsBuffer = append(p.metricsBuffer, metric) +} + +// flushMetrics sends buffered metrics to Datadog +func (p *DatadogPlugin) flushMetrics(ctx *plugins.PluginContext) error { + if !p.config.Enabled || !p.config.EnableMetrics { + return nil + } + + p.metricsMutex.Lock() + if len(p.metricsBuffer) == 0 { + p.metricsMutex.Unlock() + return nil + } + + // Get metrics and clear buffer + metrics := make([]DatadogMetric, len(p.metricsBuffer)) + copy(metrics, p.metricsBuffer) + p.metricsBuffer = []DatadogMetric{} + p.metricsMutex.Unlock() + + // Send to Datadog + payload := DatadogMetricSeries{Series: metrics} + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal metrics: %w", err) + } + + url := fmt.Sprintf("https://api.%s/api/v1/series", p.config.Site) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("DD-API-KEY", p.config.APIKey) + + resp, err := p.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send metrics: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusAccepted { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Datadog API returned status %d: %s", resp.StatusCode, string(body)) + } + + ctx.Logger.Info("Sent metrics to Datadog", "count", len(metrics)) + return nil +} + +// sendEvent sends an event to Datadog +func (p *DatadogPlugin) sendEvent(ctx *plugins.PluginContext, title, text, alertType, priority string) error { + if !p.config.Enabled { + return nil + } + + event := DatadogEvent{ + Title: title, + Text: text, + Priority: priority, + Tags: p.config.GlobalTags, + AlertType: alertType, + } + + payloadBytes, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("failed to marshal event: %w", err) + } + + url := fmt.Sprintf("https://api.%s/api/v1/events", p.config.Site) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("DD-API-KEY", p.config.APIKey) + + resp, err := p.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send event: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusAccepted { + body, _ := io.ReadAll(resp.Body) + ctx.Logger.Warn("Failed to send event to Datadog", "status", resp.StatusCode, "body", string(body)) + } + + return nil +} + +// Export the plugin +func init() { + plugins.Register("streamspace-datadog", &DatadogPlugin{}) +} diff --git a/streamspace-datadog/manifest.json b/streamspace-datadog/manifest.json new file mode 100644 index 0000000..ea6c5be --- /dev/null +++ b/streamspace-datadog/manifest.json @@ -0,0 +1,126 @@ +{ + "name": "streamspace-datadog", + "version": "1.0.0", + "displayName": "Datadog Monitoring", + "description": "Send metrics, traces, and logs to Datadog for comprehensive observability", + "author": "StreamSpace Team", + "license": "MIT", + "type": "integration", + "category": "Monitoring", + "tags": ["monitoring", "datadog", "metrics", "apm", "observability"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "datadog_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Datadog Integration", + "description": "Enable sending metrics and traces to Datadog", + "default": true + }, + "apiKey": { + "type": "string", + "title": "Datadog API Key", + "description": "Your Datadog API key", + "format": "password" + }, + "appKey": { + "type": "string", + "title": "Datadog Application Key", + "description": "Your Datadog application key (optional, for advanced features)", + "format": "password" + }, + "site": { + "type": "string", + "title": "Datadog Site", + "description": "Your Datadog site (e.g., datadoghq.com, datadoghq.eu)", + "enum": ["datadoghq.com", "us3.datadoghq.com", "us5.datadoghq.com", "datadoghq.eu", "ap1.datadoghq.com"], + "default": "datadoghq.com" + }, + "enableMetrics": { + "type": "boolean", + "title": "Enable Metrics", + "description": "Send custom metrics to Datadog", + "default": true + }, + "enableTraces": { + "type": "boolean", + "title": "Enable APM Traces", + "description": "Send APM traces to Datadog", + "default": true + }, + "enableLogs": { + "type": "boolean", + "title": "Enable Logs", + "description": "Send logs to Datadog", + "default": false + }, + "globalTags": { + "type": "array", + "title": "Global Tags", + "description": "Tags to apply to all metrics and traces", + "items": { + "type": "string" + }, + "default": ["env:production", "service:streamspace"] + }, + "metricsInterval": { + "type": "integer", + "title": "Metrics Interval (seconds)", + "description": "How often to send metrics to Datadog", + "default": 60, + "minimum": 10, + "maximum": 300 + }, + "trackSessionMetrics": { + "type": "boolean", + "title": "Track Session Metrics", + "description": "Track session lifecycle metrics (created, terminated, duration)", + "default": true + }, + "trackResourceMetrics": { + "type": "boolean", + "title": "Track Resource Metrics", + "description": "Track CPU, memory, and storage usage metrics", + "default": true + }, + "trackUserMetrics": { + "type": "boolean", + "title": "Track User Metrics", + "description": "Track user activity and counts", + "default": true + } + }, + "required": ["apiKey", "site"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "session.heartbeat": "OnSessionHeartbeat", + "user.created": "OnUserCreated", + "user.login": "OnUserLogin", + "user.logout": "OnUserLogout" + }, + "scheduler": { + "jobs": [ + { + "name": "send-metrics", + "schedule": "*/1 * * * *", + "description": "Send metrics to Datadog" + } + ] + } +} diff --git a/streamspace-discord/README.md b/streamspace-discord/README.md new file mode 100644 index 0000000..7a452dc --- /dev/null +++ b/streamspace-discord/README.md @@ -0,0 +1,217 @@ +# StreamSpace Discord Integration Plugin + +Send real-time notifications about StreamSpace events to your Discord channels. + +## Features + +- Session event notifications (created, hibernated) +- User event notifications (created) +- Configurable notification preferences +- Rate limiting to prevent spam +- Customizable bot username and avatar +- Rich Discord embeds with colors and formatting +- Detailed or summary notifications + +## Installation + +### Via StreamSpace UI + +1. Navigate to **Admin** → **Plugins** +2. Search for "Discord Integration" +3. Click **Install** +4. Configure your Discord webhook URL +5. Enable the plugin + +### Via kubectl + +```bash +kubectl apply -f - < time.Hour { + p.messageCount = 0 + p.lastReset = now + } + + if p.messageCount >= int(maxMessages) { + return false + } + + p.messageCount++ + return true +} + +// Helper functions to safely extract values from maps +func (p *DiscordPlugin) getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (p *DiscordPlugin) getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return false +} + +// init auto-registers the plugin globally +func init() { + plugins.Register("streamspace-discord", func() plugins.PluginHandler { + return NewDiscordPlugin() + }) +} diff --git a/streamspace-discord/manifest.json b/streamspace-discord/manifest.json new file mode 100644 index 0000000..adb06d8 --- /dev/null +++ b/streamspace-discord/manifest.json @@ -0,0 +1,86 @@ +{ + "name": "streamspace-discord", + "version": "1.0.0", + "displayName": "Discord Integration", + "description": "Send session and user event notifications to Discord channels", + "author": "StreamSpace Team", + "license": "MIT", + "type": "webhook", + "category": "Integrations", + "tags": ["notifications", "discord", "integration"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "discord_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "webhookUrl": { + "type": "string", + "title": "Discord Webhook URL", + "description": "Your Discord channel webhook URL", + "pattern": "^https://discord\\.com/api/webhooks/[0-9]+/[a-zA-Z0-9_-]+$" + }, + "username": { + "type": "string", + "title": "Bot Username", + "description": "Override the default webhook username", + "default": "StreamSpace" + }, + "avatarUrl": { + "type": "string", + "title": "Bot Avatar URL", + "description": "Override the default webhook avatar image URL", + "format": "uri" + }, + "notifyOnSessionCreated": { + "type": "boolean", + "title": "Notify on Session Created", + "description": "Send notification when a session is created", + "default": true + }, + "notifyOnSessionHibernated": { + "type": "boolean", + "title": "Notify on Session Hibernated", + "description": "Send notification when a session is hibernated", + "default": true + }, + "notifyOnUserCreated": { + "type": "boolean", + "title": "Notify on User Created", + "description": "Send notification when a new user is created", + "default": true + }, + "includeDetails": { + "type": "boolean", + "title": "Include Resource Details", + "description": "Include CPU and memory information in notifications", + "default": false + }, + "rateLimit": { + "type": "number", + "title": "Rate Limit (messages per hour)", + "description": "Maximum number of messages to send per hour", + "default": 20, + "minimum": 1, + "maximum": 100 + } + }, + "required": ["webhookUrl"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.hibernated": "OnSessionHibernated", + "user.created": "OnUserCreated" + } +} diff --git a/streamspace-dlp/README.md b/streamspace-dlp/README.md new file mode 100644 index 0000000..e20bbcf --- /dev/null +++ b/streamspace-dlp/README.md @@ -0,0 +1,165 @@ +# StreamSpace Data Loss Prevention (DLP) Plugin + +Prevent data exfiltration with comprehensive controls for clipboard, file transfers, screen capture, printing, USB devices, and network access. + +## Features + +### Clipboard Controls +- Direction control (disabled, to-session, from-session, bidirectional) +- Size limits +- Content filtering (regex patterns) +- Sensitive data detection (SSN, credit cards, API keys) + +### File Transfer Controls +- Upload/download enable/disable +- File size limits +- File type whitelist/blacklist +- Malware scanning integration +- Content inspection + +### Screen Capture & Printing +- Screen capture blocking +- Watermarking (user ID, timestamp) +- Print job controls +- Screenshot detection + +### USB & Peripherals +- USB device blocking +- Audio input/output controls +- Microphone access control +- Webcam access control + +### Network Access Controls +- Domain allowlist/blocklist +- IP range restrictions +- URL filtering +- DNS-based controls + +### Session Controls +- Idle timeout enforcement +- Max session duration +- Access reason requirement +- Approval workflows + +### Violation Management +- Real-time violation detection +- Automatic blocking +- User/admin notifications +- Audit trail +- Violation analytics + +## Installation + +Via **Admin → Plugins**, search "Data Loss Prevention", click Install and Enable. + +## Configuration + +```json +{ + "enabled": true, + "defaultPolicy": "balanced", + "clipboardControl": { + "enabled": true, + "direction": "bidirectional", + "maxSize": 1048576 + }, + "fileTransferControl": { + "enabled": true, + "uploadEnabled": true, + "downloadEnabled": true, + "maxFileSize": 104857600, + "scanForMalware": true + }, + "screenCaptureControl": { + "enabled": false, + "watermarkEnabled": true, + "watermarkText": "{{user_id}} - {{timestamp}}" + }, + "deviceControl": { + "usbEnabled": false, + "audioEnabled": true, + "microphoneEnabled": false, + "webcamEnabled": false + }, + "violationActions": { + "alertOnViolation": true, + "blockOnViolation": true, + "notifyUser": true, + "notifyAdmin": true + } +} +``` + +## Policy Examples + +### Strict Security Policy +```json +{ + "name": "High Security Environment", + "clipboardDirection": "disabled", + "fileTransferEnabled": false, + "screenCaptureEnabled": false, + "printingEnabled": false, + "usbDevicesEnabled": false, + "blockOnViolation": true +} +``` + +### Balanced Policy +```json +{ + "name": "Standard Security", + "clipboardDirection": "bidirectional", + "clipboardMaxSize": 10240, + "fileUploadEnabled": true, + "fileDownloadEnabled": true, + "fileMaxSize": 10485760, + "fileTypeBlacklist": [".exe", ".bat", ".sh"], + "screenCaptureEnabled": true, + "watermarkEnabled": true +} +``` + +## Violation Types + +- **clipboard_violation** - Clipboard use blocked by policy +- **file_transfer_violation** - File transfer blocked +- **file_size_violation** - File exceeds size limit +- **file_type_violation** - File type not allowed +- **screen_capture_violation** - Screen capture attempted +- **usb_device_violation** - USB device blocked +- **network_access_violation** - Network access blocked +- **idle_timeout_violation** - Session idle timeout exceeded + +## API Usage + +### Create DLP Policy +```bash +POST /api/plugins/dlp/policies +{ + "name": "Finance Team DLP", + "priority": 10, + "applyToTeams": ["finance"], + "clipboardEnabled": false, + "fileDownloadEnabled": false, + "alertOnViolation": true +} +``` + +### List Violations +```bash +GET /api/plugins/dlp/violations?severity=high&resolved=false +``` + +## Support + +- Docs: https://docs.streamspace.io/plugins/dlp +- GitHub: https://github.com/JoshuaAFerguson/streamspace-plugins/issues + +## License + +MIT License + +## Version History + +- **1.0.0** (2025-01-15) - Initial release with complete DLP controls diff --git a/streamspace-dlp/dlp_plugin.go b/streamspace-dlp/dlp_plugin.go new file mode 100644 index 0000000..94b5832 --- /dev/null +++ b/streamspace-dlp/dlp_plugin.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/json" + "fmt" + "time" + "github.com/yourusername/streamspace/api/internal/plugins" +) + +type DLPPlugin struct { + plugins.BasePlugin + config DLPConfig + policies []DLPPolicy +} + +type DLPConfig struct { + Enabled bool `json:"enabled"` + DefaultPolicy string `json:"defaultPolicy"` + ClipboardControl map[string]interface{} `json:"clipboardControl"` + FileTransferControl map[string]interface{} `json:"fileTransferControl"` + ScreenCaptureControl map[string]interface{} `json:"screenCaptureControl"` + DeviceControl map[string]interface{} `json:"deviceControl"` + NetworkControl map[string]interface{} `json:"networkControl"` + ViolationActions map[string]interface{} `json:"violationActions"` +} + +type DLPPolicy struct { + ID int64 `json:"id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + ClipboardDirection string `json:"clipboard_direction"` + FileTransferEnabled bool `json:"file_transfer_enabled"` + ScreenCaptureEnabled bool `json:"screen_capture_enabled"` + BlockOnViolation bool `json:"block_on_violation"` + CreatedAt time.Time `json:"created_at"` +} + +type DLPViolation struct { + ID int64 `json:"id"` + PolicyID int64 `json:"policy_id"` + UserID string `json:"user_id"` + ViolationType string `json:"violation_type"` + Severity string `json:"severity"` + Description string `json:"description"` + Action string `json:"action"` + OccurredAt time.Time `json:"occurred_at"` +} + +func (p *DLPPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("DLP plugin is disabled") + return nil + } + + p.createDatabaseTables(ctx) + p.loadPolicies(ctx) + + ctx.Logger.Info("DLP plugin initialized", "policies", len(p.policies)) + return nil +} + +func (p *DLPPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("DLP plugin loaded") + return nil +} + +func (p *DLPPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, _ := session.(map[string]interface{}) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + + for _, policy := range p.policies { + if policy.Enabled { + p.enforcePolicy(ctx, policy, userID) + } + } + return nil +} + +func (p *DLPPlugin) createDatabaseTables(ctx *plugins.PluginContext) error { + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS dlp_policies ( + id SERIAL PRIMARY KEY, name VARCHAR(200), enabled BOOLEAN, + clipboard_direction VARCHAR(50), file_transfer_enabled BOOLEAN, + screen_capture_enabled BOOLEAN, block_on_violation BOOLEAN, + created_at TIMESTAMP DEFAULT NOW() + )`) + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS dlp_violations ( + id SERIAL PRIMARY KEY, policy_id INTEGER, user_id VARCHAR(255), + violation_type VARCHAR(100), severity VARCHAR(50), description TEXT, + action VARCHAR(50), occurred_at TIMESTAMP DEFAULT NOW() + )`) + return nil +} + +func (p *DLPPlugin) loadPolicies(ctx *plugins.PluginContext) error { + rows, _ := ctx.Database.Query(`SELECT id, name, enabled, clipboard_direction, + file_transfer_enabled, screen_capture_enabled, block_on_violation, created_at + FROM dlp_policies WHERE enabled = true`) + defer rows.Close() + + for rows.Next() { + var policy DLPPolicy + rows.Scan(&policy.ID, &policy.Name, &policy.Enabled, &policy.ClipboardDirection, + &policy.FileTransferEnabled, &policy.ScreenCaptureEnabled, + &policy.BlockOnViolation, &policy.CreatedAt) + p.policies = append(p.policies, policy) + } + return nil +} + +func (p *DLPPlugin) enforcePolicy(ctx *plugins.PluginContext, policy DLPPolicy, userID string) { + ctx.Logger.Debug("Enforcing DLP policy", "policy", policy.Name, "user", userID) +} + +func init() { + plugins.Register("streamspace-dlp", &DLPPlugin{}) +} diff --git a/streamspace-dlp/manifest.json b/streamspace-dlp/manifest.json new file mode 100644 index 0000000..96bfaa9 --- /dev/null +++ b/streamspace-dlp/manifest.json @@ -0,0 +1,161 @@ +{ + "name": "streamspace-dlp", + "version": "1.0.0", + "displayName": "Data Loss Prevention (DLP)", + "description": "Prevent data exfiltration with comprehensive controls for clipboard, file transfers, screen capture, printing, USB devices, and network access", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Security", + "tags": ["dlp", "data-loss-prevention", "security", "clipboard", "file-transfer", "exfiltration"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "dlp_plugin.go" + }, + + "permissions": ["database", "admin_ui", "network"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable DLP", + "description": "Enable Data Loss Prevention policies", + "default": true + }, + "defaultPolicy": { + "type": "string", + "title": "Default Policy Mode", + "description": "Default DLP enforcement mode", + "enum": ["permissive", "balanced", "strict"], + "default": "balanced" + }, + "clipboardControl": { + "type": "object", + "title": "Clipboard Controls", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "direction": { + "type": "string", + "enum": ["disabled", "to_session", "from_session", "bidirectional"], + "default": "bidirectional" + }, + "maxSize": {"type": "integer", "default": 1048576} + } + }, + "fileTransferControl": { + "type": "object", + "title": "File Transfer Controls", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "uploadEnabled": {"type": "boolean", "default": true}, + "downloadEnabled": {"type": "boolean", "default": true}, + "maxFileSize": {"type": "integer", "default": 104857600}, + "scanForMalware": {"type": "boolean", "default": true} + } + }, + "screenCaptureControl": { + "type": "object", + "title": "Screen Capture Controls", + "properties": { + "enabled": {"type": "boolean", "default": false}, + "watermarkEnabled": {"type": "boolean", "default": true}, + "watermarkText": {"type": "string", "default": "{{user_id}} - {{timestamp}}"} + } + }, + "deviceControl": { + "type": "object", + "title": "Device Controls", + "properties": { + "usbEnabled": {"type": "boolean", "default": false}, + "audioEnabled": {"type": "boolean", "default": true}, + "microphoneEnabled": {"type": "boolean", "default": false}, + "webcamEnabled": {"type": "boolean", "default": false} + } + }, + "networkControl": { + "type": "object", + "title": "Network Access Controls", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "allowedDomains": {"type": "array", "items": {"type": "string"}, "default": []}, + "blockedDomains": {"type": "array", "items": {"type": "string"}, "default": []} + } + }, + "violationActions": { + "type": "object", + "title": "Violation Actions", + "properties": { + "alertOnViolation": {"type": "boolean", "default": true}, + "blockOnViolation": {"type": "boolean", "default": true}, + "notifyUser": {"type": "boolean", "default": true}, + "notifyAdmin": {"type": "boolean", "default": true} + } + } + } + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.clipboard_access": "OnClipboardAccess", + "session.file_transfer": "OnFileTransfer", + "session.screen_capture": "OnScreenCapture" + }, + "database": { + "tables": [ + "dlp_policies", + "dlp_violations", + "dlp_audit_log" + ] + }, + "api": { + "endpoints": [ + "/dlp/policies", + "/dlp/policies/:id", + "/dlp/violations", + "/dlp/violations/:id", + "/dlp/audit" + ] + }, + "ui": { + "adminPages": [ + { + "id": "dlp-dashboard", + "title": "DLP Dashboard", + "route": "/admin/dlp", + "component": "DLPDashboard", + "icon": "shield" + }, + { + "id": "dlp-policies", + "title": "DLP Policies", + "route": "/admin/dlp/policies", + "component": "DLPPolicies", + "icon": "policy" + }, + { + "id": "dlp-violations", + "title": "DLP Violations", + "route": "/admin/dlp/violations", + "component": "DLPViolations", + "icon": "warning" + } + ] + }, + "scheduler": { + "jobs": [ + { + "name": "audit-dlp-policies", + "schedule": "0 */6 * * *", + "description": "Audit DLP policy effectiveness" + } + ] + } +} diff --git a/streamspace-elastic-apm/README.md b/streamspace-elastic-apm/README.md new file mode 100644 index 0000000..dc76617 --- /dev/null +++ b/streamspace-elastic-apm/README.md @@ -0,0 +1,179 @@ +# StreamSpace Elastic APM Plugin + +Application Performance Monitoring integration with Elastic APM for distributed tracing and performance analysis. + +## Features + +- **Distributed Tracing** - Track requests across services +- **Performance Monitoring** - Identify slow transactions and bottlenecks +- **Resource Tracking** - Monitor CPU, memory, storage usage +- **Session Lifecycle** - Track session creation, duration, termination +- **Custom Labels** - Tag transactions for filtering and analysis +- **Error Tracking** - Capture and analyze errors + +## Installation + +1. Navigate to **Admin → Plugins** +2. Search for "Elastic APM" +3. Click **Install** and configure +4. Click **Enable** + +## Configuration + +### Basic Setup + +```json +{ + "enabled": true, + "serverUrl": "http://apm-server:8200", + "serviceName": "streamspace", + "environment": "production" +} +``` + +### With Authentication + +```json +{ + "enabled": true, + "serverUrl": "https://your-deployment.apm.elastic-cloud.com:443", + "secretToken": "your-secret-token", + "serviceName": "streamspace", + "serviceVersion": "1.0.0", + "environment": "production", + "transactionSampleRate": 1.0, + "globalLabels": { + "team": "platform", + "region": "us-east-1" + } +} +``` + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `serverUrl` | string | *required* | APM Server URL | +| `secretToken` | string | - | Authentication token | +| `apiKey` | string | - | API key (alternative to token) | +| `serviceName` | string | `streamspace` | Service name in APM | +| `serviceVersion` | string | `1.0.0` | Service version | +| `environment` | string | `production` | Environment name | +| `transactionSampleRate` | number | `1.0` | Sample rate (0.0-1.0) | +| `captureBody` | string | `errors` | Capture request bodies | +| `captureHeaders` | boolean | `true` | Capture HTTP headers | +| `globalLabels` | object | `{}` | Labels for all events | + +## Usage + +### View in Kibana + +1. Open Kibana +2. Navigate to **Observability → APM** +3. Select **streamspace** service +4. View: + - **Transactions** - Session lifecycle events + - **Errors** - Captured errors and exceptions + - **Metrics** - CPU, memory, throughput + - **Service Map** - Dependencies and connections + +### Transaction Types + +- **session-lifecycle** - Session creation/termination +- **session-monitor** - Heartbeat and resource monitoring +- **user-lifecycle** - User creation and activity +- **plugin-lifecycle** - Plugin load/unload + +### Analyzing Performance + +#### Slow Transactions +``` +APM → Transactions → Sort by Latency +- Identify slow session operations +- Analyze transaction timeline +- Review span details +``` + +#### Error Rate +``` +APM → Errors → Group by error type +- See most common errors +- Track error trends over time +- Link to affected transactions +``` + +#### Resource Usage +``` +APM → Metrics → Select metric +- CPU usage trends +- Memory consumption patterns +- Session count over time +``` + +## Elastic Cloud Setup + +### Getting APM Credentials + +1. Log into Elastic Cloud +2. Create deployment or use existing +3. Navigate to **APM & Fleet** +4. Copy **APM Server URL** and **Secret Token** +5. Use these in plugin configuration + +### Self-Hosted APM Server + +If running your own APM Server: + +```yaml +# apm-server.yml +apm-server: + host: "0.0.0.0:8200" + secret_token: "your-secret-token" + +output.elasticsearch: + hosts: ["localhost:9200"] +``` + +## Best Practices + +1. **Sample Rate** - Use 1.0 in development, 0.1-0.5 in production +2. **Global Labels** - Add environment, region, team for filtering +3. **Service Versions** - Update version on each deployment +4. **Monitor Errors** - Set up alerts for error spikes +5. **Review Weekly** - Check slow transactions and optimize + +## Troubleshooting + +### Transactions not appearing + +- Check APM server URL is accessible +- Verify secret token is correct +- Review APM server logs +- Ensure `transactionSampleRate` > 0 + +### High APM costs + +- Reduce `transactionSampleRate` (e.g., 0.1 = 10%) +- Disable `captureBody` if not needed +- Limit `transactionMaxSpans` +- Review Elastic Cloud pricing + +### Missing spans + +- Increase `transactionMaxSpans` +- Check `stackTraceLimit` setting +- Verify transactions are being sampled + +## Support + +- GitHub: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- Docs: https://docs.streamspace.io/plugins/elastic-apm +- Elastic APM Docs: https://www.elastic.co/guide/en/apm/get-started/current/overview.html + +## License + +MIT License + +## Version History + +- **1.0.0** (2025-01-15) - Initial release with distributed tracing and performance monitoring diff --git a/streamspace-elastic-apm/elastic_apm_plugin.go b/streamspace-elastic-apm/elastic_apm_plugin.go new file mode 100644 index 0000000..bd4122d --- /dev/null +++ b/streamspace-elastic-apm/elastic_apm_plugin.go @@ -0,0 +1,287 @@ +package main + +import ( + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/yourusername/streamspace/api/internal/plugins" + "go.elastic.co/apm/v2" +) + +// ElasticAPMPlugin integrates with Elastic APM for performance monitoring +type ElasticAPMPlugin struct { + plugins.BasePlugin + config ElasticAPMConfig + tracer *apm.Tracer + sessionStart map[string]time.Time + sessionMutex sync.Mutex +} + +// ElasticAPMConfig holds Elastic APM configuration +type ElasticAPMConfig struct { + Enabled bool `json:"enabled"` + ServerURL string `json:"serverUrl"` + SecretToken string `json:"secretToken"` + APIKey string `json:"apiKey"` + ServiceName string `json:"serviceName"` + ServiceVersion string `json:"serviceVersion"` + Environment string `json:"environment"` + TransactionSampleRate float64 `json:"transactionSampleRate"` + CaptureBody string `json:"captureBody"` + CaptureHeaders bool `json:"captureHeaders"` + StackTraceLimit int `json:"stackTraceLimit"` + TransactionMaxSpans int `json:"transactionMaxSpans"` + GlobalLabels map[string]string `json:"globalLabels"` +} + +// Initialize sets up the Elastic APM plugin +func (p *ElasticAPMPlugin) Initialize(ctx *plugins.PluginContext) error { + // Load configuration + configBytes, err := json.Marshal(ctx.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := json.Unmarshal(configBytes, &p.config); err != nil { + return fmt.Errorf("failed to unmarshal Elastic APM config: %w", err) + } + + if !p.config.Enabled { + ctx.Logger.Info("Elastic APM integration is disabled") + return nil + } + + if p.config.ServerURL == "" { + return fmt.Errorf("Elastic APM server URL is required") + } + + if p.config.ServiceName == "" { + p.config.ServiceName = "streamspace" + } + + // Initialize APM tracer + p.tracer, err = apm.NewTracer(p.config.ServiceName, p.config.ServiceVersion) + if err != nil { + return fmt.Errorf("failed to create APM tracer: %w", err) + } + + // Configure tracer (these would normally be set via environment variables) + // Note: The actual Elastic APM Go agent primarily uses environment variables + // This is a simplified example + + p.sessionStart = make(map[string]time.Time) + + ctx.Logger.Info("Elastic APM plugin initialized successfully", + "service_name", p.config.ServiceName, + "service_version", p.config.ServiceVersion, + "environment", p.config.Environment, + "sample_rate", p.config.TransactionSampleRate, + ) + + return nil +} + +// OnLoad is called when the plugin is loaded +func (p *ElasticAPMPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Elastic APM plugin loaded") + + // Send custom event + tx := p.tracer.StartTransaction("plugin.loaded", "plugin-lifecycle") + defer tx.End() + + tx.Context.SetLabel("plugin", "streamspace-elastic-apm") + tx.Context.SetLabel("version", "1.0.0") + + for k, v := range p.config.GlobalLabels { + tx.Context.SetLabel(k, v) + } + + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *ElasticAPMPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Elastic APM plugin unloading") + + // Flush and close tracer + if p.tracer != nil { + p.tracer.Flush(nil) + p.tracer.Close() + } + + return nil +} + +// OnSessionCreated tracks session creation +func (p *ElasticAPMPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Track session start time + p.sessionMutex.Lock() + p.sessionStart[sessionID] = time.Now() + p.sessionMutex.Unlock() + + // Create transaction + tx := p.tracer.StartTransaction("session.created", "session-lifecycle") + defer tx.End() + + tx.Context.SetLabel("session_id", sessionID) + tx.Context.SetLabel("user_id", userID) + tx.Context.SetLabel("template", templateName) + + for k, v := range p.config.GlobalLabels { + tx.Context.SetLabel(k, v) + } + + // Add custom context + tx.Context.SetCustom("session", map[string]interface{}{ + "id": sessionID, + "user_id": userID, + "template": templateName, + }) + + return nil +} + +// OnSessionTerminated tracks session termination +func (p *ElasticAPMPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Calculate duration + p.sessionMutex.Lock() + startTime, exists := p.sessionStart[sessionID] + duration := time.Duration(0) + if exists { + duration = time.Since(startTime) + delete(p.sessionStart, sessionID) + } + p.sessionMutex.Unlock() + + // Create transaction + tx := p.tracer.StartTransaction("session.terminated", "session-lifecycle") + defer tx.End() + + tx.Context.SetLabel("session_id", sessionID) + tx.Context.SetLabel("user_id", userID) + tx.Context.SetLabel("template", templateName) + tx.Context.SetLabel("duration_seconds", fmt.Sprintf("%.2f", duration.Seconds())) + + for k, v := range p.config.GlobalLabels { + tx.Context.SetLabel(k, v) + } + + // Add custom metrics + tx.Context.SetCustom("session", map[string]interface{}{ + "id": sessionID, + "user_id": userID, + "template": templateName, + "duration": duration.Seconds(), + }) + + return nil +} + +// OnSessionHeartbeat tracks session resource usage +func (p *ElasticAPMPlugin) OnSessionHeartbeat(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return nil + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + + // Create a short transaction for heartbeat + tx := p.tracer.StartTransaction("session.heartbeat", "session-monitor") + defer tx.End() + + tx.Context.SetLabel("session_id", sessionID) + + // Add resource usage metrics + if cpuUsage, ok := sessionMap["cpu_usage"].(float64); ok { + tx.Context.SetLabel("cpu_usage", fmt.Sprintf("%.2f", cpuUsage*100)) + } + + if memoryUsage, ok := sessionMap["memory_usage"].(float64); ok { + tx.Context.SetLabel("memory_mb", fmt.Sprintf("%.2f", memoryUsage/(1024*1024))) + } + + return nil +} + +// OnUserCreated tracks user creation +func (p *ElasticAPMPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user format") + } + + userID := fmt.Sprintf("%v", userMap["id"]) + + // Create transaction + tx := p.tracer.StartTransaction("user.created", "user-lifecycle") + defer tx.End() + + tx.Context.SetLabel("user_id", userID) + + for k, v := range p.config.GlobalLabels { + tx.Context.SetLabel(k, v) + } + + return nil +} + +// StartTransaction is a helper to start custom transactions +func (p *ElasticAPMPlugin) StartTransaction(name string, txType string) *apm.Transaction { + if !p.config.Enabled { + return nil + } + + return p.tracer.StartTransaction(name, txType) +} + +// RecordError records an error in APM +func (p *ElasticAPMPlugin) RecordError(err error, context map[string]interface{}) { + if !p.config.Enabled { + return + } + + // Send error to APM + apm.CaptureError(nil, err).Send() +} + +// Export the plugin +func init() { + plugins.Register("streamspace-elastic-apm", &ElasticAPMPlugin{}) +} diff --git a/streamspace-elastic-apm/manifest.json b/streamspace-elastic-apm/manifest.json new file mode 100644 index 0000000..813addf --- /dev/null +++ b/streamspace-elastic-apm/manifest.json @@ -0,0 +1,127 @@ +{ + "name": "streamspace-elastic-apm", + "version": "1.0.0", + "displayName": "Elastic APM Integration", + "description": "Application Performance Monitoring with Elastic APM and distributed tracing", + "author": "StreamSpace Team", + "license": "MIT", + "type": "integration", + "category": "Monitoring", + "tags": ["monitoring", "elastic", "apm", "performance", "tracing"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "elastic_apm_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Elastic APM", + "description": "Enable APM monitoring with Elastic", + "default": true + }, + "serverUrl": { + "type": "string", + "title": "APM Server URL", + "description": "Elastic APM Server URL (e.g., http://apm-server:8200)", + "default": "http://localhost:8200" + }, + "secretToken": { + "type": "string", + "title": "Secret Token", + "description": "APM Server secret token for authentication", + "format": "password" + }, + "apiKey": { + "type": "string", + "title": "API Key", + "description": "APM Server API key (alternative to secret token)", + "format": "password" + }, + "serviceName": { + "type": "string", + "title": "Service Name", + "description": "Name of the service in Elastic APM", + "default": "streamspace" + }, + "serviceVersion": { + "type": "string", + "title": "Service Version", + "description": "Version of the StreamSpace service", + "default": "1.0.0" + }, + "environment": { + "type": "string", + "title": "Environment", + "description": "Environment name (production, staging, development)", + "default": "production" + }, + "transactionSampleRate": { + "type": "number", + "title": "Transaction Sample Rate", + "description": "Percentage of transactions to sample (0.0-1.0)", + "default": 1.0, + "minimum": 0, + "maximum": 1 + }, + "captureBody": { + "type": "string", + "title": "Capture Request/Response Bodies", + "description": "Capture HTTP request/response bodies", + "enum": ["off", "errors", "transactions", "all"], + "default": "errors" + }, + "captureHeaders": { + "type": "boolean", + "title": "Capture Headers", + "description": "Capture HTTP request/response headers", + "default": true + }, + "stackTraceLimit": { + "type": "integer", + "title": "Stack Trace Limit", + "description": "Maximum depth of stack traces", + "default": 50, + "minimum": 0, + "maximum": 100 + }, + "transactionMaxSpans": { + "type": "integer", + "title": "Max Spans per Transaction", + "description": "Maximum number of spans per transaction", + "default": 500, + "minimum": 0, + "maximum": 1000 + }, + "globalLabels": { + "type": "object", + "title": "Global Labels", + "description": "Labels to add to all events", + "additionalProperties": { + "type": "string" + }, + "default": { + "team": "platform" + } + } + }, + "required": ["serverUrl", "serviceName"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "session.heartbeat": "OnSessionHeartbeat", + "user.created": "OnUserCreated" + } +} diff --git a/streamspace-email/README.md b/streamspace-email/README.md new file mode 100644 index 0000000..b547ae7 --- /dev/null +++ b/streamspace-email/README.md @@ -0,0 +1,318 @@ +# StreamSpace Email SMTP Integration Plugin + +Send email notifications via SMTP for StreamSpace session and user events with rich HTML formatting. + +## Features + +- SMTP email notifications for session and user events +- Support for multiple SMTP providers (Gmail, Office365, custom) +- HTML and plain text email formats +- Multiple recipients (To, CC) +- TLS/STARTTLS encryption support +- Configurable notification preferences +- Rate limiting to prevent email spam +- Detailed or summary notifications +- Customizable sender name and address + +## Installation + +### Via StreamSpace UI + +1. Navigate to **Admin** → **Plugins** +2. Search for "Email SMTP Integration" +3. Click **Install** +4. Configure your SMTP settings +5. Enable the plugin + +### Via kubectl + +```bash +kubectl apply -f - <", fromName, fromAddress) + headers := make(map[string]string) + headers["From"] = from + headers["To"] = strings.Join(to, ", ") + if len(cc) > 0 { + headers["Cc"] = strings.Join(cc, ", ") + } + headers["Subject"] = subject + headers["MIME-Version"] = "1.0" + + if strings.Contains(body, "") { + headers["Content-Type"] = "text/html; charset=UTF-8" + } else { + headers["Content-Type"] = "text/plain; charset=UTF-8" + } + + // Build message + message := "" + for k, v := range headers { + message += fmt.Sprintf("%s: %s\r\n", k, v) + } + message += "\r\n" + body + + // All recipients (to + cc) + recipients := append(to, cc...) + + // Setup SMTP authentication + auth := smtp.PlainAuth("", username, password, host) + + // Connect to SMTP server + addr := fmt.Sprintf("%s:%d", host, port) + + // Send email + if useTLS && port == 587 { + // Use STARTTLS + return p.sendMailTLS(addr, auth, fromAddress, recipients, []byte(message)) + } else { + // Use plain SMTP or implicit TLS + return smtp.SendMail(addr, auth, fromAddress, recipients, []byte(message)) + } +} + +// sendMailTLS sends email with STARTTLS +func (p *EmailPlugin) sendMailTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte) error { + // Connect to server + client, err := smtp.Dial(addr) + if err != nil { + return err + } + defer client.Close() + + // Start TLS + tlsConfig := &tls.Config{ + ServerName: strings.Split(addr, ":")[0], + } + if err = client.StartTLS(tlsConfig); err != nil { + return err + } + + // Authenticate + if err = client.Auth(auth); err != nil { + return err + } + + // Set sender + if err = client.Mail(from); err != nil { + return err + } + + // Set recipients + for _, recipient := range to { + if err = client.Rcpt(recipient); err != nil { + return err + } + } + + // Send message + w, err := client.Data() + if err != nil { + return err + } + + _, err = w.Write(msg) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + return client.Quit() +} + +// testSMTP tests the SMTP connection +func (p *EmailPlugin) testSMTP(ctx *plugins.PluginContext) error { + subject := "StreamSpace Email Plugin Test" + body := p.buildHTMLTest() + return p.sendEmail(ctx, subject, body) +} + +// validateConfig validates the plugin configuration +func (p *EmailPlugin) validateConfig(config map[string]interface{}) error { + required := []string{"smtpHost", "smtpPort", "username", "password", "fromAddress", "toAddresses"} + + for _, field := range required { + if _, ok := config[field]; !ok { + return fmt.Errorf("required field '%s' is missing", field) + } + } + + return nil +} + +// checkRateLimit checks if we're within the rate limit +func (p *EmailPlugin) checkRateLimit(ctx *plugins.PluginContext) bool { + maxEmails, _ := ctx.Config["rateLimit"].(float64) + if maxEmails == 0 { + maxEmails = 30 // Default + } + + now := time.Now() + if now.Sub(p.lastReset) > time.Hour { + p.emailCount = 0 + p.lastReset = now + } + + if p.emailCount >= int(maxEmails) { + return false + } + + p.emailCount++ + return true +} + +// HTML email templates + +func (p *EmailPlugin) buildHTMLSessionCreated(user, template, sessionID string, sessionMap map[string]interface{}, ctx *plugins.PluginContext) string { + details := "" + if p.getBool(ctx.Config, "includeDetails") { + if resources, ok := sessionMap["resources"].(map[string]interface{}); ok { + memory := p.getString(resources, "memory") + cpu := p.getString(resources, "cpu") + details = fmt.Sprintf(` + Memory:%s + CPU:%s + `, memory, cpu) + } + } + + return fmt.Sprintf(` + + + + + + +
+
+

🚀 New Session Created

+
+
+

A new session has been created in StreamSpace.

+ + + + + %s +
User:%s
Template:%s
Session ID:%s
+ +
+
+ + + `, user, template, sessionID, details, time.Now().Format("2006-01-02 15:04:05 MST")) +} + +func (p *EmailPlugin) buildHTMLSessionHibernated(user, sessionID string) string { + return fmt.Sprintf(` + + + + + + +
+
+

💤 Session Hibernated

+
+
+

A session has been hibernated due to inactivity.

+ + + +
User:%s
Session ID:%s
+ +
+
+ + + `, user, sessionID, time.Now().Format("2006-01-02 15:04:05 MST")) +} + +func (p *EmailPlugin) buildHTMLUserCreated(username, fullName, email, tier string) string { + return fmt.Sprintf(` + + + + + + +
+
+

👤 New User Created

+
+
+

A new user has been created in StreamSpace.

+ + + + + +
Username:%s
Full Name:%s
Email:%s
Tier:%s
+ +
+
+ + + `, username, fullName, email, tier, time.Now().Format("2006-01-02 15:04:05 MST")) +} + +func (p *EmailPlugin) buildHTMLTest() string { + return ` + + + + + + +
+
+

🎉 StreamSpace Email Plugin Activated

+
+
+

Your SMTP email integration is now configured and ready to send notifications.

+

This is a test email to verify that your SMTP settings are correct.

+
+
+ + + ` +} + +// Plain text email templates + +func (p *EmailPlugin) buildPlainSessionCreated(user, template, sessionID string, sessionMap map[string]interface{}, ctx *plugins.PluginContext) string { + details := "" + if p.getBool(ctx.Config, "includeDetails") { + if resources, ok := sessionMap["resources"].(map[string]interface{}); ok { + memory := p.getString(resources, "memory") + cpu := p.getString(resources, "cpu") + details = fmt.Sprintf("\nMemory: %s\nCPU: %s", memory, cpu) + } + } + + return fmt.Sprintf(`New Session Created + +A new session has been created in StreamSpace. + +User: %s +Template: %s +Session ID: %s%s + +--- +StreamSpace Notifications +%s + `, user, template, sessionID, details, time.Now().Format("2006-01-02 15:04:05 MST")) +} + +func (p *EmailPlugin) buildPlainSessionHibernated(user, sessionID string) string { + return fmt.Sprintf(`Session Hibernated + +A session has been hibernated due to inactivity. + +User: %s +Session ID: %s + +--- +StreamSpace Notifications +%s + `, user, sessionID, time.Now().Format("2006-01-02 15:04:05 MST")) +} + +func (p *EmailPlugin) buildPlainUserCreated(username, fullName, email, tier string) string { + return fmt.Sprintf(`New User Created + +A new user has been created in StreamSpace. + +Username: %s +Full Name: %s +Email: %s +Tier: %s + +--- +StreamSpace Notifications +%s + `, username, fullName, email, tier, time.Now().Format("2006-01-02 15:04:05 MST")) +} + +// Helper functions + +func (p *EmailPlugin) getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (p *EmailPlugin) getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return false +} + +func (p *EmailPlugin) getInt(m map[string]interface{}, key string) int { + if val, ok := m[key]; ok { + if i, ok := val.(float64); ok { + return int(i) + } + if i, ok := val.(int); ok { + return i + } + } + return 0 +} + +func (p *EmailPlugin) getStringArray(m map[string]interface{}, key string) []string { + if val, ok := m[key]; ok { + if arr, ok := val.([]interface{}); ok { + result := make([]string, 0, len(arr)) + for _, item := range arr { + if str, ok := item.(string); ok { + result = append(result, str) + } + } + return result + } + } + return []string{} +} + +// init auto-registers the plugin globally +func init() { + plugins.Register("streamspace-email", func() plugins.PluginHandler { + return NewEmailPlugin() + }) +} diff --git a/streamspace-email/manifest.json b/streamspace-email/manifest.json new file mode 100644 index 0000000..61b9e60 --- /dev/null +++ b/streamspace-email/manifest.json @@ -0,0 +1,135 @@ +{ + "name": "streamspace-email", + "version": "1.0.0", + "displayName": "Email SMTP Integration", + "description": "Send email notifications via SMTP for session and user events", + "author": "StreamSpace Team", + "license": "MIT", + "type": "integration", + "category": "Integrations", + "tags": ["email", "smtp", "notifications", "alerts"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "email_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "smtpHost": { + "type": "string", + "title": "SMTP Host", + "description": "SMTP server hostname (e.g., smtp.gmail.com)", + "examples": ["smtp.gmail.com", "smtp.office365.com", "mail.example.com"] + }, + "smtpPort": { + "type": "integer", + "title": "SMTP Port", + "description": "SMTP server port (587 for STARTTLS, 465 for TLS, 25 for plain)", + "default": 587, + "enum": [25, 465, 587, 2525] + }, + "username": { + "type": "string", + "title": "SMTP Username", + "description": "Username for SMTP authentication" + }, + "password": { + "type": "string", + "title": "SMTP Password", + "description": "Password for SMTP authentication", + "format": "password" + }, + "fromAddress": { + "type": "string", + "title": "From Email Address", + "description": "Email address to send from", + "format": "email" + }, + "fromName": { + "type": "string", + "title": "From Name", + "description": "Display name for the sender", + "default": "StreamSpace Notifications" + }, + "toAddresses": { + "type": "array", + "title": "To Email Addresses", + "description": "List of recipient email addresses", + "items": { + "type": "string", + "format": "email" + }, + "minItems": 1 + }, + "ccAddresses": { + "type": "array", + "title": "CC Email Addresses", + "description": "List of CC recipient email addresses", + "items": { + "type": "string", + "format": "email" + } + }, + "useTLS": { + "type": "boolean", + "title": "Use TLS", + "description": "Enable TLS encryption (recommended for port 587)", + "default": true + }, + "notifyOnSessionCreated": { + "type": "boolean", + "title": "Notify on Session Created", + "description": "Send email when a session is created", + "default": true + }, + "notifyOnSessionHibernated": { + "type": "boolean", + "title": "Notify on Session Hibernated", + "description": "Send email when a session is hibernated", + "default": true + }, + "notifyOnUserCreated": { + "type": "boolean", + "title": "Notify on User Created", + "description": "Send email when a new user is created", + "default": true + }, + "includeDetails": { + "type": "boolean", + "title": "Include Resource Details", + "description": "Include CPU and memory information in emails", + "default": true + }, + "htmlFormat": { + "type": "boolean", + "title": "HTML Format", + "description": "Send HTML formatted emails (recommended)", + "default": true + }, + "rateLimit": { + "type": "number", + "title": "Rate Limit (emails per hour)", + "description": "Maximum number of emails to send per hour", + "default": 30, + "minimum": 1, + "maximum": 100 + } + }, + "required": ["smtpHost", "smtpPort", "username", "password", "fromAddress", "toAddresses"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.hibernated": "OnSessionHibernated", + "user.created": "OnUserCreated" + } +} diff --git a/streamspace-honeycomb/README.md b/streamspace-honeycomb/README.md new file mode 100644 index 0000000..d16ed1c --- /dev/null +++ b/streamspace-honeycomb/README.md @@ -0,0 +1,303 @@ +# StreamSpace Honeycomb Plugin + +High-definition observability integration with Honeycomb for deep system analysis and debugging. + +## Features + +- **High-Cardinality Events** - Track unlimited unique dimensions +- **Deep Debugging** - Drill down into any attribute or combination +- **Session Tracking** - Complete session lifecycle with duration +- **Resource Monitoring** - CPU, memory, storage metrics +- **User Activity** - Track user behavior patterns +- **BubbleUp Analysis** - Automatically find outliers and anomalies +- **Custom Fields** - Add any metadata to events + +## Installation + +1. Navigate to **Admin → Plugins** +2. Search for "Honeycomb Observability" +3. Click **Install** and configure +4. Click **Enable** + +## Configuration + +### Basic Setup + +```json +{ + "enabled": true, + "apiKey": "your-honeycomb-api-key", + "dataset": "streamspace" +} +``` + +### Full Configuration + +```json +{ + "enabled": true, + "apiKey": "hcaik_1234567890abcdef", + "dataset": "streamspace-production", + "apiHost": "https://api.honeycomb.io", + "sampleRate": 1, + "sendFrequency": 1000, + "maxBatchSize": 100, + "trackSessions": true, + "trackResources": true, + "trackUsers": true, + "enableTracing": true, + "customFields": { + "service": "streamspace", + "environment": "production", + "region": "us-east-1", + "team": "platform" + } +} +``` + +### Getting Your API Key + +1. Log into Honeycomb.io +2. Navigate to **Team Settings → API Keys** +3. Create new key or copy existing +4. Use in plugin configuration + +## Events Sent + +### Session Events + +- **session.created** - New session started + - Fields: `session_id`, `user_id`, `template`, `duration_ms` +- **session.terminated** - Session ended + - Fields: `session_id`, `user_id`, `template`, `duration_ms`, `duration_sec` +- **session.heartbeat** - Resource usage snapshot + - Fields: `session_id`, `cpu_usage_percent`, `memory_mb`, `storage_mb` + +### User Events + +- **user.created** - New user registered + - Fields: `user_id` +- **user.login** - User logged in + - Fields: `user_id` +- **user.logout** - User logged out + - Fields: `user_id` + +### Plugin Events + +- **plugin.loaded** - Plugin activated +- **plugin.unloaded** - Plugin deactivated + +## Usage in Honeycomb + +### Query Examples + +#### Find Slow Sessions +``` +VISUALIZE: HEATMAP(duration_sec) +WHERE: name = "session.terminated" +GROUP BY: template +``` + +#### Session Count by User +``` +VISUALIZE: COUNT +WHERE: name = "session.created" +GROUP BY: user_id +``` + +#### High CPU Usage +``` +VISUALIZE: P99(cpu_usage_percent) +WHERE: name = "session.heartbeat" +GROUP BY: template +``` + +#### Memory Usage Trends +``` +VISUALIZE: AVG(memory_mb) +WHERE: name = "session.heartbeat" +GROUP BY: session_id +``` + +### BubbleUp Analysis + +Automatically find what's different about slow sessions: + +1. Create query: `WHERE name = "session.terminated"` +2. Filter for slow sessions: `duration_sec > 3600` +3. Click **BubbleUp** +4. Honeycomb shows which attributes correlate with slow sessions + +### Tracing + +View distributed traces: + +1. Query: `WHERE name = "session.created"` +2. Click on an event +3. View **Trace Timeline** +4. See all related events in chronological order + +## Queries & Boards + +### Session Overview Board + +``` +Widget 1: Session Rate +- COUNT WHERE name = "session.created" +- VISUALIZE: Line chart, Group by time + +Widget 2: Active Sessions by Template +- COUNT WHERE name IN ("session.created", "session.terminated") +- VISUALIZE: Stacked area, Group by template + +Widget 3: Average Session Duration +- AVG(duration_sec) WHERE name = "session.terminated" +- VISUALIZE: Heatmap, Group by template + +Widget 4: CPU Usage Distribution +- HEATMAP(cpu_usage_percent) WHERE name = "session.heartbeat" +- VISUALIZE: Heatmap +``` + +### Resource Utilization Board + +``` +Widget 1: CPU Usage P99 +- P99(cpu_usage_percent) WHERE name = "session.heartbeat" +- VISUALIZE: Line chart, Group by template + +Widget 2: Memory Usage Trend +- AVG(memory_mb) WHERE name = "session.heartbeat" +- VISUALIZE: Line chart, Group by session_id (top 10) + +Widget 3: Storage by User +- SUM(storage_mb) WHERE name = "session.heartbeat" +- VISUALIZE: Bar chart, Group by user_id +``` + +## Triggers (Alerts) + +### High Session Creation Rate + +``` +Query: COUNT WHERE name = "session.created" +Frequency: Check every 1 minute +Threshold: Alert when > 100 +Recipients: #platform-alerts Slack channel +``` + +### Long-Running Sessions + +``` +Query: MAX(duration_sec) WHERE name = "session.terminated" +Frequency: Check every 5 minutes +Threshold: Alert when > 28800 (8 hours) +Recipients: ops-team@company.com +``` + +### High CPU Usage + +``` +Query: AVG(cpu_usage_percent) WHERE name = "session.heartbeat" +Frequency: Check every 1 minute +Threshold: Alert when > 90 +Recipients: PagerDuty integration +``` + +## Best Practices + +1. **Start Broad** - Query all events, then filter down +2. **Use BubbleUp** - Let Honeycomb find patterns automatically +3. **Add Context** - Use customFields for environment, region, version +4. **Create Boards** - Build dashboards for common views +5. **Set Up Triggers** - Proactive alerts on anomalies +6. **Sample Wisely** - Use sampleRate=1 unless very high volume +7. **Batch Events** - Don't set sendFrequency too low + +## Sampling + +Control data volume and costs: + +```json +{ + "sampleRate": 10 // 1 in 10 events (10%) +} +``` + +Honeycomb adjusts counts automatically when displaying results. + +**Recommendations**: +- Development: `sampleRate: 1` (100%) +- Production (low volume): `sampleRate: 1` (100%) +- Production (high volume): `sampleRate: 10-100` (10%-1%) + +## Troubleshooting + +### Events not appearing + +- Verify API key is correct +- Check dataset name matches +- Review plugin logs +- Wait 10-30 seconds for events to appear +- Check Honeycomb team quota + +### High costs + +- Increase `sampleRate` (10, 100, 1000) +- Reduce `maxBatchSize` +- Disable `trackResources` if not needed (high frequency) +- Review Honeycomb pricing and event volume + +### Missing fields + +- Ensure custom fields are in `customFields` config +- Check event data contains expected fields +- Verify no field name conflicts + +## Advanced Features + +### Derived Columns + +Create calculated fields in Honeycomb: + +``` +Column: session_hours +Formula: duration_sec / 3600 +``` + +### Query Specifications + +Save complex queries: + +1. Build query in Honeycomb +2. Click **Save Query Spec** +3. Share with team +4. Reuse in multiple boards + +### Service Level Objectives (SLOs) + +Track service health: + +``` +SLO: 99% of sessions start within 5 seconds +Query: P99(duration_ms) WHERE name = "session.created" < 5000 +``` + +## Support + +- GitHub: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- Docs: https://docs.streamspace.io/plugins/honeycomb +- Honeycomb Docs: https://docs.honeycomb.io/ + +## License + +MIT License + +## Version History + +- **1.0.0** (2025-01-15) + - Initial release + - Session, resource, and user tracking + - High-cardinality events + - BubbleUp support + - Distributed tracing diff --git a/streamspace-honeycomb/honeycomb_plugin.go b/streamspace-honeycomb/honeycomb_plugin.go new file mode 100644 index 0000000..114a4d6 --- /dev/null +++ b/streamspace-honeycomb/honeycomb_plugin.go @@ -0,0 +1,391 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/yourusername/streamspace/api/internal/plugins" +) + +// HoneycombPlugin sends high-cardinality observability events to Honeycomb +type HoneycombPlugin struct { + plugins.BasePlugin + config HoneycombConfig + httpClient *http.Client + eventBuffer []HoneycombEvent + bufferMutex sync.Mutex + sessionStart map[string]time.Time + sessionMutex sync.Mutex +} + +// HoneycombConfig holds Honeycomb configuration +type HoneycombConfig struct { + Enabled bool `json:"enabled"` + APIKey string `json:"apiKey"` + Dataset string `json:"dataset"` + APIHost string `json:"apiHost"` + SampleRate int `json:"sampleRate"` + SendFrequency int `json:"sendFrequency"` + MaxBatchSize int `json:"maxBatchSize"` + TrackSessions bool `json:"trackSessions"` + TrackResources bool `json:"trackResources"` + TrackUsers bool `json:"trackUsers"` + EnableTracing bool `json:"enableTracing"` + CustomFields map[string]string `json:"customFields"` +} + +// HoneycombEvent represents a single event sent to Honeycomb +type HoneycombEvent struct { + Timestamp time.Time `json:"time"` + Data map[string]interface{} `json:"data"` + SampleRate int `json:"samplerate,omitempty"` +} + +// HoneycombBatch represents a batch of events +type HoneycombBatch []HoneycombEvent + +// Initialize sets up the Honeycomb plugin +func (p *HoneycombPlugin) Initialize(ctx *plugins.PluginContext) error { + // Load configuration + configBytes, err := json.Marshal(ctx.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := json.Unmarshal(configBytes, &p.config); err != nil { + return fmt.Errorf("failed to unmarshal Honeycomb config: %w", err) + } + + if !p.config.Enabled { + ctx.Logger.Info("Honeycomb integration is disabled") + return nil + } + + if p.config.APIKey == "" { + return fmt.Errorf("Honeycomb API key is required") + } + + if p.config.Dataset == "" { + return fmt.Errorf("Honeycomb dataset is required") + } + + if p.config.APIHost == "" { + p.config.APIHost = "https://api.honeycomb.io" + } + + if p.config.SampleRate < 1 { + p.config.SampleRate = 1 + } + + // Initialize HTTP client + p.httpClient = &http.Client{ + Timeout: 10 * time.Second, + } + + // Initialize buffers + p.eventBuffer = []HoneycombEvent{} + p.sessionStart = make(map[string]time.Time) + + ctx.Logger.Info("Honeycomb plugin initialized successfully", + "dataset", p.config.Dataset, + "api_host", p.config.APIHost, + "sample_rate", p.config.SampleRate, + ) + + return nil +} + +// OnLoad is called when the plugin is loaded +func (p *HoneycombPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Honeycomb observability plugin loaded") + + return p.sendEvent("plugin.loaded", map[string]interface{}{ + "plugin_name": "streamspace-honeycomb", + "plugin_version": "1.0.0", + "status": "active", + }) +} + +// OnUnload is called when the plugin is unloaded +func (p *HoneycombPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Honeycomb observability plugin unloading") + + // Flush any remaining events + if err := p.flushEvents(ctx); err != nil { + ctx.Logger.Error("Failed to flush events on unload", "error", err) + } + + return p.sendEvent("plugin.unloaded", map[string]interface{}{ + "plugin_name": "streamspace-honeycomb", + "status": "inactive", + }) +} + +// OnSessionCreated tracks session creation +func (p *HoneycombPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackSessions { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Track session start time + p.sessionMutex.Lock() + p.sessionStart[sessionID] = time.Now() + p.sessionMutex.Unlock() + + // Send event + return p.sendEvent("session.created", map[string]interface{}{ + "session_id": sessionID, + "user_id": userID, + "template": templateName, + "event_type": "session_lifecycle", + "duration_ms": 0, + }) +} + +// OnSessionTerminated tracks session termination +func (p *HoneycombPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackSessions { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Calculate duration + p.sessionMutex.Lock() + startTime, exists := p.sessionStart[sessionID] + durationMs := int64(0) + if exists { + durationMs = time.Since(startTime).Milliseconds() + delete(p.sessionStart, sessionID) + } + p.sessionMutex.Unlock() + + // Send event + return p.sendEvent("session.terminated", map[string]interface{}{ + "session_id": sessionID, + "user_id": userID, + "template": templateName, + "event_type": "session_lifecycle", + "duration_ms": durationMs, + "duration_sec": float64(durationMs) / 1000.0, + }) +} + +// OnSessionHeartbeat tracks session resource usage +func (p *HoneycombPlugin) OnSessionHeartbeat(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackResources { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return nil + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + data := map[string]interface{}{ + "session_id": sessionID, + "user_id": userID, + "template": templateName, + "event_type": "resource_usage", + } + + // Add resource metrics + if cpuUsage, ok := sessionMap["cpu_usage"].(float64); ok { + data["cpu_usage_percent"] = cpuUsage * 100 + } + + if memoryUsage, ok := sessionMap["memory_usage"].(float64); ok { + data["memory_bytes"] = memoryUsage + data["memory_mb"] = memoryUsage / (1024 * 1024) + } + + if storageUsage, ok := sessionMap["storage_usage"].(float64); ok { + data["storage_bytes"] = storageUsage + data["storage_mb"] = storageUsage / (1024 * 1024) + } + + return p.sendEvent("session.heartbeat", data) +} + +// OnUserCreated tracks user creation +func (p *HoneycombPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUsers { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user format") + } + + userID := fmt.Sprintf("%v", userMap["id"]) + + return p.sendEvent("user.created", map[string]interface{}{ + "user_id": userID, + "event_type": "user_lifecycle", + }) +} + +// OnUserLogin tracks user login +func (p *HoneycombPlugin) OnUserLogin(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUsers { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", userMap["id"]) + + return p.sendEvent("user.login", map[string]interface{}{ + "user_id": userID, + "event_type": "user_activity", + }) +} + +// OnUserLogout tracks user logout +func (p *HoneycombPlugin) OnUserLogout(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUsers { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", userMap["id"]) + + return p.sendEvent("user.logout", map[string]interface{}{ + "user_id": userID, + "event_type": "user_activity", + }) +} + +// RunScheduledJob handles the scheduled event flush +func (p *HoneycombPlugin) RunScheduledJob(ctx *plugins.PluginContext, jobName string) error { + if jobName == "flush-events" { + return p.flushEvents(ctx) + } + return nil +} + +// sendEvent adds an event to the buffer +func (p *HoneycombPlugin) sendEvent(name string, data map[string]interface{}) error { + if !p.config.Enabled { + return nil + } + + // Merge with custom fields + eventData := make(map[string]interface{}) + for k, v := range p.config.CustomFields { + eventData[k] = v + } + for k, v := range data { + eventData[k] = v + } + + // Add event name + eventData["name"] = name + + p.bufferMutex.Lock() + defer p.bufferMutex.Unlock() + + event := HoneycombEvent{ + Timestamp: time.Now(), + Data: eventData, + SampleRate: p.config.SampleRate, + } + + p.eventBuffer = append(p.eventBuffer, event) + + // Auto-flush if batch size reached + if len(p.eventBuffer) >= p.config.MaxBatchSize { + go p.flushEvents(nil) + } + + return nil +} + +// flushEvents sends buffered events to Honeycomb +func (p *HoneycombPlugin) flushEvents(ctx *plugins.PluginContext) error { + if !p.config.Enabled { + return nil + } + + p.bufferMutex.Lock() + if len(p.eventBuffer) == 0 { + p.bufferMutex.Unlock() + return nil + } + + // Get events and clear buffer + events := make([]HoneycombEvent, len(p.eventBuffer)) + copy(events, p.eventBuffer) + p.eventBuffer = []HoneycombEvent{} + p.bufferMutex.Unlock() + + // Send to Honeycomb + payloadBytes, err := json.Marshal(events) + if err != nil { + return fmt.Errorf("failed to marshal events: %w", err) + } + + url := fmt.Sprintf("%s/1/batch/%s", p.config.APIHost, p.config.Dataset) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Honeycomb-Team", p.config.APIKey) + + resp, err := p.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send events: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Honeycomb API returned status %d: %s", resp.StatusCode, string(body)) + } + + if ctx != nil { + ctx.Logger.Info("Sent events to Honeycomb", "count", len(events)) + } + + return nil +} + +// Export the plugin +func init() { + plugins.Register("streamspace-honeycomb", &HoneycombPlugin{}) +} diff --git a/streamspace-honeycomb/manifest.json b/streamspace-honeycomb/manifest.json new file mode 100644 index 0000000..719411f --- /dev/null +++ b/streamspace-honeycomb/manifest.json @@ -0,0 +1,131 @@ +{ + "name": "streamspace-honeycomb", + "version": "1.0.0", + "displayName": "Honeycomb Observability", + "description": "High-definition observability with Honeycomb for deep system analysis and debugging", + "author": "StreamSpace Team", + "license": "MIT", + "type": "integration", + "category": "Monitoring", + "tags": ["monitoring", "honeycomb", "observability", "tracing", "debugging"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "honeycomb_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Honeycomb Integration", + "description": "Enable observability with Honeycomb", + "default": true + }, + "apiKey": { + "type": "string", + "title": "Honeycomb API Key", + "description": "Your Honeycomb write key", + "format": "password" + }, + "dataset": { + "type": "string", + "title": "Dataset Name", + "description": "Honeycomb dataset to send events to", + "default": "streamspace" + }, + "apiHost": { + "type": "string", + "title": "API Host", + "description": "Honeycomb API endpoint", + "default": "https://api.honeycomb.io" + }, + "sampleRate": { + "type": "integer", + "title": "Sample Rate", + "description": "1 in N events to sample (1 = all events)", + "default": 1, + "minimum": 1 + }, + "sendFrequency": { + "type": "integer", + "title": "Send Frequency (ms)", + "description": "How often to batch and send events", + "default": 1000, + "minimum": 100, + "maximum": 60000 + }, + "maxBatchSize": { + "type": "integer", + "title": "Max Batch Size", + "description": "Maximum events per batch", + "default": 100, + "minimum": 1, + "maximum": 1000 + }, + "trackSessions": { + "type": "boolean", + "title": "Track Sessions", + "description": "Send session lifecycle events", + "default": true + }, + "trackResources": { + "type": "boolean", + "title": "Track Resources", + "description": "Send resource usage metrics", + "default": true + }, + "trackUsers": { + "type": "boolean", + "title": "Track Users", + "description": "Send user activity events", + "default": true + }, + "enableTracing": { + "type": "boolean", + "title": "Enable Distributed Tracing", + "description": "Send distributed traces", + "default": true + }, + "customFields": { + "type": "object", + "title": "Custom Fields", + "description": "Custom fields to add to all events", + "additionalProperties": { + "type": "string" + }, + "default": { + "service": "streamspace", + "environment": "production" + } + } + }, + "required": ["apiKey", "dataset"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "session.heartbeat": "OnSessionHeartbeat", + "user.created": "OnUserCreated", + "user.login": "OnUserLogin", + "user.logout": "OnUserLogout" + }, + "scheduler": { + "jobs": [ + { + "name": "flush-events", + "schedule": "*/1 * * * *", + "description": "Flush buffered events to Honeycomb" + } + ] + } +} diff --git a/streamspace-multi-monitor/README.md b/streamspace-multi-monitor/README.md new file mode 100644 index 0000000..82ff4d9 --- /dev/null +++ b/streamspace-multi-monitor/README.md @@ -0,0 +1,37 @@ +# StreamSpace Multi-Monitor Support Plugin + +Enables advanced multi-monitor configurations for sessions with independent VNC streams for each display. + +## Features +- Create custom monitor layouts (horizontal, vertical, grid, custom) +- Support for up to 16 monitors per session +- Independent VNC streams for each display +- Monitor-specific settings (resolution, rotation, scale) +- Save and reuse monitor configurations + +## Installation +Install via Plugin Marketplace: Admin > Plugins > Search "Multi-Monitor" + +## Configuration +```json +{ + "maxMonitorsPerSession": 8, + "defaultLayout": "horizontal", + "allowCustomLayouts": true +} +``` + +## API Endpoints +All endpoints are prefixed with `/api/plugins/streamspace-multi-monitor` + +- `POST /sessions/:sessionId/monitors` - Create monitor configuration +- `GET /sessions/:sessionId/monitors` - List configurations +- `POST /sessions/:sessionId/monitors/:configId/activate` - Activate configuration +- `GET /sessions/:sessionId/monitors/:configId/streams` - Get VNC stream URLs + +## Database Tables +- `monitor_configurations` - Saved monitor layouts +- `monitor_displays` - Individual display settings + +## License +MIT - StreamSpace Team diff --git a/streamspace-multi-monitor/manifest.json b/streamspace-multi-monitor/manifest.json new file mode 100644 index 0000000..2174d97 --- /dev/null +++ b/streamspace-multi-monitor/manifest.json @@ -0,0 +1,32 @@ +{ + "name": "streamspace-multi-monitor", + "version": "1.0.0", + "displayName": "Multi-Monitor Support", + "description": "Advanced multi-monitor configuration for sessions with independent display streams and custom layouts", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Advanced Features", + "tags": ["multi-monitor", "displays", "vnc", "advanced"], + "requirements": {"streamspaceVersion": ">=1.0.0"}, + "entrypoints": {"main": "multi_monitor_plugin.go"}, + "configSchema": { + "type": "object", + "properties": { + "maxMonitorsPerSession": {"type": "number", "default": 8, "minimum": 1, "maximum": 16}, + "defaultLayout": {"type": "string", "enum": ["horizontal", "vertical", "grid", "custom"], "default": "horizontal"}, + "allowCustomLayouts": {"type": "boolean", "default": true} + } + }, + "defaultConfig": {"maxMonitorsPerSession": 8, "defaultLayout": "horizontal", "allowCustomLayouts": true}, + "permissions": ["database", "api"], + "apiEndpoints": [ + {"method": "POST", "path": "/sessions/:sessionId/monitors", "description": "Create monitor configuration"}, + {"method": "GET", "path": "/sessions/:sessionId/monitors", "description": "List monitor configurations"}, + {"method": "GET", "path": "/sessions/:sessionId/monitors/active", "description": "Get active configuration"}, + {"method": "PATCH", "path": "/sessions/:sessionId/monitors/:configId", "description": "Update configuration"}, + {"method": "DELETE", "path": "/sessions/:sessionId/monitors/:configId", "description": "Delete configuration"}, + {"method": "POST", "path": "/sessions/:sessionId/monitors/:configId/activate", "description": "Activate configuration"}, + {"method": "GET", "path": "/sessions/:sessionId/monitors/:configId/streams", "description": "Get monitor streams"} + ] +} diff --git a/streamspace-multi-monitor/multi_monitor_plugin.go b/streamspace-multi-monitor/multi_monitor_plugin.go new file mode 100644 index 0000000..b8e9c40 --- /dev/null +++ b/streamspace-multi-monitor/multi_monitor_plugin.go @@ -0,0 +1,35 @@ +package multimonitorplugin + +import ( + "github.com/streamspace/streamspace/api/internal/plugins" +) + +// MultiMonitorPlugin provides multi-monitor configuration support +type MultiMonitorPlugin struct { + plugins.BasePlugin +} + +// NewMultiMonitorPlugin creates a new multi-monitor plugin instance +func NewMultiMonitorPlugin() *MultiMonitorPlugin { + return &MultiMonitorPlugin{ + BasePlugin: plugins.BasePlugin{Name: "streamspace-multi-monitor"}, + } +} + +// OnLoad initializes the plugin +func (p *MultiMonitorPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Multi-Monitor plugin loading") + + // TODO: Extract monitor configuration logic from /api/internal/handlers/multimonitor.go + // TODO: Register API endpoints for monitor management + // TODO: Initialize database tables (monitor_configurations, monitor_displays) + + return nil +} + +// Auto-register plugin +func init() { + plugins.Register("streamspace-multi-monitor", func() plugins.Plugin { + return NewMultiMonitorPlugin() + }) +} diff --git a/streamspace-newrelic/README.md b/streamspace-newrelic/README.md new file mode 100644 index 0000000..8dd6cf8 --- /dev/null +++ b/streamspace-newrelic/README.md @@ -0,0 +1,219 @@ +# StreamSpace New Relic Plugin + +Full-stack observability integration with New Relic for metrics, events, traces, and logs. + +## Features + +- **Custom Metrics** - Send session, resource, and user metrics to New Relic +- **Custom Events** - Track session lifecycle and user activity events +- **APM Integration** - Distributed tracing for API requests +- **Real-time Monitoring** - Live dashboards and alerting +- **Flexible Configuration** - Track only what you need + +## Installation + +### Via Plugin Marketplace + +1. Navigate to **Admin → Plugins** +2. Search for "New Relic Monitoring" +3. Click **Install** and configure +4. Click **Enable** + +### Manual Installation + +```bash +cp -r streamspace-newrelic /path/to/streamspace/plugins/ +systemctl restart streamspace-api +``` + +## Configuration + +### Required Settings + +```json +{ + "enabled": true, + "licenseKey": "your-newrelic-license-key", + "accountId": "your-account-id", + "region": "US" +} +``` + +### Full Configuration + +```json +{ + "enabled": true, + "licenseKey": "NRII-...", + "accountId": "1234567", + "region": "US", + "appName": "StreamSpace Production", + "enableMetrics": true, + "enableEvents": true, + "enableTraces": true, + "enableLogs": false, + "metricsInterval": 60, + "trackSessionMetrics": true, + "trackResourceMetrics": true, + "trackUserMetrics": true, + "customAttributes": { + "environment": "production", + "datacenter": "us-east-1", + "team": "platform" + } +} +``` + +### Getting Your Keys + +1. Log into New Relic +2. Navigate to **Account Settings → API Keys** +3. Copy your **Ingest - License** key +4. Note your **Account ID** from the URL + +### Regions + +- **US**: `https://insights-collector.newrelic.com` +- **EU**: `https://insights-collector.eu01.nr-data.net` + +## Metrics + +| Metric | Type | Description | +|--------|------|-------------| +| `streamspace.session.created` | count | Sessions created | +| `streamspace.session.terminated` | count | Sessions terminated | +| `streamspace.session.active` | gauge | Active sessions | +| `streamspace.session.duration` | gauge | Session duration (seconds) | +| `streamspace.session.cpu` | gauge | CPU usage (%) | +| `streamspace.session.memory` | gauge | Memory usage (bytes) | +| `streamspace.session.storage` | gauge | Storage usage (bytes) | +| `streamspace.user.created` | count | Users created | +| `streamspace.user.login` | count | User logins | +| `streamspace.user.logout` | count | User logouts | + +## Events + +- **SessionCreated** - New session started +- **SessionTerminated** - Session ended +- **UserCreated** - New user registered +- **PluginLoaded** - Plugin activated +- **PluginUnloaded** - Plugin deactivated + +## Usage + +### Query Metrics (NRQL) + +```sql +-- Active sessions over time +SELECT average(streamspace.session.active) +FROM Metric +SINCE 1 hour ago +TIMESERIES + +-- Session duration by template +SELECT average(streamspace.session.duration) +FROM Metric +FACET template +SINCE 1 day ago + +-- CPU usage by session +SELECT max(streamspace.session.cpu) +FROM Metric +FACET sessionId +SINCE 30 minutes ago +``` + +### Query Events (NRQL) + +```sql +-- Recent session creations +SELECT * FROM SessionCreated +SINCE 1 hour ago + +-- User activity +SELECT count(*) FROM UserCreated, UserLogin +FACET eventType +SINCE 1 day ago + +-- Session duration histogram +SELECT histogram(duration, 100, 10) +FROM SessionTerminated +SINCE 1 day ago +``` + +### Create Dashboards + +1. Go to **Dashboards → Create dashboard** +2. Add widgets with NRQL queries +3. Set up auto-refresh intervals +4. Share with team + +### Create Alerts + +```sql +-- Alert: High active sessions +SELECT average(streamspace.session.active) +FROM Metric +WHERE appName = 'StreamSpace' + +Threshold: Alert when > 100 for 5 minutes +``` + +```sql +-- Alert: High CPU usage +SELECT max(streamspace.session.cpu) +FROM Metric + +Threshold: Alert when > 90 for 10 minutes +``` + +```sql +-- Alert: Long-running sessions +SELECT max(duration) FROM SessionTerminated + +Threshold: Alert when > 28800 (8 hours) +``` + +## Troubleshooting + +### Metrics not appearing + +- Verify license key and account ID +- Check region setting (US vs EU) +- Review logs: `tail -f /var/log/streamspace/plugins/newrelic.log` +- Wait 1-2 minutes for data to appear + +### Authentication errors + +- Regenerate license key in New Relic +- Ensure using **Ingest - License** key (not User API key) +- Check key hasn't been deleted or rotated + +### High data ingestion costs + +- Reduce `metricsInterval` (increase from 60 to 120+ seconds) +- Disable `trackResourceMetrics` if not needed +- Use fewer custom attributes +- Review New Relic pricing and data limits + +## Best Practices + +1. **Start Simple** - Enable basic session metrics first +2. **Use Custom Attributes** - Add environment, region, team tags +3. **Set Up Alerts** - Proactive monitoring prevents issues +4. **Create Dashboards** - Visualize trends before incidents +5. **Monitor Costs** - Track data ingestion to control New Relic bills + +## Support + +- GitHub: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- Docs: https://docs.streamspace.io/plugins/newrelic +- New Relic Docs: https://docs.newrelic.com/ + +## License + +MIT License + +## Version History + +- **1.0.0** (2025-01-15) - Initial release with metrics, events, and custom attributes diff --git a/streamspace-newrelic/manifest.json b/streamspace-newrelic/manifest.json new file mode 100644 index 0000000..4669edf --- /dev/null +++ b/streamspace-newrelic/manifest.json @@ -0,0 +1,140 @@ +{ + "name": "streamspace-newrelic", + "version": "1.0.0", + "displayName": "New Relic Monitoring", + "description": "Send performance metrics, traces, and events to New Relic for full-stack observability", + "author": "StreamSpace Team", + "license": "MIT", + "type": "integration", + "category": "Monitoring", + "tags": ["monitoring", "newrelic", "apm", "metrics", "observability"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "newrelic_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable New Relic Integration", + "description": "Enable sending metrics and events to New Relic", + "default": true + }, + "licenseKey": { + "type": "string", + "title": "New Relic License Key", + "description": "Your New Relic license key (Ingest - License)", + "format": "password" + }, + "accountId": { + "type": "string", + "title": "Account ID", + "description": "Your New Relic account ID" + }, + "region": { + "type": "string", + "title": "Data Center Region", + "description": "New Relic data center region", + "enum": ["US", "EU"], + "default": "US" + }, + "appName": { + "type": "string", + "title": "Application Name", + "description": "Application name in New Relic", + "default": "StreamSpace" + }, + "enableMetrics": { + "type": "boolean", + "title": "Enable Metrics", + "description": "Send custom metrics to New Relic", + "default": true + }, + "enableEvents": { + "type": "boolean", + "title": "Enable Custom Events", + "description": "Send custom events to New Relic", + "default": true + }, + "enableTraces": { + "type": "boolean", + "title": "Enable Distributed Tracing", + "description": "Send distributed traces to New Relic APM", + "default": true + }, + "enableLogs": { + "type": "boolean", + "title": "Enable Logs", + "description": "Send logs to New Relic Logs", + "default": false + }, + "metricsInterval": { + "type": "integer", + "title": "Metrics Interval (seconds)", + "description": "How often to send metrics to New Relic", + "default": 60, + "minimum": 5, + "maximum": 300 + }, + "trackSessionMetrics": { + "type": "boolean", + "title": "Track Session Metrics", + "description": "Track session lifecycle and duration", + "default": true + }, + "trackResourceMetrics": { + "type": "boolean", + "title": "Track Resource Metrics", + "description": "Track CPU, memory, and storage usage", + "default": true + }, + "trackUserMetrics": { + "type": "boolean", + "title": "Track User Metrics", + "description": "Track user activity and engagement", + "default": true + }, + "customAttributes": { + "type": "object", + "title": "Custom Attributes", + "description": "Custom attributes to add to all events and metrics", + "additionalProperties": { + "type": "string" + }, + "default": { + "environment": "production", + "service": "streamspace" + } + } + }, + "required": ["licenseKey", "accountId"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "session.heartbeat": "OnSessionHeartbeat", + "user.created": "OnUserCreated", + "user.login": "OnUserLogin", + "user.logout": "OnUserLogout" + }, + "scheduler": { + "jobs": [ + { + "name": "send-metrics", + "schedule": "*/1 * * * *", + "description": "Send metrics to New Relic" + } + ] + } +} diff --git a/streamspace-newrelic/newrelic_plugin.go b/streamspace-newrelic/newrelic_plugin.go new file mode 100644 index 0000000..a64eae3 --- /dev/null +++ b/streamspace-newrelic/newrelic_plugin.go @@ -0,0 +1,493 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/yourusername/streamspace/api/internal/plugins" +) + +// NewRelicPlugin sends metrics, events, and traces to New Relic +type NewRelicPlugin struct { + plugins.BasePlugin + config NewRelicConfig + httpClient *http.Client + metricsBuffer []NewRelicMetric + eventsBuffer []NewRelicEvent + bufferMutex sync.Mutex + sessionStart map[string]time.Time + sessionMutex sync.Mutex +} + +// NewRelicConfig holds New Relic configuration +type NewRelicConfig struct { + Enabled bool `json:"enabled"` + LicenseKey string `json:"licenseKey"` + AccountID string `json:"accountId"` + Region string `json:"region"` + AppName string `json:"appName"` + EnableMetrics bool `json:"enableMetrics"` + EnableEvents bool `json:"enableEvents"` + EnableTraces bool `json:"enableTraces"` + EnableLogs bool `json:"enableLogs"` + MetricsInterval int `json:"metricsInterval"` + TrackSessionMetrics bool `json:"trackSessionMetrics"` + TrackResourceMetrics bool `json:"trackResourceMetrics"` + TrackUserMetrics bool `json:"trackUserMetrics"` + CustomAttributes map[string]string `json:"customAttributes"` +} + +// NewRelicMetric represents a New Relic metric +type NewRelicMetric struct { + Name string `json:"name"` + Type string `json:"type"` + Value interface{} `json:"value"` + Timestamp int64 `json:"timestamp"` + Attributes map[string]interface{} `json:"attributes"` +} + +// NewRelicEvent represents a New Relic custom event +type NewRelicEvent struct { + EventType string `json:"eventType"` + Timestamp int64 `json:"timestamp"` + Attributes map[string]interface{} `json:"attributes"` +} + +// Initialize sets up the plugin +func (p *NewRelicPlugin) Initialize(ctx *plugins.PluginContext) error { + // Load configuration + configBytes, err := json.Marshal(ctx.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := json.Unmarshal(configBytes, &p.config); err != nil { + return fmt.Errorf("failed to unmarshal New Relic config: %w", err) + } + + if !p.config.Enabled { + ctx.Logger.Info("New Relic integration is disabled") + return nil + } + + if p.config.LicenseKey == "" { + return fmt.Errorf("New Relic license key is required") + } + + if p.config.Region == "" { + p.config.Region = "US" + } + + if p.config.AppName == "" { + p.config.AppName = "StreamSpace" + } + + // Initialize HTTP client + p.httpClient = &http.Client{ + Timeout: 10 * time.Second, + } + + // Initialize buffers + p.sessionStart = make(map[string]time.Time) + p.metricsBuffer = []NewRelicMetric{} + p.eventsBuffer = []NewRelicEvent{} + + ctx.Logger.Info("New Relic plugin initialized successfully", + "region", p.config.Region, + "app_name", p.config.AppName, + "metrics_enabled", p.config.EnableMetrics, + "events_enabled", p.config.EnableEvents, + ) + + return nil +} + +// OnLoad is called when the plugin is loaded +func (p *NewRelicPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("New Relic monitoring plugin loaded") + return p.sendEvent(ctx, "PluginLoaded", map[string]interface{}{ + "pluginName": "streamspace-newrelic", + "pluginVersion": "1.0.0", + "status": "active", + }) +} + +// OnUnload is called when the plugin is unloaded +func (p *NewRelicPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("New Relic monitoring plugin unloading") + + // Flush any remaining metrics and events + if err := p.flushMetrics(ctx); err != nil { + ctx.Logger.Error("Failed to flush metrics on unload", "error", err) + } + if err := p.flushEvents(ctx); err != nil { + ctx.Logger.Error("Failed to flush events on unload", "error", err) + } + + return p.sendEvent(ctx, "PluginUnloaded", map[string]interface{}{ + "pluginName": "streamspace-newrelic", + "status": "inactive", + }) +} + +// OnSessionCreated tracks session creation +func (p *NewRelicPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackSessionMetrics { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Track session start time + p.sessionMutex.Lock() + p.sessionStart[sessionID] = time.Now() + p.sessionMutex.Unlock() + + // Add metrics + attrs := p.getBaseAttributes() + attrs["userId"] = userID + attrs["template"] = templateName + attrs["sessionId"] = sessionID + + p.addMetric("streamspace.session.created", "count", 1, attrs) + p.addMetric("streamspace.session.active", "gauge", 1, attrs) + + // Add event + return p.sendEvent(ctx, "SessionCreated", map[string]interface{}{ + "sessionId": sessionID, + "userId": userID, + "template": templateName, + }) +} + +// OnSessionTerminated tracks session termination +func (p *NewRelicPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackSessionMetrics { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Calculate session duration + p.sessionMutex.Lock() + startTime, exists := p.sessionStart[sessionID] + duration := 0.0 + if exists { + duration = time.Since(startTime).Seconds() + delete(p.sessionStart, sessionID) + } + p.sessionMutex.Unlock() + + // Add metrics + attrs := p.getBaseAttributes() + attrs["userId"] = userID + attrs["template"] = templateName + attrs["sessionId"] = sessionID + + p.addMetric("streamspace.session.terminated", "count", 1, attrs) + p.addMetric("streamspace.session.duration", "gauge", duration, attrs) + + // Add event + return p.sendEvent(ctx, "SessionTerminated", map[string]interface{}{ + "sessionId": sessionID, + "userId": userID, + "template": templateName, + "duration": duration, + }) +} + +// OnSessionHeartbeat tracks session resource usage +func (p *NewRelicPlugin) OnSessionHeartbeat(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.TrackResourceMetrics { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return nil + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + attrs := p.getBaseAttributes() + attrs["sessionId"] = sessionID + attrs["userId"] = userID + attrs["template"] = templateName + + // Track resource usage + if cpuUsage, ok := sessionMap["cpu_usage"].(float64); ok { + p.addMetric("streamspace.session.cpu", "gauge", cpuUsage*100, attrs) + } + + if memoryUsage, ok := sessionMap["memory_usage"].(float64); ok { + p.addMetric("streamspace.session.memory", "gauge", memoryUsage, attrs) + } + + if storageUsage, ok := sessionMap["storage_usage"].(float64); ok { + p.addMetric("streamspace.session.storage", "gauge", storageUsage, attrs) + } + + return nil +} + +// OnUserCreated tracks user creation +func (p *NewRelicPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUserMetrics { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user format") + } + + userID := fmt.Sprintf("%v", userMap["id"]) + attrs := p.getBaseAttributes() + attrs["userId"] = userID + + p.addMetric("streamspace.user.created", "count", 1, attrs) + + return p.sendEvent(ctx, "UserCreated", map[string]interface{}{ + "userId": userID, + }) +} + +// OnUserLogin tracks user login +func (p *NewRelicPlugin) OnUserLogin(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUserMetrics { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", userMap["id"]) + attrs := p.getBaseAttributes() + attrs["userId"] = userID + + p.addMetric("streamspace.user.login", "count", 1, attrs) + + return nil +} + +// OnUserLogout tracks user logout +func (p *NewRelicPlugin) OnUserLogout(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled || !p.config.TrackUserMetrics { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return nil + } + + userID := fmt.Sprintf("%v", userMap["id"]) + attrs := p.getBaseAttributes() + attrs["userId"] = userID + + p.addMetric("streamspace.user.logout", "count", 1, attrs) + + return nil +} + +// RunScheduledJob handles the scheduled metrics/events flush +func (p *NewRelicPlugin) RunScheduledJob(ctx *plugins.PluginContext, jobName string) error { + if jobName == "send-metrics" { + if err := p.flushMetrics(ctx); err != nil { + ctx.Logger.Error("Failed to flush metrics", "error", err) + } + if err := p.flushEvents(ctx); err != nil { + ctx.Logger.Error("Failed to flush events", "error", err) + } + } + return nil +} + +// getBaseAttributes returns base attributes with custom attributes +func (p *NewRelicPlugin) getBaseAttributes() map[string]interface{} { + attrs := make(map[string]interface{}) + for k, v := range p.config.CustomAttributes { + attrs[k] = v + } + attrs["appName"] = p.config.AppName + return attrs +} + +// addMetric adds a metric to the buffer +func (p *NewRelicPlugin) addMetric(name, metricType string, value interface{}, attributes map[string]interface{}) { + p.bufferMutex.Lock() + defer p.bufferMutex.Unlock() + + metric := NewRelicMetric{ + Name: name, + Type: metricType, + Value: value, + Timestamp: time.Now().Unix(), + Attributes: attributes, + } + + p.metricsBuffer = append(p.metricsBuffer, metric) +} + +// sendEvent adds an event to the buffer +func (p *NewRelicPlugin) sendEvent(ctx *plugins.PluginContext, eventType string, attributes map[string]interface{}) error { + if !p.config.Enabled || !p.config.EnableEvents { + return nil + } + + // Merge with base attributes + allAttrs := p.getBaseAttributes() + for k, v := range attributes { + allAttrs[k] = v + } + + p.bufferMutex.Lock() + defer p.bufferMutex.Unlock() + + event := NewRelicEvent{ + EventType: eventType, + Timestamp: time.Now().Unix(), + Attributes: allAttrs, + } + + p.eventsBuffer = append(p.eventsBuffer, event) + return nil +} + +// flushMetrics sends buffered metrics to New Relic +func (p *NewRelicPlugin) flushMetrics(ctx *plugins.PluginContext) error { + if !p.config.Enabled || !p.config.EnableMetrics { + return nil + } + + p.bufferMutex.Lock() + if len(p.metricsBuffer) == 0 { + p.bufferMutex.Unlock() + return nil + } + + metrics := make([]NewRelicMetric, len(p.metricsBuffer)) + copy(metrics, p.metricsBuffer) + p.metricsBuffer = []NewRelicMetric{} + p.bufferMutex.Unlock() + + // Send to New Relic + payload := []map[string]interface{}{} + for _, m := range metrics { + payload = append(payload, map[string]interface{}{ + "name": m.Name, + "type": m.Type, + "value": m.Value, + "timestamp": m.Timestamp, + "attributes": m.Attributes, + }) + } + + return p.sendToNewRelic(ctx, "metrics", payload) +} + +// flushEvents sends buffered events to New Relic +func (p *NewRelicPlugin) flushEvents(ctx *plugins.PluginContext) error { + if !p.config.Enabled || !p.config.EnableEvents { + return nil + } + + p.bufferMutex.Lock() + if len(p.eventsBuffer) == 0 { + p.bufferMutex.Unlock() + return nil + } + + events := make([]NewRelicEvent, len(p.eventsBuffer)) + copy(events, p.eventsBuffer) + p.eventsBuffer = []NewRelicEvent{} + p.bufferMutex.Unlock() + + // Convert to payload format + payload := []map[string]interface{}{} + for _, e := range events { + eventMap := map[string]interface{}{ + "eventType": e.EventType, + "timestamp": e.Timestamp, + } + for k, v := range e.Attributes { + eventMap[k] = v + } + payload = append(payload, eventMap) + } + + return p.sendToNewRelic(ctx, "events", payload) +} + +// sendToNewRelic sends data to New Relic Insights API +func (p *NewRelicPlugin) sendToNewRelic(ctx *plugins.PluginContext, dataType string, payload interface{}) error { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal payload: %w", err) + } + + // Build URL based on region and data type + baseURL := "https://insights-collector.newrelic.com" + if p.config.Region == "EU" { + baseURL = "https://insights-collector.eu01.nr-data.net" + } + + endpoint := "v1/accounts/" + p.config.AccountID + if dataType == "events" { + endpoint += "/events" + } else { + endpoint += "/metrics" + } + + url := baseURL + "/" + endpoint + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Api-Key", p.config.LicenseKey) + + resp, err := p.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send to New Relic: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("New Relic API returned status %d: %s", resp.StatusCode, string(body)) + } + + ctx.Logger.Info("Sent data to New Relic", "type", dataType, "count", len(payload.([]map[string]interface{}))) + return nil +} + +// Export the plugin +func init() { + plugins.Register("streamspace-newrelic", &NewRelicPlugin{}) +} diff --git a/streamspace-node-manager/README.md b/streamspace-node-manager/README.md new file mode 100644 index 0000000..75f858c --- /dev/null +++ b/streamspace-node-manager/README.md @@ -0,0 +1,353 @@ +# StreamSpace Node Manager Plugin + +Advanced Kubernetes node management plugin for StreamSpace, providing comprehensive cluster infrastructure control. + +## Features + +- **Node Listing**: View all nodes in the cluster with detailed information +- **Cluster Statistics**: Get overall cluster resource utilization and health +- **Label Management**: Add and remove labels from nodes +- **Taint Management**: Configure node taints for pod scheduling control +- **Node Scheduling**: Cordon/uncordon nodes to control workload placement +- **Node Draining**: Safely drain pods from nodes for maintenance +- **Resource Metrics**: Real-time CPU and memory usage (requires metrics-server) +- **Health Monitoring**: Automated health checks with alerting +- **Auto-Scaling Support**: Configure thresholds for cluster autoscaling + +## Requirements + +- StreamSpace >= 1.0.0 +- Kubernetes >= 1.19.0 +- Kubernetes metrics-server (optional, for resource metrics) +- Cluster autoscaler (optional, for auto-scaling) + +## Installation + +1. **Via Plugin Marketplace** (Recommended): + ```bash + # Navigate to Admin > Plugins > Marketplace + # Search for "Node Manager" + # Click "Install" + ``` + +2. **Manual Installation**: + ```bash + # Copy plugin to plugins directory + cp -r streamspace-node-manager /path/to/streamspace/plugins/ + + # Restart StreamSpace API + kubectl rollout restart deployment/streamspace-api -n streamspace + ``` + +## Configuration + +### Basic Configuration + +```json +{ + "nodeSelectionStrategy": "least-sessions", + "healthCheckInterval": 60, + "metricsEnabled": true, + "alertOnNodeFailure": true +} +``` + +### Auto-Scaling Configuration + +```json +{ + "enableAutoScaling": true, + "minNodes": 1, + "maxNodes": 10, + "scaleUpThreshold": 80, + "scaleDownThreshold": 20 +} +``` + +## Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enableAutoScaling` | boolean | false | Enable automatic node scaling | +| `scaleUpThreshold` | number | 80 | CPU/Memory % to trigger scale up | +| `scaleDownThreshold` | number | 20 | CPU/Memory % to trigger scale down | +| `nodeSelectionStrategy` | string | "least-sessions" | Node selection algorithm | +| `healthCheckInterval` | number | 60 | Seconds between health checks | +| `metricsEnabled` | boolean | true | Enable resource metrics collection | +| `alertOnNodeFailure` | boolean | true | Alert when nodes become NotReady | +| `minNodes` | number | 1 | Minimum cluster nodes | +| `maxNodes` | number | 10 | Maximum cluster nodes | + +### Node Selection Strategies + +- **least-sessions**: Place workloads on nodes with fewest sessions +- **most-resources**: Place workloads on nodes with most available resources +- **random**: Random node selection +- **round-robin**: Distribute workloads evenly across nodes + +## API Endpoints + +All endpoints require `admin` permissions and are prefixed with `/api/plugins/streamspace-node-manager`. + +### List Nodes +```http +GET /nodes +``` + +**Response**: +```json +[ + { + "name": "node-1", + "labels": {"role": "worker"}, + "taints": [], + "status": {"ready": true, "phase": "Ready"}, + "capacity": {"cpu": "4", "memory": "16Gi"}, + "allocatable": {"cpu": "3.8", "memory": "15Gi"}, + "usage": {"cpu_percent": 45.2, "memory_percent": 62.1}, + "pods": 12, + "age": "5d3h" + } +] +``` + +### Get Node Details +```http +GET /nodes/:name +``` + +### Get Cluster Statistics +```http +GET /nodes/stats +``` + +**Response**: +```json +{ + "total_nodes": 3, + "ready_nodes": 3, + "not_ready_nodes": 0, + "total_capacity": {"cpu": "12", "memory": "48Gi"}, + "total_allocatable": {"cpu": "11.4", "memory": "45Gi"} +} +``` + +### Add Label to Node +```http +PUT /nodes/:name/labels +Content-Type: application/json + +{ + "key": "environment", + "value": "production" +} +``` + +### Remove Label from Node +```http +DELETE /nodes/:name/labels/:key +``` + +### Add Taint to Node +```http +POST /nodes/:name/taints +Content-Type: application/json + +{ + "key": "dedicated", + "value": "gpu-workloads", + "effect": "NoSchedule" +} +``` + +**Taint Effects**: +- `NoSchedule`: Don't schedule new pods +- `PreferNoSchedule`: Avoid scheduling new pods +- `NoExecute`: Evict existing pods + +### Remove Taint from Node +```http +DELETE /nodes/:name/taints/:key +``` + +### Cordon Node (Mark Unschedulable) +```http +POST /nodes/:name/cordon +``` + +### Uncordon Node (Mark Schedulable) +```http +POST /nodes/:name/uncordon +``` + +### Drain Node +```http +POST /nodes/:name/drain +Content-Type: application/json + +{ + "grace_period_seconds": 30 +} +``` + +**Response**: +```json +{ + "message": "Node drained successfully", + "pods_deleted": 8 +} +``` + +## Permissions + +This plugin requires the following Kubernetes RBAC permissions: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: streamspace-node-manager +rules: +- apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list", "update", "patch"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "delete"] +- apiGroups: ["metrics.k8s.io"] + resources: ["nodes"] + verbs: ["get", "list"] +``` + +## Admin UI + +The plugin adds the following to the admin panel: + +### Pages +- **Node Management** (`/admin/nodes`): Full node management interface + +### Dashboard Widgets +- **Cluster Health**: Overview of node status +- **Node Resources**: Resource utilization graphs + +## Use Cases + +### 1. Cluster Maintenance +```bash +# Cordon node for maintenance +POST /api/plugins/streamspace-node-manager/nodes/worker-1/cordon + +# Drain all pods +POST /api/plugins/streamspace-node-manager/nodes/worker-1/drain + +# Perform maintenance... + +# Uncordon node +POST /api/plugins/streamspace-node-manager/nodes/worker-1/uncordon +``` + +### 2. Dedicated Node Pools +```bash +# Taint GPU nodes +POST /api/plugins/streamspace-node-manager/nodes/gpu-node-1/taints +{ + "key": "nvidia.com/gpu", + "value": "true", + "effect": "NoSchedule" +} + +# Label GPU nodes +PUT /api/plugins/streamspace-node-manager/nodes/gpu-node-1/labels +{ + "key": "accelerator", + "value": "nvidia-tesla-t4" +} +``` + +### 3. Environment Segregation +```bash +# Label production nodes +PUT /api/plugins/streamspace-node-manager/nodes/prod-1/labels +{ + "key": "environment", + "value": "production" +} + +# Taint to prevent non-production workloads +POST /api/plugins/streamspace-node-manager/nodes/prod-1/taints +{ + "key": "environment", + "value": "production", + "effect": "NoSchedule" +} +``` + +## Troubleshooting + +### Metrics Not Available +**Problem**: Node usage metrics not showing + +**Solution**: Install Kubernetes metrics-server +```bash +kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml +``` + +### Permission Denied Errors +**Problem**: Plugin cannot access Kubernetes API + +**Solution**: Ensure StreamSpace has proper RBAC: +```bash +kubectl apply -f streamspace-node-manager-rbac.yaml +``` + +### Auto-Scaling Not Working +**Problem**: Nodes not scaling automatically + +**Solution**: +1. Ensure cluster autoscaler is installed +2. Verify `enableAutoScaling` is true in config +3. Check logs: `kubectl logs -l app=streamspace-api -f | grep node-manager` + +## Best Practices + +1. **Always drain nodes before maintenance** to avoid disrupting sessions +2. **Use taints for specialized workloads** (GPU, high-memory, etc.) +3. **Monitor cluster health regularly** via the dashboard widgets +4. **Set appropriate min/max nodes** based on workload patterns +5. **Use labels for organization** (environment, region, instance-type) + +## Uninstallation + +```bash +# Via UI: Admin > Plugins > Installed > Node Manager > Uninstall + +# Or via API: +DELETE /api/plugins/streamspace-node-manager +``` + +**Note**: Uninstalling this plugin will not affect your nodes or their configurations. All node labels and taints will remain. + +## Support + +- **Issues**: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- **Documentation**: https://docs.streamspace.io/plugins/node-manager +- **Community**: https://discord.gg/streamspace + +## License + +MIT License - See LICENSE file for details + +## Author + +StreamSpace Team + +## Changelog + +### 1.0.0 (2025-11-16) +- Initial release +- Node listing and details +- Label and taint management +- Cordon/uncordon/drain operations +- Resource metrics support +- Health monitoring +- Auto-scaling support diff --git a/streamspace-node-manager/manifest.json b/streamspace-node-manager/manifest.json new file mode 100644 index 0000000..9418d50 --- /dev/null +++ b/streamspace-node-manager/manifest.json @@ -0,0 +1,204 @@ +{ + "name": "streamspace-node-manager", + "version": "1.0.0", + "displayName": "Kubernetes Node Manager", + "description": "Advanced Kubernetes node management including labels, taints, cordon/uncordon, drain operations, and cluster statistics", + "author": "StreamSpace Team", + "license": "MIT", + "homepage": "https://github.com/JoshuaAFerguson/streamspace-plugins/tree/main/streamspace-node-manager", + "repository": "https://github.com/JoshuaAFerguson/streamspace-plugins", + "icon": "node-manager-icon.png", + "type": "extension", + "category": "Infrastructure", + "tags": ["kubernetes", "nodes", "infrastructure", "cluster-management", "admin"], + + "requirements": { + "streamspaceVersion": ">=1.0.0", + "kubernetes": ">=1.19.0" + }, + + "entrypoints": { + "main": "node_manager_plugin.go" + }, + + "configSchema": { + "type": "object", + "properties": { + "enableAutoScaling": { + "type": "boolean", + "title": "Enable Auto-Scaling", + "description": "Automatically scale nodes based on resource usage (requires cluster autoscaler)", + "default": false + }, + "scaleUpThreshold": { + "type": "number", + "title": "Scale Up Threshold (%)", + "description": "CPU/Memory usage threshold to trigger scale up", + "minimum": 50, + "maximum": 95, + "default": 80 + }, + "scaleDownThreshold": { + "type": "number", + "title": "Scale Down Threshold (%)", + "description": "CPU/Memory usage threshold to trigger scale down", + "minimum": 5, + "maximum": 50, + "default": 20 + }, + "nodeSelectionStrategy": { + "type": "string", + "title": "Node Selection Strategy", + "description": "Strategy for selecting nodes for session placement", + "enum": ["least-sessions", "most-resources", "random", "round-robin"], + "default": "least-sessions" + }, + "healthCheckInterval": { + "type": "number", + "title": "Health Check Interval (seconds)", + "description": "How often to check node health status", + "minimum": 30, + "maximum": 600, + "default": 60 + }, + "metricsEnabled": { + "type": "boolean", + "title": "Enable Metrics", + "description": "Collect and display node resource usage metrics (requires metrics-server)", + "default": true + }, + "alertOnNodeFailure": { + "type": "boolean", + "title": "Alert on Node Failure", + "description": "Send alerts when nodes become NotReady", + "default": true + }, + "minNodes": { + "type": "number", + "title": "Minimum Nodes", + "description": "Minimum number of nodes to maintain in cluster", + "minimum": 1, + "maximum": 100, + "default": 1 + }, + "maxNodes": { + "type": "number", + "title": "Maximum Nodes", + "description": "Maximum number of nodes allowed in cluster", + "minimum": 1, + "maximum": 1000, + "default": 10 + } + } + }, + + "defaultConfig": { + "enableAutoScaling": false, + "scaleUpThreshold": 80, + "scaleDownThreshold": 20, + "nodeSelectionStrategy": "least-sessions", + "healthCheckInterval": 60, + "metricsEnabled": true, + "alertOnNodeFailure": true, + "minNodes": 1, + "maxNodes": 10 + }, + + "permissions": [ + "kubernetes", + "admin_ui", + "api", + "scheduler" + ], + + "adminUI": { + "pages": [ + { + "id": "node-management", + "title": "Node Management", + "icon": "server", + "path": "/admin/nodes", + "description": "Manage Kubernetes nodes" + } + ], + "widgets": [ + { + "id": "cluster-health", + "title": "Cluster Health", + "description": "Overview of cluster node status", + "position": "top", + "width": "half" + }, + { + "id": "node-resources", + "title": "Node Resources", + "description": "Cluster resource utilization", + "position": "top", + "width": "half" + } + ] + }, + + "apiEndpoints": [ + { + "method": "GET", + "path": "/nodes", + "description": "List all Kubernetes nodes", + "permissions": ["admin"] + }, + { + "method": "GET", + "path": "/nodes/stats", + "description": "Get cluster statistics", + "permissions": ["admin"] + }, + { + "method": "GET", + "path": "/nodes/:name", + "description": "Get node details", + "permissions": ["admin"] + }, + { + "method": "PUT", + "path": "/nodes/:name/labels", + "description": "Add label to node", + "permissions": ["admin"] + }, + { + "method": "DELETE", + "path": "/nodes/:name/labels/:key", + "description": "Remove label from node", + "permissions": ["admin"] + }, + { + "method": "POST", + "path": "/nodes/:name/taints", + "description": "Add taint to node", + "permissions": ["admin"] + }, + { + "method": "DELETE", + "path": "/nodes/:name/taints/:key", + "description": "Remove taint from node", + "permissions": ["admin"] + }, + { + "method": "POST", + "path": "/nodes/:name/cordon", + "description": "Mark node as unschedulable", + "permissions": ["admin"] + }, + { + "method": "POST", + "path": "/nodes/:name/uncordon", + "description": "Mark node as schedulable", + "permissions": ["admin"] + }, + { + "method": "POST", + "path": "/nodes/:name/drain", + "description": "Drain all pods from node", + "permissions": ["admin"] + } + ] +} diff --git a/streamspace-node-manager/node_manager_plugin.go b/streamspace-node-manager/node_manager_plugin.go new file mode 100644 index 0000000..77583b2 --- /dev/null +++ b/streamspace-node-manager/node_manager_plugin.go @@ -0,0 +1,574 @@ +package nodemanagerplugin + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/streamspace/streamspace/api/internal/plugins" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + metricsv "k8s.io/metrics/pkg/apis/metrics/v1beta1" + metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned" +) + +// NodeManagerPlugin implements Kubernetes node management +type NodeManagerPlugin struct { + plugins.BasePlugin + clientset *kubernetes.Clientset + metricsClientset *metricsclientset.Clientset +} + +// NewNodeManagerPlugin creates a new node manager plugin instance +func NewNodeManagerPlugin() *NodeManagerPlugin { + return &NodeManagerPlugin{ + BasePlugin: plugins.BasePlugin{Name: "streamspace-node-manager"}, + } +} + +// OnLoad is called when the plugin is loaded +func (p *NodeManagerPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Node Manager plugin loading", map[string]interface{}{ + "version": "1.0.0", + }) + + // Initialize Kubernetes client + config, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("failed to get in-cluster config: %w", err) + } + + p.clientset, err = kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create kubernetes clientset: %w", err) + } + + // Try to initialize metrics client (optional) + p.metricsClientset, err = metricsclientset.NewForConfig(config) + if err != nil { + ctx.Logger.Warn("Failed to create metrics clientset, metrics will be unavailable", map[string]interface{}{ + "error": err.Error(), + }) + } + + // Register API endpoints + p.registerEndpoints(ctx) + + // Start health check scheduler if enabled + healthCheckInterval, _ := ctx.Config["healthCheckInterval"].(float64) + if healthCheckInterval > 0 { + ctx.Scheduler.Schedule(fmt.Sprintf("@every %ds", int(healthCheckInterval)), func() { + p.checkNodeHealth(ctx) + }) + } + + ctx.Logger.Info("Node Manager plugin loaded successfully") + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *NodeManagerPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Node Manager plugin unloading") + return nil +} + +// registerEndpoints registers all API endpoints +func (p *NodeManagerPlugin) registerEndpoints(ctx *plugins.PluginContext) { + // GET /api/plugins/streamspace-node-manager/nodes + ctx.APIRegistry.RegisterEndpoint("GET", "/nodes", p.listNodes) + + // GET /api/plugins/streamspace-node-manager/nodes/stats + ctx.APIRegistry.RegisterEndpoint("GET", "/nodes/stats", p.getClusterStats) + + // GET /api/plugins/streamspace-node-manager/nodes/:name + ctx.APIRegistry.RegisterEndpoint("GET", "/nodes/:name", p.getNode) + + // PUT /api/plugins/streamspace-node-manager/nodes/:name/labels + ctx.APIRegistry.RegisterEndpoint("PUT", "/nodes/:name/labels", p.addLabel) + + // DELETE /api/plugins/streamspace-node-manager/nodes/:name/labels/:key + ctx.APIRegistry.RegisterEndpoint("DELETE", "/nodes/:name/labels/:key", p.removeLabel) + + // POST /api/plugins/streamspace-node-manager/nodes/:name/taints + ctx.APIRegistry.RegisterEndpoint("POST", "/nodes/:name/taints", p.addTaint) + + // DELETE /api/plugins/streamspace-node-manager/nodes/:name/taints/:key + ctx.APIRegistry.RegisterEndpoint("DELETE", "/nodes/:name/taints/:key", p.removeTaint) + + // POST /api/plugins/streamspace-node-manager/nodes/:name/cordon + ctx.APIRegistry.RegisterEndpoint("POST", "/nodes/:name/cordon", p.cordonNode) + + // POST /api/plugins/streamspace-node-manager/nodes/:name/uncordon + ctx.APIRegistry.RegisterEndpoint("POST", "/nodes/:name/uncordon", p.uncordonNode) + + // POST /api/plugins/streamspace-node-manager/nodes/:name/drain + ctx.APIRegistry.RegisterEndpoint("POST", "/nodes/:name/drain", p.drainNode) +} + +// API Handlers + +func (p *NodeManagerPlugin) listNodes(c *gin.Context) { + nodes, err := p.clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list nodes", "message": err.Error()}) + return + } + + // Get metrics if available + var metricsMap map[string]*v1beta1.NodeMetrics + if p.metricsClientset != nil { + nodeMetrics, err := p.metricsClientset.MetricsV1beta1().NodeMetricses().List(context.Background(), metav1.ListOptions{}) + if err == nil { + metricsMap = make(map[string]*v1beta1.NodeMetrics) + for i := range nodeMetrics.Items { + metricsMap[nodeMetrics.Items[i].Name] = &nodeMetrics.Items[i] + } + } + } + + // Get pod count per node + pods, err := p.clientset.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list pods"}) + return + } + + podCountMap := make(map[string]int) + for _, pod := range pods.Items { + if pod.Spec.NodeName != "" { + podCountMap[pod.Spec.NodeName]++ + } + } + + // Convert to response format + result := make([]map[string]interface{}, 0, len(nodes.Items)) + for _, node := range nodes.Items { + nodeInfo := p.convertNodeToInfo(&node) + nodeInfo["pods"] = podCountMap[node.Name] + + if metrics, ok := metricsMap[node.Name]; ok { + nodeInfo["usage"] = p.calculateUsage(&node, metrics) + } + + result = append(result, nodeInfo) + } + + c.JSON(http.StatusOK, result) +} + +func (p *NodeManagerPlugin) getNode(c *gin.Context) { + nodeName := c.Param("name") + + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found", "message": err.Error()}) + return + } + + nodeInfo := p.convertNodeToInfo(node) + + // Get metrics if available + if p.metricsClientset != nil { + metrics, err := p.metricsClientset.MetricsV1beta1().NodeMetricses().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err == nil { + nodeInfo["usage"] = p.calculateUsage(node, metrics) + } + } + + // Get pod count + pods, err := p.clientset.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("spec.nodeName=%s", nodeName), + }) + if err == nil { + nodeInfo["pods"] = len(pods.Items) + } + + c.JSON(http.StatusOK, nodeInfo) +} + +func (p *NodeManagerPlugin) getClusterStats(c *gin.Context) { + nodes, err := p.clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list nodes"}) + return + } + + stats := map[string]interface{}{ + "total_nodes": len(nodes.Items), + "ready_nodes": 0, + "not_ready_nodes": 0, + "total_pods": 0, + } + + var totalCPUCap, totalMemCap, totalCPUAlloc, totalMemAlloc resource.Quantity + + for _, node := range nodes.Items { + // Count ready nodes + for _, cond := range node.Status.Conditions { + if cond.Type == corev1.NodeReady && cond.Status == corev1.ConditionTrue { + stats["ready_nodes"] = stats["ready_nodes"].(int) + 1 + } + } + + // Sum resources + cpuCap := node.Status.Capacity.Cpu() + memCap := node.Status.Capacity.Memory() + cpuAlloc := node.Status.Allocatable.Cpu() + memAlloc := node.Status.Allocatable.Memory() + + totalCPUCap.Add(*cpuCap) + totalMemCap.Add(*memCap) + totalCPUAlloc.Add(*cpuAlloc) + totalMemAlloc.Add(*memAlloc) + } + + stats["not_ready_nodes"] = len(nodes.Items) - stats["ready_nodes"].(int) + stats["total_capacity"] = map[string]string{ + "cpu": totalCPUCap.String(), + "memory": totalMemCap.String(), + } + stats["total_allocatable"] = map[string]string{ + "cpu": totalCPUAlloc.String(), + "memory": totalMemAlloc.String(), + } + + c.JSON(http.StatusOK, stats) +} + +func (p *NodeManagerPlugin) addLabel(c *gin.Context) { + nodeName := c.Param("name") + var req struct { + Key string `json:"key" binding:"required"` + Value string `json:"value" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"}) + return + } + + if node.Labels == nil { + node.Labels = make(map[string]string) + } + node.Labels[req.Key] = req.Value + + _, err = p.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update node"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Label added successfully"}) +} + +func (p *NodeManagerPlugin) removeLabel(c *gin.Context) { + nodeName := c.Param("name") + labelKey := c.Param("key") + + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"}) + return + } + + if node.Labels != nil { + delete(node.Labels, labelKey) + } + + _, err = p.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update node"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Label removed successfully"}) +} + +func (p *NodeManagerPlugin) addTaint(c *gin.Context) { + nodeName := c.Param("name") + var taint struct { + Key string `json:"key" binding:"required"` + Value string `json:"value"` + Effect string `json:"effect" binding:"required"` + } + if err := c.ShouldBindJSON(&taint); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"}) + return + } + + // Check if taint already exists and update, or add new + found := false + for i, t := range node.Spec.Taints { + if t.Key == taint.Key { + node.Spec.Taints[i].Value = taint.Value + node.Spec.Taints[i].Effect = corev1.TaintEffect(taint.Effect) + found = true + break + } + } + + if !found { + node.Spec.Taints = append(node.Spec.Taints, corev1.Taint{ + Key: taint.Key, + Value: taint.Value, + Effect: corev1.TaintEffect(taint.Effect), + }) + } + + _, err = p.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update node"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Taint added successfully"}) +} + +func (p *NodeManagerPlugin) removeTaint(c *gin.Context) { + nodeName := c.Param("name") + taintKey := c.Param("key") + + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"}) + return + } + + newTaints := []corev1.Taint{} + for _, t := range node.Spec.Taints { + if t.Key != taintKey { + newTaints = append(newTaints, t) + } + } + node.Spec.Taints = newTaints + + _, err = p.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update node"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Taint removed successfully"}) +} + +func (p *NodeManagerPlugin) cordonNode(c *gin.Context) { + nodeName := c.Param("name") + + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"}) + return + } + + node.Spec.Unschedulable = true + + _, err = p.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cordon node"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Node cordoned successfully"}) +} + +func (p *NodeManagerPlugin) uncordonNode(c *gin.Context) { + nodeName := c.Param("name") + + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"}) + return + } + + node.Spec.Unschedulable = false + + _, err = p.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to uncordon node"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Node uncordoned successfully"}) +} + +func (p *NodeManagerPlugin) drainNode(c *gin.Context) { + nodeName := c.Param("name") + var req struct { + GracePeriodSeconds int64 `json:"grace_period_seconds"` + } + if err := c.ShouldBindJSON(&req); err != nil { + req.GracePeriodSeconds = 30 // Default + } + + // First cordon the node + node, err := p.clientset.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{}) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Node not found"}) + return + } + + node.Spec.Unschedulable = true + _, err = p.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cordon node"}) + return + } + + // Get all pods on the node + pods, err := p.clientset.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("spec.nodeName=%s", nodeName), + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list pods"}) + return + } + + // Delete each pod (skip DaemonSet pods) + deleted := 0 + for _, pod := range pods.Items { + isDaemonSet := false + if pod.OwnerReferences != nil { + for _, owner := range pod.OwnerReferences { + if owner.Kind == "DaemonSet" { + isDaemonSet = true + break + } + } + } + + if !isDaemonSet { + err := p.clientset.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{ + GracePeriodSeconds: &req.GracePeriodSeconds, + }) + if err == nil { + deleted++ + } + } + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Node drained successfully", + "pods_deleted": deleted, + }) +} + +// Helper functions + +func (p *NodeManagerPlugin) convertNodeToInfo(node *corev1.Node) map[string]interface{} { + taints := make([]map[string]string, len(node.Spec.Taints)) + for i, t := range node.Spec.Taints { + taints[i] = map[string]string{ + "key": t.Key, + "value": t.Value, + "effect": string(t.Effect), + } + } + + ready := false + for _, cond := range node.Status.Conditions { + if cond.Type == corev1.NodeReady && cond.Status == corev1.ConditionTrue { + ready = true + break + } + } + + return map[string]interface{}{ + "name": node.Name, + "labels": node.Labels, + "taints": taints, + "status": map[string]interface{}{ + "ready": ready, + "phase": map[bool]string{true: "Ready", false: "NotReady"}[ready], + }, + "capacity": map[string]string{ + "cpu": node.Status.Capacity.Cpu().String(), + "memory": node.Status.Capacity.Memory().String(), + "pods": node.Status.Capacity.Pods().String(), + }, + "allocatable": map[string]string{ + "cpu": node.Status.Allocatable.Cpu().String(), + "memory": node.Status.Allocatable.Memory().String(), + "pods": node.Status.Allocatable.Pods().String(), + }, + "info": map[string]string{ + "architecture": node.Status.NodeInfo.Architecture, + "os_image": node.Status.NodeInfo.OSImage, + "kernel_version": node.Status.NodeInfo.KernelVersion, + "kubelet_version": node.Status.NodeInfo.KubeletVersion, + "container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion, + }, + "age": time.Since(node.CreationTimestamp.Time).Round(time.Second).String(), + } +} + +func (p *NodeManagerPlugin) calculateUsage(node *corev1.Node, metrics *v1beta1.NodeMetrics) map[string]interface{} { + cpuUsage := metrics.Usage.Cpu() + memUsage := metrics.Usage.Memory() + + cpuCap := node.Status.Capacity.Cpu() + memCap := node.Status.Capacity.Memory() + + cpuPercent := float64(cpuUsage.MilliValue()) / float64(cpuCap.MilliValue()) * 100 + memPercent := float64(memUsage.Value()) / float64(memCap.Value()) * 100 + + return map[string]interface{}{ + "cpu": cpuUsage.String(), + "memory": memUsage.String(), + "cpu_percent": cpuPercent, + "memory_percent": memPercent, + } +} + +func (p *NodeManagerPlugin) checkNodeHealth(ctx *plugins.PluginContext) { + alertOnFailure, _ := ctx.Config["alertOnNodeFailure"].(bool) + if !alertOnFailure { + return + } + + nodes, err := p.clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) + if err != nil { + ctx.Logger.Error("Failed to check node health", map[string]interface{}{"error": err.Error()}) + return + } + + for _, node := range nodes.Items { + ready := false + for _, cond := range node.Status.Conditions { + if cond.Type == corev1.NodeReady && cond.Status == corev1.ConditionTrue { + ready = true + break + } + } + + if !ready { + ctx.Logger.Warn("Node is not ready", map[string]interface{}{ + "node": node.Name, + }) + // Could emit an event here for other plugins to handle alerts + } + } +} + +// Auto-register plugin +func init() { + plugins.Register("streamspace-node-manager", func() plugins.Plugin { + return NewNodeManagerPlugin() + }) +} diff --git a/streamspace-pagerduty/README.md b/streamspace-pagerduty/README.md new file mode 100644 index 0000000..cb648ac --- /dev/null +++ b/streamspace-pagerduty/README.md @@ -0,0 +1,243 @@ +# StreamSpace PagerDuty Integration Plugin + +Send incident alerts to PagerDuty for critical StreamSpace events with configurable severity levels. + +## Features + +- Incident alerting for session and user events +- Configurable severity levels (info, warning, error, critical) +- PagerDuty Events API v2 integration +- Auto-resolve capability for informational alerts +- Rate limiting to prevent alert fatigue +- Detailed custom fields with resource information +- Event deduplication support + +## Installation + +### Via StreamSpace UI + +1. Navigate to **Admin** → **Plugins** +2. Search for "PagerDuty Integration" +3. Click **Install** +4. Configure your PagerDuty integration key +5. Enable the plugin + +### Via kubectl + +```bash +kubectl apply -f - <=1.0.0" + }, + + "entrypoints": { + "main": "pagerduty_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "routingKey": { + "type": "string", + "title": "Integration Key (Routing Key)", + "description": "Your PagerDuty Events API v2 integration key", + "pattern": "^[a-zA-Z0-9]{32}$" + }, + "notifyOnSessionCreated": { + "type": "boolean", + "title": "Notify on Session Created", + "description": "Send alert when a session is created", + "default": false + }, + "notifyOnSessionHibernated": { + "type": "boolean", + "title": "Notify on Session Hibernated", + "description": "Send alert when a session is hibernated", + "default": true + }, + "notifyOnUserCreated": { + "type": "boolean", + "title": "Notify on User Created", + "description": "Send alert when a new user is created", + "default": false + }, + "sessionCreatedSeverity": { + "type": "string", + "title": "Session Created Severity", + "description": "Severity level for session created events", + "enum": ["info", "warning", "error", "critical"], + "default": "info" + }, + "sessionHibernatedSeverity": { + "type": "string", + "title": "Session Hibernated Severity", + "description": "Severity level for session hibernated events", + "enum": ["info", "warning", "error", "critical"], + "default": "warning" + }, + "userCreatedSeverity": { + "type": "string", + "title": "User Created Severity", + "description": "Severity level for user created events", + "enum": ["info", "warning", "error", "critical"], + "default": "info" + }, + "includeDetails": { + "type": "boolean", + "title": "Include Resource Details", + "description": "Include CPU and memory information in custom details", + "default": true + }, + "autoResolve": { + "type": "boolean", + "title": "Auto-Resolve Events", + "description": "Automatically resolve events after sending (useful for informational alerts)", + "default": false + }, + "rateLimit": { + "type": "number", + "title": "Rate Limit (events per hour)", + "description": "Maximum number of events to send per hour", + "default": 50, + "minimum": 1, + "maximum": 200 + } + }, + "required": ["routingKey"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.hibernated": "OnSessionHibernated", + "user.created": "OnUserCreated" + } +} diff --git a/streamspace-pagerduty/pagerduty_plugin.go b/streamspace-pagerduty/pagerduty_plugin.go new file mode 100644 index 0000000..cafb623 --- /dev/null +++ b/streamspace-pagerduty/pagerduty_plugin.go @@ -0,0 +1,402 @@ +package pagerdutyplugin + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/streamspace/streamspace/api/internal/plugins" +) + +// PagerDutyPlugin implements PagerDuty incident alerting integration +type PagerDutyPlugin struct { + plugins.BasePlugin + + // Rate limiting + eventCount int + lastReset time.Time +} + +// PagerDutyEvent represents a PagerDuty Events API v2 event +type PagerDutyEvent struct { + RoutingKey string `json:"routing_key"` + EventAction string `json:"event_action"` // trigger, acknowledge, resolve + DedupKey string `json:"dedup_key,omitempty"` + Payload PagerDutyPayload `json:"payload"` + Links []PagerDutyLink `json:"links,omitempty"` + Images []PagerDutyImage `json:"images,omitempty"` +} + +// PagerDutyPayload represents the event payload +type PagerDutyPayload struct { + Summary string `json:"summary"` + Source string `json:"source"` + Severity string `json:"severity"` // info, warning, error, critical + Timestamp string `json:"timestamp,omitempty"` + Component string `json:"component,omitempty"` + Group string `json:"group,omitempty"` + Class string `json:"class,omitempty"` + CustomDetails map[string]interface{} `json:"custom_details,omitempty"` +} + +// PagerDutyLink represents a link in the event +type PagerDutyLink struct { + Href string `json:"href"` + Text string `json:"text"` +} + +// PagerDutyImage represents an image in the event +type PagerDutyImage struct { + Src string `json:"src"` + Href string `json:"href,omitempty"` + Alt string `json:"alt,omitempty"` +} + +// PagerDuty Events API endpoint +const pagerDutyEventsURL = "https://events.pagerduty.com/v2/enqueue" + +// NewPagerDutyPlugin creates a new PagerDuty plugin instance +func NewPagerDutyPlugin() *PagerDutyPlugin { + return &PagerDutyPlugin{ + BasePlugin: plugins.BasePlugin{Name: "streamspace-pagerduty"}, + lastReset: time.Now(), + } +} + +// OnLoad is called when the plugin is loaded +func (p *PagerDutyPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("PagerDuty plugin loading", map[string]interface{}{ + "version": "1.0.0", + "config": ctx.Config, + }) + + // Validate configuration + routingKey, ok := ctx.Config["routingKey"].(string) + if !ok || routingKey == "" { + return fmt.Errorf("pagerduty routing key is required") + } + + // Test integration connectivity + if err := p.testIntegration(ctx, routingKey); err != nil { + ctx.Logger.Warn("Failed to test PagerDuty integration", map[string]interface{}{ + "error": err.Error(), + }) + // Don't fail on test error - PagerDuty might rate limit or have issues + } + + ctx.Logger.Info("PagerDuty plugin loaded successfully") + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *PagerDutyPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("PagerDuty plugin unloading") + return nil +} + +// OnSessionCreated is called when a session is created +func (p *PagerDutyPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + notify, _ := ctx.Config["notifyOnSessionCreated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + ctx.Logger.Warn("Rate limit exceeded, skipping PagerDuty event") + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + user := p.getString(sessionMap, "user") + template := p.getString(sessionMap, "template") + sessionID := p.getString(sessionMap, "id") + + // Build custom details + customDetails := map[string]interface{}{ + "user": user, + "template": template, + "sessionId": sessionID, + "eventType": "session.created", + } + + // Include resource details if configured + if p.getBool(ctx.Config, "includeDetails") { + if resources, ok := sessionMap["resources"].(map[string]interface{}); ok { + customDetails["memory"] = p.getString(resources, "memory") + customDetails["cpu"] = p.getString(resources, "cpu") + } + } + + severity := p.getString(ctx.Config, "sessionCreatedSeverity") + if severity == "" { + severity = "info" + } + + event := PagerDutyEvent{ + RoutingKey: p.getString(ctx.Config, "routingKey"), + EventAction: "trigger", + DedupKey: fmt.Sprintf("streamspace-session-%s", sessionID), + Payload: PagerDutyPayload{ + Summary: fmt.Sprintf("StreamSpace Session Created: %s by %s", template, user), + Source: "streamspace", + Severity: severity, + Timestamp: time.Now().Format(time.RFC3339), + Component: "sessions", + Class: "session.created", + CustomDetails: customDetails, + }, + } + + if err := p.sendEvent(ctx, event); err != nil { + return err + } + + // Auto-resolve if configured + if p.getBool(ctx.Config, "autoResolve") { + return p.resolveEvent(ctx, event.DedupKey) + } + + return nil +} + +// OnSessionHibernated is called when a session is hibernated +func (p *PagerDutyPlugin) OnSessionHibernated(ctx *plugins.PluginContext, session interface{}) error { + notify, _ := ctx.Config["notifyOnSessionHibernated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + user := p.getString(sessionMap, "user") + sessionID := p.getString(sessionMap, "id") + + customDetails := map[string]interface{}{ + "user": user, + "sessionId": sessionID, + "eventType": "session.hibernated", + "reason": "inactivity", + } + + severity := p.getString(ctx.Config, "sessionHibernatedSeverity") + if severity == "" { + severity = "warning" + } + + event := PagerDutyEvent{ + RoutingKey: p.getString(ctx.Config, "routingKey"), + EventAction: "trigger", + DedupKey: fmt.Sprintf("streamspace-session-hibernated-%s", sessionID), + Payload: PagerDutyPayload{ + Summary: fmt.Sprintf("StreamSpace Session Hibernated: %s (User: %s)", sessionID, user), + Source: "streamspace", + Severity: severity, + Timestamp: time.Now().Format(time.RFC3339), + Component: "sessions", + Class: "session.hibernated", + CustomDetails: customDetails, + }, + } + + if err := p.sendEvent(ctx, event); err != nil { + return err + } + + // Auto-resolve if configured + if p.getBool(ctx.Config, "autoResolve") { + return p.resolveEvent(ctx, event.DedupKey) + } + + return nil +} + +// OnUserCreated is called when a user is created +func (p *PagerDutyPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + notify, _ := ctx.Config["notifyOnUserCreated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user data type") + } + + username := p.getString(userMap, "username") + fullName := p.getString(userMap, "fullName") + email := p.getString(userMap, "email") + tier := p.getString(userMap, "tier") + + customDetails := map[string]interface{}{ + "username": username, + "fullName": fullName, + "email": email, + "tier": tier, + "eventType": "user.created", + } + + severity := p.getString(ctx.Config, "userCreatedSeverity") + if severity == "" { + severity = "info" + } + + event := PagerDutyEvent{ + RoutingKey: p.getString(ctx.Config, "routingKey"), + EventAction: "trigger", + DedupKey: fmt.Sprintf("streamspace-user-%s", username), + Payload: PagerDutyPayload{ + Summary: fmt.Sprintf("StreamSpace User Created: %s (%s)", fullName, username), + Source: "streamspace", + Severity: severity, + Timestamp: time.Now().Format(time.RFC3339), + Component: "users", + Class: "user.created", + CustomDetails: customDetails, + }, + } + + if err := p.sendEvent(ctx, event); err != nil { + return err + } + + // Auto-resolve if configured + if p.getBool(ctx.Config, "autoResolve") { + return p.resolveEvent(ctx, event.DedupKey) + } + + return nil +} + +// sendEvent sends an event to PagerDuty +func (p *PagerDutyPlugin) sendEvent(ctx *plugins.PluginContext, event PagerDutyEvent) error { + // Marshal event to JSON + payload, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("failed to marshal PagerDuty event: %w", err) + } + + // Send HTTP POST to PagerDuty Events API + resp, err := http.Post(pagerDutyEventsURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("failed to send PagerDuty event: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("pagerduty API returned status: %d", resp.StatusCode) + } + + ctx.Logger.Debug("PagerDuty event sent successfully", map[string]interface{}{ + "dedupKey": event.DedupKey, + "severity": event.Payload.Severity, + }) + + return nil +} + +// resolveEvent resolves an event in PagerDuty +func (p *PagerDutyPlugin) resolveEvent(ctx *plugins.PluginContext, dedupKey string) error { + event := PagerDutyEvent{ + RoutingKey: p.getString(ctx.Config, "routingKey"), + EventAction: "resolve", + DedupKey: dedupKey, + Payload: PagerDutyPayload{ + Summary: "Event auto-resolved", + Source: "streamspace", + Severity: "info", + }, + } + + return p.sendEvent(ctx, event) +} + +// testIntegration tests the PagerDuty integration +func (p *PagerDutyPlugin) testIntegration(ctx *plugins.PluginContext, routingKey string) error { + event := PagerDutyEvent{ + RoutingKey: routingKey, + EventAction: "trigger", + DedupKey: fmt.Sprintf("streamspace-test-%d", time.Now().Unix()), + Payload: PagerDutyPayload{ + Summary: "StreamSpace PagerDuty Plugin Test", + Source: "streamspace", + Severity: "info", + Timestamp: time.Now().Format(time.RFC3339), + Component: "plugin-test", + CustomDetails: map[string]interface{}{ + "message": "PagerDuty integration is configured and working", + }, + }, + } + + if err := p.sendEvent(ctx, event); err != nil { + return err + } + + // Auto-resolve the test event + time.Sleep(2 * time.Second) // Small delay before resolving + return p.resolveEvent(ctx, event.DedupKey) +} + +// checkRateLimit checks if we're within the rate limit +func (p *PagerDutyPlugin) checkRateLimit(ctx *plugins.PluginContext) bool { + maxEvents, _ := ctx.Config["rateLimit"].(float64) + if maxEvents == 0 { + maxEvents = 50 // Default + } + + now := time.Now() + if now.Sub(p.lastReset) > time.Hour { + p.eventCount = 0 + p.lastReset = now + } + + if p.eventCount >= int(maxEvents) { + return false + } + + p.eventCount++ + return true +} + +// Helper functions to safely extract values from maps +func (p *PagerDutyPlugin) getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (p *PagerDutyPlugin) getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return false +} + +// init auto-registers the plugin globally +func init() { + plugins.Register("streamspace-pagerduty", func() plugins.PluginHandler { + return NewPagerDutyPlugin() + }) +} diff --git a/streamspace-recording/README.md b/streamspace-recording/README.md new file mode 100644 index 0000000..62d6d71 --- /dev/null +++ b/streamspace-recording/README.md @@ -0,0 +1,28 @@ +# StreamSpace Session Recording & Playback Plugin + +Record and replay sessions with multiple formats, retention policies, and compliance-driven recording. + +## Features +- Multiple formats (webm, mp4, vnc) +- Automatic compliance recording +- Retention policies with auto-cleanup +- Encrypted storage +- Playback controls +- Download capability + +## Installation +Admin → Plugins → "Session Recording & Playback" → Install + +## Configuration +```json +{ + "enabled": true, + "defaultFormat": "webm", + "defaultRetentionDays": 365, + "autoRecordForCompliance": false, + "encryptRecordings": true +} +``` + +## License +MIT diff --git a/streamspace-recording/manifest.json b/streamspace-recording/manifest.json new file mode 100644 index 0000000..a7f8de7 --- /dev/null +++ b/streamspace-recording/manifest.json @@ -0,0 +1,45 @@ +{ + "name": "streamspace-recording", + "version": "1.0.0", + "displayName": "Session Recording & Playback", + "description": "Record and replay sessions with multiple formats (webm, mp4, vnc), retention policies, and compliance-driven recording", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Session Management", + "tags": ["recording", "playback", "compliance", "audit", "video"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "recording_plugin.go" + }, + + "permissions": ["database", "storage", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "defaultFormat": {"type": "string", "enum": ["webm", "mp4", "vnc"], "default": "webm"}, + "defaultRetentionDays": {"type": "integer", "default": 365}, + "maxFileSize": {"type": "integer", "default": 10737418240}, + "storagePath": {"type": "string", "default": "/var/lib/streamspace/recordings"}, + "autoRecordForCompliance": {"type": "boolean", "default": false}, + "complianceFrameworks": {"type": "array", "items": {"type": "string"}, "default": []}, + "encryptRecordings": {"type": "boolean", "default": true} + } + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated" + }, + "database": {"tables": ["session_recordings", "recording_playback"]}, + "api": {"endpoints": ["/recordings", "/recordings/:id", "/recordings/:id/playback", "/recordings/:id/download"]}, + "ui": { + "adminPages": [{"id": "recordings", "title": "Session Recordings", "route": "/admin/recordings", "component": "Recordings", "icon": "videocam"}], + "userPages": [{"id": "my-recordings", "title": "My Recordings", "route": "/recordings", "component": "MyRecordings", "icon": "video_library"}] + }, + "scheduler": {"jobs": [{"name": "cleanup-expired-recordings", "schedule": "0 2 * * *", "description": "Delete expired recordings"}]} +} diff --git a/streamspace-recording/recording_plugin.go b/streamspace-recording/recording_plugin.go new file mode 100644 index 0000000..015d774 --- /dev/null +++ b/streamspace-recording/recording_plugin.go @@ -0,0 +1,124 @@ +package main + +import ("encoding/json"; "fmt"; "time"; "github.com/yourusername/streamspace/api/internal/plugins") + +type RecordingPlugin struct { + plugins.BasePlugin + config RecordingConfig +} + +type RecordingConfig struct { + Enabled bool `json:"enabled"` + DefaultFormat string `json:"defaultFormat"` + DefaultRetentionDays int `json:"defaultRetentionDays"` + MaxFileSize int64 `json:"maxFileSize"` + StoragePath string `json:"storagePath"` + AutoRecordForCompliance bool `json:"autoRecordForCompliance"` + ComplianceFrameworks []string `json:"complianceFrameworks"` + EncryptRecordings bool `json:"encryptRecordings"` +} + +type SessionRecording struct { + ID int64 `json:"id"` + SessionID string `json:"session_id"` + UserID string `json:"user_id"` + StartTime time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time,omitempty"` + Duration int `json:"duration"` + FileSize int64 `json:"file_size"` + FilePath string `json:"file_path"` + FileHash string `json:"file_hash"` + Format string `json:"format"` + Status string `json:"status"` + RetentionDays int `json:"retention_days"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + IsAutomatic bool `json:"is_automatic"` + Reason string `json:"reason,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func (p *RecordingPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("Recording plugin is disabled") + return nil + } + + p.createDatabaseTables(ctx) + ctx.Logger.Info("Recording plugin initialized", "storage", p.config.StoragePath) + return nil +} + +func (p *RecordingPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Session Recording plugin loaded") + return nil +} + +func (p *RecordingPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled || !p.config.AutoRecordForCompliance { + return nil + } + + sessionMap, _ := session.(map[string]interface{}) + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + + // Start automatic recording for compliance + return p.startRecording(ctx, sessionID, userID, "compliance") +} + +func (p *RecordingPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, _ := session.(map[string]interface{}) + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + + // Finalize recording + return p.finalizeRecording(ctx, sessionID) +} + +func (p *RecordingPlugin) RunScheduledJob(ctx *plugins.PluginContext, jobName string) error { + if jobName == "cleanup-expired-recordings" { + return p.cleanupExpiredRecordings(ctx) + } + return nil +} + +func (p *RecordingPlugin) createDatabaseTables(ctx *plugins.PluginContext) error { + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS session_recordings ( + id SERIAL PRIMARY KEY, session_id VARCHAR(255), user_id VARCHAR(255), + start_time TIMESTAMP, end_time TIMESTAMP, duration INTEGER, + file_size BIGINT, file_path TEXT, file_hash VARCHAR(255), + format VARCHAR(50), status VARCHAR(50), retention_days INTEGER, + expires_at TIMESTAMP, is_automatic BOOLEAN, reason TEXT, + created_at TIMESTAMP DEFAULT NOW() + )`) + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS recording_playback ( + id SERIAL PRIMARY KEY, recording_id INTEGER, user_id VARCHAR(255), + started_at TIMESTAMP DEFAULT NOW(), position INTEGER, speed FLOAT + )`) + return nil +} + +func (p *RecordingPlugin) startRecording(ctx *plugins.PluginContext, sessionID, userID, reason string) error { + ctx.Logger.Info("Starting recording", "session", sessionID, "reason", reason) + return nil +} + +func (p *RecordingPlugin) finalizeRecording(ctx *plugins.PluginContext, sessionID string) error { + ctx.Logger.Info("Finalizing recording", "session", sessionID) + return nil +} + +func (p *RecordingPlugin) cleanupExpiredRecordings(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Cleaning up expired recordings") + return nil +} + +func init() { + plugins.Register("streamspace-recording", &RecordingPlugin{}) +} diff --git a/streamspace-sentry/README.md b/streamspace-sentry/README.md new file mode 100644 index 0000000..3729a93 --- /dev/null +++ b/streamspace-sentry/README.md @@ -0,0 +1,351 @@ +# StreamSpace Sentry Plugin + +Error tracking and performance monitoring integration with Sentry. + +## Features + +- **Error Tracking** - Automatically capture and report errors and exceptions +- **Performance Monitoring** - Track transaction performance and bottlenecks +- **Breadcrumbs** - Detailed event trail leading to errors +- **Source Maps** - Link errors to exact code locations +- **Releases** - Track errors across deployments +- **User Context** - Associate errors with specific users and sessions +- **Custom Tags** - Organize and filter errors +- **Ignore Patterns** - Filter out expected errors and noise + +## Installation + +### Via Plugin Marketplace + +1. Navigate to **Admin → Plugins** +2. Search for "Sentry Error Tracking" +3. Click **Install** +4. Configure with your Sentry DSN +5. Click **Enable** + +## Configuration + +### Basic Setup + +```json +{ + "enabled": true, + "dsn": "https://[key]@[organization].ingest.sentry.io/[project]", + "environment": "production" +} +``` + +### Full Configuration + +```json +{ + "enabled": true, + "dsn": "https://examplePublicKey@o0.ingest.sentry.io/0", + "environment": "production", + "release": "streamspace@1.0.0", + "serverName": "api-server-01", + "enableTracing": true, + "tracesSampleRate": 0.1, + "attachStacktrace": true, + "sendDefaultPii": false, + "captureSessionErrors": true, + "captureAPIErrors": true, + "captureUnhandledErrors": true, + "ignoreErrors": [ + "context canceled", + "connection reset by peer", + "broken pipe" + ], + "tags": { + "service": "streamspace", + "region": "us-east-1", + "team": "platform" + } +} +``` + +### Getting Your Sentry DSN + +1. Log into Sentry.io +2. Go to **Settings → Projects → [Your Project]** +3. Click **Client Keys (DSN)** +4. Copy the DSN URL + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | boolean | `true` | Enable Sentry integration | +| `dsn` | string | *required* | Sentry Data Source Name | +| `environment` | string | `production` | Environment name | +| `release` | string | `1.0.0` | Release version for tracking | +| `serverName` | string | `streamspace-api` | Server identifier | +| `enableTracing` | boolean | `true` | Enable performance tracing | +| `tracesSampleRate` | number | `0.1` | % of transactions to trace (0.0-1.0) | +| `attachStacktrace` | boolean | `true` | Include stack traces | +| `sendDefaultPii` | boolean | `false` | Send user IDs and IPs | +| `captureSessionErrors` | boolean | `true` | Capture session errors | +| `captureAPIErrors` | boolean | `true` | Capture API errors | +| `captureUnhandledErrors` | boolean | `true` | Capture unhandled exceptions | +| `ignoreErrors` | array | `[]` | Error patterns to ignore (regex) | +| `tags` | object | `{}` | Global tags for all events | + +## Usage + +### View Errors in Sentry + +1. Log into Sentry.io +2. Navigate to **Issues** +3. Filter by: + - Environment (production, staging) + - Release version + - User ID + - Session ID + - Tags + +### Error Details + +Each error in Sentry includes: +- **Stack Trace** - Full stack trace with code context +- **Breadcrumbs** - Events leading up to error +- **User Context** - User ID, session ID +- **Tags** - Categorization and filtering +- **Environment** - Where error occurred + +### Creating Alerts + +#### High Error Rate Alert + +``` +Alert Conditions: +- Number of events > 100 +- In 1 minute +- For errors matching: is:unresolved + +Actions: +- Send Slack notification to #alerts +- Send email to platform-team@company.com +``` + +#### New Error Type Alert + +``` +Alert Conditions: +- A new issue is created +- For errors matching: is:unresolved level:error + +Actions: +- Create PagerDuty incident +- Post to #platform-alerts Slack channel +``` + +#### Session Error Spike + +``` +Alert Conditions: +- Number of events > 50 +- In 5 minutes +- For errors matching: session_id:* + +Actions: +- Send webhook to monitoring system +- Email ops-team@company.com +``` + +### Releases and Deploys + +Track which errors came from which deployment: + +```bash +# Create a release +sentry-cli releases new streamspace@1.2.0 + +# Associate commits +sentry-cli releases set-commits streamspace@1.2.0 --auto + +# Deploy +sentry-cli releases deploys streamspace@1.2.0 new -e production + +# Finalize +sentry-cli releases finalize streamspace@1.2.0 +``` + +### Performance Monitoring + +View transaction performance: + +1. Navigate to **Performance** in Sentry +2. View slow transactions +3. Analyze bottlenecks +4. Track improvements over releases + +## Events Captured + +### Automatic Events + +- **Session Errors** - Errors during session creation/termination +- **API Errors** - Failed API requests and validations +- **Unhandled Exceptions** - Panics and uncaught errors +- **Database Errors** - Query failures and connection issues + +### Manual Events + +You can manually capture errors in your code: + +```go +// Capture an error +plugin.CaptureError(err, map[string]interface{}{ + "user_id": userID, + "session_id": sessionID, + "action": "create_session", +}) + +// Capture a message +plugin.CaptureMessage("Important event occurred", sentry.LevelWarning, map[string]interface{}{ + "detail": "xyz", +}) + +// Start a transaction (performance) +span := plugin.StartTransaction("session.create", "http.request") +defer span.Finish() +``` + +## Breadcrumbs + +Breadcrumbs provide context about what happened before an error: + +**Automatic Breadcrumbs**: +- Session created +- Session terminated +- User created +- API requests +- Database queries + +**Example Breadcrumb Trail**: +``` +1. User logged in (user_id: 123) +2. Session created (session_id: abc, template: firefox) +3. API request: GET /api/sessions/abc +4. Database query: SELECT * FROM sessions WHERE id = 'abc' +5. ERROR: Session not found +``` + +## Ignore Patterns + +Prevent noise from expected errors: + +```json +{ + "ignoreErrors": [ + "context canceled", // User canceled operation + "connection reset", // Network issues + "broken pipe", // Client disconnected + "session not found", // Expected 404 + "unauthorized", // Auth failures (use rate limit instead) + "EOF" // Connection closed + ] +} +``` + +## Troubleshooting + +### Errors not appearing in Sentry + +**Problem**: Events not showing up + +**Solution**: +- Verify DSN is correct +- Check `enabled` is `true` +- Review Sentry project quota (may be exhausted) +- Check error doesn't match ignore patterns +- Wait 30-60 seconds for events to appear + +### Too many errors + +**Problem**: Error quota exhausted, high Sentry costs + +**Solution**: +- Add ignore patterns for noisy errors +- Reduce `tracesSampleRate` (e.g., 0.01 = 1%) +- Set up error grouping rules +- Use Sentry's spike protection +- Upgrade Sentry plan or add more quota + +### Missing stack traces + +**Problem**: Errors don't show code context + +**Solution**: +- Ensure `attachStacktrace: true` +- Upload source maps for minified code +- Check stack trace depth limits +- Verify release is set correctly + +### High memory usage + +**Problem**: Sentry SDK using too much memory + +**Solution**: +- Reduce `tracesSampleRate` +- Disable `attachStacktrace` if not needed +- Limit breadcrumb buffer size +- Review event size limits + +## Best Practices + +1. **Set Releases** - Always set release version for tracking +2. **Use Environments** - Separate production, staging, development +3. **Add Context** - Include user_id, session_id in error context +4. **Create Alerts** - Proactive alerting on new/high error rates +5. **Review Weekly** - Triage new issues, resolve old ones +6. **Ignore Wisely** - Filter noise but don't over-filter +7. **Track Performance** - Use tracing to find bottlenecks +8. **Monitor Quota** - Track Sentry usage to control costs + +## Integration with Other Tools + +### Slack + +``` +Sentry → Settings → Integrations → Slack +- Link Slack workspace +- Choose #alerts channel +- Configure notification rules +``` + +### Jira + +``` +Sentry → Settings → Integrations → Jira +- Link Jira instance +- Auto-create tickets for new issues +- Link Sentry issues to Jira tickets +``` + +### GitHub + +``` +Sentry → Settings → Integrations → GitHub +- Link GitHub repository +- Create GitHub issues from Sentry +- See suspect commits in error details +``` + +## Support + +- GitHub: https://github.com/JoshuaAFerguson/streamspace-plugins/issues +- Docs: https://docs.streamspace.io/plugins/sentry +- Sentry Docs: https://docs.sentry.io/ + +## License + +MIT License + +## Version History + +- **1.0.0** (2025-01-15) + - Initial release + - Error tracking + - Performance monitoring + - Breadcrumbs + - Custom tags and ignore patterns diff --git a/streamspace-sentry/manifest.json b/streamspace-sentry/manifest.json new file mode 100644 index 0000000..e24ea92 --- /dev/null +++ b/streamspace-sentry/manifest.json @@ -0,0 +1,137 @@ +{ + "name": "streamspace-sentry", + "version": "1.0.0", + "displayName": "Sentry Error Tracking", + "description": "Track errors, exceptions, and performance issues with Sentry integration", + "author": "StreamSpace Team", + "license": "MIT", + "type": "integration", + "category": "Monitoring", + "tags": ["monitoring", "sentry", "errors", "exceptions", "performance"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "sentry_plugin.go" + }, + + "permissions": ["network"], + "configSchema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Sentry Integration", + "description": "Enable error tracking and performance monitoring with Sentry", + "default": true + }, + "dsn": { + "type": "string", + "title": "Sentry DSN", + "description": "Your Sentry Data Source Name (DSN)", + "format": "password" + }, + "environment": { + "type": "string", + "title": "Environment", + "description": "Environment name (production, staging, development)", + "default": "production" + }, + "release": { + "type": "string", + "title": "Release Version", + "description": "StreamSpace release version for tracking", + "default": "1.0.0" + }, + "serverName": { + "type": "string", + "title": "Server Name", + "description": "Server/instance identifier", + "default": "streamspace-api" + }, + "enableTracing": { + "type": "boolean", + "title": "Enable Performance Tracing", + "description": "Track performance and transaction data", + "default": true + }, + "tracesSampleRate": { + "type": "number", + "title": "Traces Sample Rate", + "description": "Percentage of transactions to trace (0.0-1.0)", + "default": 0.1, + "minimum": 0, + "maximum": 1 + }, + "attachStacktrace": { + "type": "boolean", + "title": "Attach Stack Trace", + "description": "Attach stack traces to all events", + "default": true + }, + "sendDefaultPii": { + "type": "boolean", + "title": "Send Default PII", + "description": "Send personally identifiable information (user IDs, IPs)", + "default": false + }, + "captureSessionErrors": { + "type": "boolean", + "title": "Capture Session Errors", + "description": "Automatically capture session-related errors", + "default": true + }, + "captureAPIErrors": { + "type": "boolean", + "title": "Capture API Errors", + "description": "Automatically capture API errors and failures", + "default": true + }, + "captureUnhandledErrors": { + "type": "boolean", + "title": "Capture Unhandled Errors", + "description": "Capture unhandled exceptions and panics", + "default": true + }, + "ignoreErrors": { + "type": "array", + "title": "Ignore Error Patterns", + "description": "Error messages to ignore (regex patterns)", + "items": { + "type": "string" + }, + "default": ["context canceled", "connection reset"] + }, + "beforeSend": { + "type": "string", + "title": "Before Send Hook", + "description": "Custom JavaScript function to modify events before sending", + "default": "" + }, + "tags": { + "type": "object", + "title": "Global Tags", + "description": "Tags to attach to all Sentry events", + "additionalProperties": { + "type": "string" + }, + "default": { + "service": "streamspace" + } + } + }, + "required": ["dsn"] + }, + "lifecycle": { + "onLoad": true, + "onUnload": true + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "session.error": "OnSessionError", + "user.created": "OnUserCreated" + } +} diff --git a/streamspace-sentry/sentry_plugin.go b/streamspace-sentry/sentry_plugin.go new file mode 100644 index 0000000..4635d64 --- /dev/null +++ b/streamspace-sentry/sentry_plugin.go @@ -0,0 +1,326 @@ +package main + +import ( + "encoding/json" + "fmt" + "regexp" + + "github.com/getsentry/sentry-go" + "github.com/yourusername/streamspace/api/internal/plugins" +) + +// SentryPlugin sends errors and performance data to Sentry +type SentryPlugin struct { + plugins.BasePlugin + config SentryConfig + ignoreRegexps []*regexp.Regexp +} + +// SentryConfig holds Sentry configuration +type SentryConfig struct { + Enabled bool `json:"enabled"` + DSN string `json:"dsn"` + Environment string `json:"environment"` + Release string `json:"release"` + ServerName string `json:"serverName"` + EnableTracing bool `json:"enableTracing"` + TracesSampleRate float64 `json:"tracesSampleRate"` + AttachStacktrace bool `json:"attachStacktrace"` + SendDefaultPii bool `json:"sendDefaultPii"` + CaptureSessionErrors bool `json:"captureSessionErrors"` + CaptureAPIErrors bool `json:"captureAPIErrors"` + CaptureUnhandledErrors bool `json:"captureUnhandledErrors"` + IgnoreErrors []string `json:"ignoreErrors"` + BeforeSend string `json:"beforeSend"` + Tags map[string]string `json:"tags"` +} + +// Initialize sets up the Sentry plugin +func (p *SentryPlugin) Initialize(ctx *plugins.PluginContext) error { + // Load configuration + configBytes, err := json.Marshal(ctx.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := json.Unmarshal(configBytes, &p.config); err != nil { + return fmt.Errorf("failed to unmarshal Sentry config: %w", err) + } + + if !p.config.Enabled { + ctx.Logger.Info("Sentry integration is disabled") + return nil + } + + if p.config.DSN == "" { + return fmt.Errorf("Sentry DSN is required") + } + + // Compile ignore error regexps + p.ignoreRegexps = make([]*regexp.Regexp, 0, len(p.config.IgnoreErrors)) + for _, pattern := range p.config.IgnoreErrors { + re, err := regexp.Compile(pattern) + if err != nil { + ctx.Logger.Warn("Failed to compile ignore error pattern", "pattern", pattern, "error", err) + continue + } + p.ignoreRegexps = append(p.ignoreRegexps, re) + } + + // Initialize Sentry SDK + err = sentry.Init(sentry.ClientOptions{ + Dsn: p.config.DSN, + Environment: p.config.Environment, + Release: p.config.Release, + ServerName: p.config.ServerName, + AttachStacktrace: p.config.AttachStacktrace, + SendDefaultPII: p.config.SendDefaultPii, + TracesSampleRate: p.config.TracesSampleRate, + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + // Apply ignore patterns + if event.Message != "" { + for _, re := range p.ignoreRegexps { + if re.MatchString(event.Message) { + return nil // Ignore this error + } + } + } + + // Add global tags + for k, v := range p.config.Tags { + event.Tags[k] = v + } + + return event + }, + }) + + if err != nil { + return fmt.Errorf("failed to initialize Sentry: %w", err) + } + + // Set global tags + for k, v := range p.config.Tags { + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetTag(k, v) + }) + } + + ctx.Logger.Info("Sentry plugin initialized successfully", + "environment", p.config.Environment, + "release", p.config.Release, + "tracing_enabled", p.config.EnableTracing, + "sample_rate", p.config.TracesSampleRate, + ) + + return nil +} + +// OnLoad is called when the plugin is loaded +func (p *SentryPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Sentry error tracking plugin loaded") + + sentry.CaptureMessage("StreamSpace Sentry Plugin Loaded") + + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *SentryPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Sentry error tracking plugin unloading") + + sentry.CaptureMessage("StreamSpace Sentry Plugin Unloaded") + + // Flush any pending events + sentry.Flush(5000) // 5 second timeout + + return nil +} + +// OnSessionCreated tracks session creation in Sentry +func (p *SentryPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + templateName := fmt.Sprintf("%v", sessionMap["template_name"]) + + // Create breadcrumb + sentry.AddBreadcrumb(&sentry.Breadcrumb{ + Type: "info", + Category: "session", + Message: "Session created", + Data: map[string]interface{}{ + "session_id": sessionID, + "user_id": userID, + "template": templateName, + }, + Level: sentry.LevelInfo, + }) + + return nil +} + +// OnSessionTerminated tracks session termination +func (p *SentryPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session format") + } + + sessionID := fmt.Sprintf("%v", sessionMap["id"]) + userID := fmt.Sprintf("%v", sessionMap["user_id"]) + + // Create breadcrumb + sentry.AddBreadcrumb(&sentry.Breadcrumb{ + Type: "info", + Category: "session", + Message: "Session terminated", + Data: map[string]interface{}{ + "session_id": sessionID, + "user_id": userID, + }, + Level: sentry.LevelInfo, + }) + + return nil +} + +// OnSessionError captures session errors +func (p *SentryPlugin) OnSessionError(ctx *plugins.PluginContext, errorData interface{}) error { + if !p.config.Enabled || !p.config.CaptureSessionErrors { + return nil + } + + errorMap, ok := errorData.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid error format") + } + + errorMsg := fmt.Sprintf("%v", errorMap["error"]) + sessionID := fmt.Sprintf("%v", errorMap["session_id"]) + userID := fmt.Sprintf("%v", errorMap["user_id"]) + + // Check if error should be ignored + for _, re := range p.ignoreRegexps { + if re.MatchString(errorMsg) { + return nil + } + } + + // Capture exception with context + sentry.WithScope(func(scope *sentry.Scope) { + scope.SetTag("session_id", sessionID) + scope.SetTag("user_id", userID) + scope.SetContext("session", map[string]interface{}{ + "session_id": sessionID, + "user_id": userID, + "error": errorMsg, + }) + + if stack, ok := errorMap["stack"].(string); ok { + scope.SetExtra("stack_trace", stack) + } + + sentry.CaptureException(fmt.Errorf("session error: %s", errorMsg)) + }) + + return nil +} + +// OnUserCreated tracks user creation +func (p *SentryPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user format") + } + + userID := fmt.Sprintf("%v", userMap["id"]) + + // Create breadcrumb + sentry.AddBreadcrumb(&sentry.Breadcrumb{ + Type: "info", + Category: "user", + Message: "User created", + Data: map[string]interface{}{ + "user_id": userID, + }, + Level: sentry.LevelInfo, + }) + + return nil +} + +// CaptureError is a helper method to capture errors from other parts of StreamSpace +func (p *SentryPlugin) CaptureError(err error, context map[string]interface{}) { + if !p.config.Enabled { + return + } + + // Check if error should be ignored + errorMsg := err.Error() + for _, re := range p.ignoreRegexps { + if re.MatchString(errorMsg) { + return + } + } + + sentry.WithScope(func(scope *sentry.Scope) { + // Add all context data + for k, v := range context { + scope.SetTag(k, fmt.Sprintf("%v", v)) + } + + sentry.CaptureException(err) + }) +} + +// CaptureMessage captures a message with level +func (p *SentryPlugin) CaptureMessage(message string, level sentry.Level, context map[string]interface{}) { + if !p.config.Enabled { + return + } + + sentry.WithScope(func(scope *sentry.Scope) { + scope.SetLevel(level) + + // Add context + for k, v := range context { + scope.SetTag(k, fmt.Sprintf("%v", v)) + } + + sentry.CaptureMessage(message) + }) +} + +// StartTransaction starts a performance transaction +func (p *SentryPlugin) StartTransaction(name string, operation string) *sentry.Span { + if !p.config.Enabled || !p.config.EnableTracing { + return nil + } + + ctx := sentry.StartTransaction(sentry.Context{}, name) + ctx.Op = operation + + return ctx +} + +// Export the plugin +func init() { + plugins.Register("streamspace-sentry", &SentryPlugin{}) +} diff --git a/streamspace-slack/README.md b/streamspace-slack/README.md new file mode 100644 index 0000000..1a6213a --- /dev/null +++ b/streamspace-slack/README.md @@ -0,0 +1,187 @@ +# StreamSpace Slack Integration Plugin + +Send real-time notifications about StreamSpace events to your Slack channels. + +## Features + +- 🚀 Session event notifications (created, hibernated, deleted) +- 👤 User event notifications (created, login, logout) +- ⚙️ Configurable notification preferences +- 🚦 Rate limiting to prevent spam +- 📊 Detailed or summary notifications +- 🎨 Rich Slack attachments with colors and formatting + +## Installation + +### Via StreamSpace UI + +1. Navigate to **Admin** → **Plugins** +2. Search for "Slack Integration" +3. Click **Install** +4. Configure your Slack webhook URL +5. Enable the plugin + +### Via kubectl + +```bash +kubectl apply -f - <=1.0.0" + }, + + "entrypoints": { + "main": "slack_plugin.go" + }, + + "configSchema": { + "type": "object", + "properties": { + "webhookUrl": { + "type": "string", + "title": "Slack Webhook URL", + "description": "Your Slack incoming webhook URL (https://hooks.slack.com/services/...)", + "pattern": "^https://hooks\\.slack\\.com/.*$" + }, + "channel": { + "type": "string", + "title": "Default Channel", + "description": "Default Slack channel for notifications (e.g., #general)", + "default": "#general" + }, + "username": { + "type": "string", + "title": "Bot Username", + "description": "Username for Slack messages", + "default": "StreamSpace" + }, + "iconEmoji": { + "type": "string", + "title": "Icon Emoji", + "description": "Emoji icon for Slack messages", + "default": ":computer:" + }, + "notifyOnSessionCreated": { + "type": "boolean", + "title": "Notify on Session Created", + "description": "Send notification when a session is created", + "default": true + }, + "notifyOnSessionHibernated": { + "type": "boolean", + "title": "Notify on Session Hibernated", + "description": "Send notification when a session is hibernated", + "default": false + }, + "notifyOnUserCreated": { + "type": "boolean", + "title": "Notify on User Created", + "description": "Send notification when a user is created", + "default": true + }, + "notifyOnQuotaExceeded": { + "type": "boolean", + "title": "Notify on Quota Exceeded", + "description": "Send notification when a user exceeds their quota", + "default": true + }, + "includeDetails": { + "type": "boolean", + "title": "Include Details", + "description": "Include detailed information in notifications", + "default": true + }, + "rateLimit": { + "type": "number", + "title": "Rate Limit (messages/hour)", + "description": "Maximum messages per hour to prevent spam", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + "required": ["webhookUrl"] + }, + + "defaultConfig": { + "channel": "#general", + "username": "StreamSpace", + "iconEmoji": ":computer:", + "notifyOnSessionCreated": true, + "notifyOnSessionHibernated": false, + "notifyOnUserCreated": true, + "notifyOnQuotaExceeded": true, + "includeDetails": true, + "rateLimit": 20 + }, + + "permissions": [ + "network" + ] +} diff --git a/streamspace-slack/slack_plugin.go b/streamspace-slack/slack_plugin.go new file mode 100644 index 0000000..c7e9a0c --- /dev/null +++ b/streamspace-slack/slack_plugin.go @@ -0,0 +1,344 @@ +package slackplugin + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/streamspace/streamspace/api/internal/plugins" +) + +// SlackPlugin implements Slack notification integration +type SlackPlugin struct { + plugins.BasePlugin + + // Rate limiting + messageCount int + lastReset time.Time +} + +// SlackMessage represents a Slack message payload +type SlackMessage struct { + Text string `json:"text,omitempty"` + Channel string `json:"channel,omitempty"` + Username string `json:"username,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` +} + +// Attachment represents a Slack message attachment +type Attachment struct { + Color string `json:"color,omitempty"` + Title string `json:"title,omitempty"` + Text string `json:"text,omitempty"` + Fields []Field `json:"fields,omitempty"` + Footer string `json:"footer,omitempty"` + FooterIcon string `json:"footer_icon,omitempty"` + Timestamp int64 `json:"ts,omitempty"` +} + +// Field represents a field in a Slack attachment +type Field struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// NewSlackPlugin creates a new Slack plugin instance +func NewSlackPlugin() *SlackPlugin { + return &SlackPlugin{ + BasePlugin: plugins.BasePlugin{Name: "streamspace-slack"}, + lastReset: time.Now(), + } +} + +// OnLoad is called when the plugin is loaded +func (p *SlackPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Slack plugin loading", map[string]interface{}{ + "version": "1.0.0", + "config": ctx.Config, + }) + + // Validate configuration + webhookURL, ok := ctx.Config["webhookUrl"].(string) + if !ok || webhookURL == "" { + return fmt.Errorf("slack webhook URL is required") + } + + // Test webhook connectivity + if err := p.testWebhook(ctx, webhookURL); err != nil { + ctx.Logger.Warn("Failed to test Slack webhook", map[string]interface{}{ + "error": err.Error(), + }) + // Don't fail on test error, webhook might have restrictions + } + + // Log successful load + ctx.Logger.Info("Slack plugin loaded successfully") + + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *SlackPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Slack plugin unloading") + return nil +} + +// OnSessionCreated is called when a session is created +func (p *SlackPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + // Check if enabled in config + notify, _ := ctx.Config["notifyOnSessionCreated"].(bool) + if !notify { + return nil + } + + // Check rate limit + if !p.checkRateLimit(ctx) { + ctx.Logger.Warn("Rate limit exceeded, skipping notification") + return nil + } + + // Extract session data + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + user := p.getString(sessionMap, "user") + template := p.getString(sessionMap, "template") + sessionID := p.getString(sessionMap, "id") + + // Build Slack message + message := SlackMessage{ + Channel: p.getString(ctx.Config, "channel"), + Username: p.getString(ctx.Config, "username"), + IconEmoji: p.getString(ctx.Config, "iconEmoji"), + Text: "🚀 New Session Created", + Attachments: []Attachment{ + { + Color: "good", + Title: "Session Details", + Fields: []Field{ + {Title: "User", Value: user, Short: true}, + {Title: "Template", Value: template, Short: true}, + {Title: "Session ID", Value: sessionID, Short: false}, + }, + Footer: "StreamSpace", + Timestamp: time.Now().Unix(), + }, + }, + } + + // Include additional details if configured + if p.getBool(ctx.Config, "includeDetails") { + if resources, ok := sessionMap["resources"].(map[string]interface{}); ok { + memory := p.getString(resources, "memory") + cpu := p.getString(resources, "cpu") + + message.Attachments[0].Fields = append(message.Attachments[0].Fields, + Field{Title: "Memory", Value: memory, Short: true}, + Field{Title: "CPU", Value: cpu, Short: true}, + ) + } + } + + // Send to Slack + return p.sendMessage(ctx, message) +} + +// OnSessionHibernated is called when a session is hibernated +func (p *SlackPlugin) OnSessionHibernated(ctx *plugins.PluginContext, session interface{}) error { + notify, _ := ctx.Config["notifyOnSessionHibernated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + user := p.getString(sessionMap, "user") + sessionID := p.getString(sessionMap, "id") + + message := SlackMessage{ + Channel: p.getString(ctx.Config, "channel"), + Username: p.getString(ctx.Config, "username"), + IconEmoji: p.getString(ctx.Config, "iconEmoji"), + Text: "💤 Session Hibernated", + Attachments: []Attachment{ + { + Color: "warning", + Title: "Session Hibernated Due to Inactivity", + Fields: []Field{ + {Title: "User", Value: user, Short: true}, + {Title: "Session ID", Value: sessionID, Short: false}, + }, + Footer: "StreamSpace", + Timestamp: time.Now().Unix(), + }, + }, + } + + return p.sendMessage(ctx, message) +} + +// OnUserCreated is called when a user is created +func (p *SlackPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + notify, _ := ctx.Config["notifyOnUserCreated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user data type") + } + + username := p.getString(userMap, "username") + fullName := p.getString(userMap, "fullName") + email := p.getString(userMap, "email") + tier := p.getString(userMap, "tier") + + message := SlackMessage{ + Channel: p.getString(ctx.Config, "channel"), + Username: p.getString(ctx.Config, "username"), + IconEmoji: p.getString(ctx.Config, "iconEmoji"), + Text: "👤 New User Created", + Attachments: []Attachment{ + { + Color: "#36a64f", + Title: "User Details", + Fields: []Field{ + {Title: "Username", Value: username, Short: true}, + {Title: "Full Name", Value: fullName, Short: true}, + {Title: "Email", Value: email, Short: false}, + {Title: "Tier", Value: tier, Short: true}, + }, + Footer: "StreamSpace", + Timestamp: time.Now().Unix(), + }, + }, + } + + return p.sendMessage(ctx, message) +} + +// sendMessage sends a message to Slack +func (p *SlackPlugin) sendMessage(ctx *plugins.PluginContext, message SlackMessage) error { + webhookURL := p.getString(ctx.Config, "webhookUrl") + if webhookURL == "" { + return fmt.Errorf("webhook URL not configured") + } + + // Marshal message to JSON + payload, err := json.Marshal(message) + if err != nil { + return fmt.Errorf("failed to marshal Slack message: %w", err) + } + + // Send HTTP POST to Slack webhook + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("failed to send Slack message: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("slack webhook returned status: %d", resp.StatusCode) + } + + ctx.Logger.Debug("Slack notification sent successfully", map[string]interface{}{ + "channel": message.Channel, + }) + + return nil +} + +// testWebhook tests the Slack webhook connection +func (p *SlackPlugin) testWebhook(ctx *plugins.PluginContext, webhookURL string) error { + message := SlackMessage{ + Text: "🎉 StreamSpace Slack plugin activated!", + Attachments: []Attachment{ + { + Color: "good", + Text: "Your Slack integration is now configured and ready to send notifications.", + }, + }, + } + + payload, err := json.Marshal(message) + if err != nil { + return err + } + + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("webhook test failed with status: %d", resp.StatusCode) + } + + return nil +} + +// checkRateLimit checks if we're within the rate limit +func (p *SlackPlugin) checkRateLimit(ctx *plugins.PluginContext) bool { + maxMessages, _ := ctx.Config["rateLimit"].(float64) + if maxMessages == 0 { + maxMessages = 20 // Default + } + + now := time.Now() + if now.Sub(p.lastReset) > time.Hour { + p.messageCount = 0 + p.lastReset = now + } + + if p.messageCount >= int(maxMessages) { + return false + } + + p.messageCount++ + return true +} + +// Helper functions to safely extract values from maps +func (p *SlackPlugin) getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (p *SlackPlugin) getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return false +} + +// init auto-registers the plugin globally +func init() { + plugins.Register("streamspace-slack", func() plugins.PluginHandler { + return NewSlackPlugin() + }) +} diff --git a/streamspace-snapshots/README.md b/streamspace-snapshots/README.md new file mode 100644 index 0000000..db07a4b --- /dev/null +++ b/streamspace-snapshots/README.md @@ -0,0 +1,28 @@ +# StreamSpace Session Snapshots & Restore Plugin + +Create, manage, and restore session snapshots with scheduling and sharing. + +## Features +- Session state snapshots +- Scheduled snapshots +- Snapshot restore +- Snapshot sharing +- Compression and encryption +- Auto-cleanup + +## Installation +Admin → Plugins → "Session Snapshots & Restore" → Install + +## Configuration +```json +{ + "enabled": true, + "maxSnapshotsPerSession": 10, + "defaultRetentionDays": 90, + "compressionEnabled": true, + "encryptSnapshots": true +} +``` + +## License +MIT diff --git a/streamspace-snapshots/manifest.json b/streamspace-snapshots/manifest.json new file mode 100644 index 0000000..a071c9b --- /dev/null +++ b/streamspace-snapshots/manifest.json @@ -0,0 +1,37 @@ +{ + "name": "streamspace-snapshots", + "version": "1.0.0", + "displayName": "Session Snapshots & Restore", + "description": "Create, manage, and restore session snapshots with scheduling, sharing, and storage management", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Session Management", + "tags": ["snapshots", "backup", "restore", "state-management"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "snapshots_plugin.go" + }, + + "permissions": ["database", "storage", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "maxSnapshotsPerSession": {"type": "integer", "default": 10}, + "defaultRetentionDays": {"type": "integer", "default": 90}, + "storagePath": {"type": "string", "default": "/var/lib/streamspace/snapshots"}, + "compressionEnabled": {"type": "boolean", "default": true}, + "encryptSnapshots": {"type": "boolean", "default": true} + } + }, + "events": {"session.created": "OnSessionCreated"}, + "database": {"tables": ["session_snapshots", "snapshot_schedules"]}, + "api": {"endpoints": ["/snapshots", "/snapshots/:id", "/snapshots/:id/restore", "/snapshots/:id/share"]}, + "ui": {"userPages": [{"id": "snapshots", "title": "Snapshots", "route": "/snapshots", "component": "Snapshots", "icon": "camera_alt"}]}, + "scheduler": {"jobs": [{"name": "cleanup-old-snapshots", "schedule": "0 3 * * *", "description": "Delete old snapshots"}]} +} diff --git a/streamspace-snapshots/snapshots_plugin.go b/streamspace-snapshots/snapshots_plugin.go new file mode 100644 index 0000000..85b172e --- /dev/null +++ b/streamspace-snapshots/snapshots_plugin.go @@ -0,0 +1,82 @@ +package main + +import ("encoding/json"; "fmt"; "time"; "github.com/yourusername/streamspace/api/internal/plugins") + +type SnapshotsPlugin struct { + plugins.BasePlugin + config SnapshotsConfig +} + +type SnapshotsConfig struct { + Enabled bool `json:"enabled"` + MaxSnapshotsPerSession int `json:"maxSnapshotsPerSession"` + DefaultRetentionDays int `json:"defaultRetentionDays"` + StoragePath string `json:"storagePath"` + CompressionEnabled bool `json:"compressionEnabled"` + EncryptSnapshots bool `json:"encryptSnapshots"` +} + +type SessionSnapshot struct { + ID int64 `json:"id"` + SessionID string `json:"session_id"` + UserID string `json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + FilePath string `json:"file_path"` + FileSize int64 `json:"file_size"` + FileHash string `json:"file_hash"` + Compressed bool `json:"compressed"` + Encrypted bool `json:"encrypted"` + Shared bool `json:"shared"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func (p *SnapshotsPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("Snapshots plugin is disabled") + return nil + } + + p.createDatabaseTables(ctx) + ctx.Logger.Info("Snapshots plugin initialized", "storage", p.config.StoragePath) + return nil +} + +func (p *SnapshotsPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Session Snapshots plugin loaded") + return nil +} + +func (p *SnapshotsPlugin) RunScheduledJob(ctx *plugins.PluginContext, jobName string) error { + if jobName == "cleanup-old-snapshots" { + return p.cleanupOldSnapshots(ctx) + } + return nil +} + +func (p *SnapshotsPlugin) createDatabaseTables(ctx *plugins.PluginContext) error { + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS session_snapshots ( + id SERIAL PRIMARY KEY, session_id VARCHAR(255), user_id VARCHAR(255), + name VARCHAR(200), description TEXT, file_path TEXT, file_size BIGINT, + file_hash VARCHAR(255), compressed BOOLEAN, encrypted BOOLEAN, + shared BOOLEAN, expires_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW() + )`) + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS snapshot_schedules ( + id SERIAL PRIMARY KEY, session_id VARCHAR(255), schedule VARCHAR(100), + retention_days INTEGER, enabled BOOLEAN, created_at TIMESTAMP DEFAULT NOW() + )`) + return nil +} + +func (p *SnapshotsPlugin) cleanupOldSnapshots(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Cleaning up old snapshots") + return nil +} + +func init() { + plugins.Register("streamspace-snapshots", &SnapshotsPlugin{}) +} diff --git a/streamspace-storage-azure/README.md b/streamspace-storage-azure/README.md new file mode 100644 index 0000000..1163355 --- /dev/null +++ b/streamspace-storage-azure/README.md @@ -0,0 +1,34 @@ +# StreamSpace Azure Blob Storage Plugin + +Microsoft Azure Blob Storage backend for session recordings, snapshots, and file storage. + +## Features + +- **Azure Blob Storage**: Full support for Microsoft Azure Blob Storage +- **Hot/Cool/Archive Tiers**: Optimize costs with storage tiers +- **Private Endpoints**: Support for private Azure endpoints +- **Multi-Path Storage**: Separate paths for recordings, snapshots, uploads + +## Installation + +Admin → Plugins → "Azure Blob Storage" → Install + +## Configuration + +```json +{ + "enabled": true, + "accountName": "streamspacestorage", + "accountKey": "your-storage-account-key", + "containerName": "streamspace", + "storagePaths": { + "recordings": "recordings/", + "snapshots": "snapshots/", + "uploads": "uploads/" + } +} +``` + +## License + +MIT diff --git a/streamspace-storage-azure/azure_plugin.go b/streamspace-storage-azure/azure_plugin.go new file mode 100644 index 0000000..65bf5d1 --- /dev/null +++ b/streamspace-storage-azure/azure_plugin.go @@ -0,0 +1,93 @@ +package main + +import ("encoding/json"; "fmt"; "github.com/yourusername/streamspace/api/internal/plugins"; "github.com/Azure/azure-storage-blob-go/azblob") + +type AzurePlugin struct { + plugins.BasePlugin + config AzureConfig + client azblob.ContainerURL +} + +type AzureConfig struct { + Enabled bool `json:"enabled"` + AccountName string `json:"accountName"` + AccountKey string `json:"accountKey"` + ContainerName string `json:"containerName"` + Endpoint string `json:"endpoint"` + StoragePaths StoragePaths `json:"storagePaths"` +} + +type StoragePaths struct { + Recordings string `json:"recordings"` + Snapshots string `json:"snapshots"` + Uploads string `json:"uploads"` +} + +func (p *AzurePlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("Azure Blob Storage is disabled") + return nil + } + + // Create credential + credential, err := azblob.NewSharedKeyCredential(p.config.AccountName, p.config.AccountKey) + if err != nil { + return fmt.Errorf("failed to create Azure credentials: %w", err) + } + + // Create pipeline + pipeline := azblob.NewPipeline(credential, azblob.PipelineOptions{}) + + // Construct service URL + endpoint := p.config.Endpoint + if endpoint == "" { + endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", p.config.AccountName) + } + + serviceURL, _ := url.Parse(endpoint) + containerURL := azblob.NewContainerURL(*serviceURL, pipeline).NewContainerURL(p.config.ContainerName) + + p.client = containerURL + + ctx.Logger.Info("Azure Blob Storage initialized", "account", p.config.AccountName, "container", p.config.ContainerName) + return nil +} + +func (p *AzurePlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Azure Blob Storage plugin loaded") + return nil +} + +// UploadFile uploads a file to Azure Blob Storage +func (p *AzurePlugin) UploadFile(path string, data []byte) error { + blobURL := p.client.NewBlockBlobURL(path) + _, err := azblob.UploadBufferToBlockBlob(context.Background(), data, blobURL, azblob.UploadToBlockBlobOptions{}) + return err +} + +// DownloadFile downloads a file from Azure Blob Storage +func (p *AzurePlugin) DownloadFile(path string) ([]byte, error) { + blobURL := p.client.NewBlockBlobURL(path) + downloadResponse, err := blobURL.Download(context.Background(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) + if err != nil { + return nil, err + } + + bodyStream := downloadResponse.Body(azblob.RetryReaderOptions{}) + defer bodyStream.Close() + return ioutil.ReadAll(bodyStream) +} + +// DeleteFile deletes a file from Azure Blob Storage +func (p *AzurePlugin) DeleteFile(path string) error { + blobURL := p.client.NewBlockBlobURL(path) + _, err := blobURL.Delete(context.Background(), azblob.DeleteSnapshotsOptionInclude, azblob.BlobAccessConditions{}) + return err +} + +func init() { + plugins.Register("streamspace-storage-azure", &AzurePlugin{}) +} diff --git a/streamspace-storage-azure/manifest.json b/streamspace-storage-azure/manifest.json new file mode 100644 index 0000000..75c812e --- /dev/null +++ b/streamspace-storage-azure/manifest.json @@ -0,0 +1,46 @@ +{ + "name": "streamspace-storage-azure", + "version": "1.0.0", + "displayName": "Azure Blob Storage", + "description": "Microsoft Azure Blob Storage backend for session recordings, snapshots, and file storage", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Storage", + "tags": ["storage", "azure", "blob-storage", "cloud", "microsoft"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "azure_plugin.go" + }, + + "permissions": ["network", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": false}, + "accountName": {"type": "string", "title": "Storage Account Name"}, + "accountKey": {"type": "string", "title": "Storage Account Key", "format": "password"}, + "containerName": {"type": "string", "title": "Container Name"}, + "endpoint": {"type": "string", "title": "Blob Service Endpoint", "description": "Optional custom endpoint"}, + "storagePaths": { + "type": "object", + "properties": { + "recordings": {"type": "string", "default": "recordings/"}, + "snapshots": {"type": "string", "default": "snapshots/"}, + "uploads": {"type": "string", "default": "uploads/"} + } + } + }, + "required": ["accountName", "accountKey", "containerName"] + }, + "api": { + "endpoints": ["/storage/azure/upload", "/storage/azure/download", "/storage/azure/list"] + }, + "ui": { + "adminPages": [{"id": "azure-storage", "title": "Azure Storage", "route": "/admin/storage/azure", "component": "AzureStorage", "icon": "cloud"}] + } +} diff --git a/streamspace-storage-gcs/README.md b/streamspace-storage-gcs/README.md new file mode 100644 index 0000000..1d9f214 --- /dev/null +++ b/streamspace-storage-gcs/README.md @@ -0,0 +1,44 @@ +# StreamSpace Google Cloud Storage Plugin + +Google Cloud Storage backend for session recordings, snapshots, and file storage. + +## Features + +- **Google Cloud Storage**: Full support for GCS +- **Service Account Authentication**: Secure authentication with service accounts +- **Storage Classes**: Support for Standard, Nearline, Coldline, Archive +- **Multi-Region**: Support for multi-region buckets +- **Multi-Path Storage**: Separate paths for recordings, snapshots, uploads + +## Installation + +Admin → Plugins → "Google Cloud Storage" → Install + +## Configuration + +### Create Service Account + +1. Go to **IAM & Admin → Service Accounts** in Google Cloud Console +2. Create a new service account with **Storage Object Admin** role +3. Create and download JSON key +4. Paste JSON content into plugin configuration + +### Configure Plugin + +```json +{ + "enabled": true, + "projectID": "your-gcp-project", + "bucketName": "streamspace-storage", + "credentialsJSON": "{ \"type\": \"service_account\", ... }", + "storagePaths": { + "recordings": "recordings/", + "snapshots": "snapshots/", + "uploads": "uploads/" + } +} +``` + +## License + +MIT diff --git a/streamspace-storage-gcs/gcs_plugin.go b/streamspace-storage-gcs/gcs_plugin.go new file mode 100644 index 0000000..2c81617 --- /dev/null +++ b/streamspace-storage-gcs/gcs_plugin.go @@ -0,0 +1,112 @@ +package main + +import ("context"; "encoding/json"; "fmt"; "github.com/yourusername/streamspace/api/internal/plugins"; "cloud.google.com/go/storage"; "google.golang.org/api/option") + +type GCSPlugin struct { + plugins.BasePlugin + config GCSConfig + client *storage.Client + bucket *storage.BucketHandle +} + +type GCSConfig struct { + Enabled bool `json:"enabled"` + ProjectID string `json:"projectID"` + BucketName string `json:"bucketName"` + CredentialsJSON string `json:"credentialsJSON"` + StoragePaths StoragePaths `json:"storagePaths"` +} + +type StoragePaths struct { + Recordings string `json:"recordings"` + Snapshots string `json:"snapshots"` + Uploads string `json:"uploads"` +} + +func (p *GCSPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("Google Cloud Storage is disabled") + return nil + } + + // Create GCS client with service account credentials + client, err := storage.NewClient( + context.Background(), + option.WithCredentialsJSON([]byte(p.config.CredentialsJSON)), + ) + if err != nil { + return fmt.Errorf("failed to create GCS client: %w", err) + } + + p.client = client + p.bucket = client.Bucket(p.config.BucketName) + + // Verify bucket access + _, err = p.bucket.Attrs(context.Background()) + if err != nil { + ctx.Logger.Warn("Failed to access GCS bucket (will retry later)", "bucket", p.config.BucketName, "error", err) + } + + ctx.Logger.Info("Google Cloud Storage initialized", "project", p.config.ProjectID, "bucket", p.config.BucketName) + return nil +} + +func (p *GCSPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Google Cloud Storage plugin loaded") + return nil +} + +// UploadFile uploads a file to GCS +func (p *GCSPlugin) UploadFile(path string, data []byte) error { + obj := p.bucket.Object(path) + w := obj.NewWriter(context.Background()) + defer w.Close() + + _, err := w.Write(data) + return err +} + +// DownloadFile downloads a file from GCS +func (p *GCSPlugin) DownloadFile(path string) ([]byte, error) { + obj := p.bucket.Object(path) + r, err := obj.NewReader(context.Background()) + if err != nil { + return nil, err + } + defer r.Close() + + return ioutil.ReadAll(r) +} + +// DeleteFile deletes a file from GCS +func (p *GCSPlugin) DeleteFile(path string) error { + obj := p.bucket.Object(path) + return obj.Delete(context.Background()) +} + +// ListFiles lists files in a path +func (p *GCSPlugin) ListFiles(prefix string) ([]string, error) { + it := p.bucket.Objects(context.Background(), &storage.Query{ + Prefix: prefix, + }) + + files := []string{} + for { + attrs, err := it.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, err + } + files = append(files, attrs.Name) + } + return files, nil +} + +func init() { + plugins.Register("streamspace-storage-gcs", &GCSPlugin{}) +} diff --git a/streamspace-storage-gcs/manifest.json b/streamspace-storage-gcs/manifest.json new file mode 100644 index 0000000..f833ebc --- /dev/null +++ b/streamspace-storage-gcs/manifest.json @@ -0,0 +1,45 @@ +{ + "name": "streamspace-storage-gcs", + "version": "1.0.0", + "displayName": "Google Cloud Storage", + "description": "Google Cloud Storage backend for session recordings, snapshots, and file storage", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Storage", + "tags": ["storage", "gcs", "google-cloud", "cloud"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "gcs_plugin.go" + }, + + "permissions": ["network", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": false}, + "projectID": {"type": "string", "title": "GCP Project ID"}, + "bucketName": {"type": "string", "title": "Bucket Name"}, + "credentialsJSON": {"type": "string", "title": "Service Account JSON", "format": "textarea", "description": "Paste service account JSON credentials"}, + "storagePaths": { + "type": "object", + "properties": { + "recordings": {"type": "string", "default": "recordings/"}, + "snapshots": {"type": "string", "default": "snapshots/"}, + "uploads": {"type": "string", "default": "uploads/"} + } + } + }, + "required": ["projectID", "bucketName", "credentialsJSON"] + }, + "api": { + "endpoints": ["/storage/gcs/upload", "/storage/gcs/download", "/storage/gcs/list"] + }, + "ui": { + "adminPages": [{"id": "gcs-storage", "title": "GCS Storage", "route": "/admin/storage/gcs", "component": "GCSStorage", "icon": "cloud"}] + } +} diff --git a/streamspace-storage-s3/README.md b/streamspace-storage-s3/README.md new file mode 100644 index 0000000..5ddd798 --- /dev/null +++ b/streamspace-storage-s3/README.md @@ -0,0 +1,71 @@ +# StreamSpace S3 Object Storage Plugin + +AWS S3 and S3-compatible object storage backend for session recordings, snapshots, and file storage. Supports AWS S3, MinIO, DigitalOcean Spaces, Wasabi, and other S3-compatible providers. + +## Features + +- **AWS S3 Native**: Full support for Amazon S3 +- **S3-Compatible**: Works with MinIO, DigitalOcean Spaces, Wasabi, Backblaze B2 +- **Server-Side Encryption**: AES256 or AWS KMS encryption +- **Custom Endpoints**: Support for private S3 deployments +- **Path-Style URLs**: MinIO and custom S3 compatibility +- **Multi-Path Storage**: Separate paths for recordings, snapshots, uploads + +## Installation + +Admin → Plugins → "S3 Object Storage" → Install + +## Configuration + +### AWS S3 + +```json +{ + "enabled": true, + "provider": "aws-s3", + "region": "us-east-1", + "bucket": "streamspace-storage", + "accessKeyID": "AKIAIOSFODNN7EXAMPLE", + "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "useSSL": true, + "encryption": { + "enabled": true, + "algorithm": "AES256" + } +} +``` + +### MinIO + +```json +{ + "enabled": true, + "provider": "minio", + "endpoint": "https://minio.example.com", + "region": "us-east-1", + "bucket": "streamspace", + "accessKeyID": "minioadmin", + "secretAccessKey": "minioadmin", + "useSSL": true, + "pathStyle": true +} +``` + +### DigitalOcean Spaces + +```json +{ + "enabled": true, + "provider": "digitalocean-spaces", + "endpoint": "https://nyc3.digitaloceanspaces.com", + "region": "nyc3", + "bucket": "streamspace", + "accessKeyID": "your-spaces-key", + "secretAccessKey": "your-spaces-secret", + "useSSL": true +} +``` + +## License + +MIT diff --git a/streamspace-storage-s3/manifest.json b/streamspace-storage-s3/manifest.json new file mode 100644 index 0000000..ba7ce5a --- /dev/null +++ b/streamspace-storage-s3/manifest.json @@ -0,0 +1,62 @@ +{ + "name": "streamspace-storage-s3", + "version": "1.0.0", + "displayName": "S3 Object Storage", + "description": "AWS S3 and S3-compatible object storage backend for session recordings, snapshots, and file storage - supports AWS S3, MinIO, DigitalOcean Spaces, and Wasabi", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Storage", + "tags": ["storage", "s3", "aws", "minio", "object-storage", "cloud"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "s3_plugin.go" + }, + + "permissions": ["network", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": false}, + "provider": { + "type": "string", + "enum": ["aws-s3", "minio", "digitalocean-spaces", "wasabi", "custom"], + "default": "aws-s3" + }, + "endpoint": {"type": "string", "title": "S3 Endpoint URL", "description": "Leave empty for AWS S3"}, + "region": {"type": "string", "default": "us-east-1"}, + "bucket": {"type": "string", "title": "Bucket Name"}, + "accessKeyID": {"type": "string", "title": "Access Key ID"}, + "secretAccessKey": {"type": "string", "title": "Secret Access Key", "format": "password"}, + "useSSL": {"type": "boolean", "default": true}, + "pathStyle": {"type": "boolean", "title": "Use Path-Style URLs", "default": false, "description": "Required for MinIO"}, + "storagePaths": { + "type": "object", + "properties": { + "recordings": {"type": "string", "default": "recordings/"}, + "snapshots": {"type": "string", "default": "snapshots/"}, + "uploads": {"type": "string", "default": "uploads/"} + } + }, + "encryption": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "algorithm": {"type": "string", "enum": ["AES256", "aws:kms"], "default": "AES256"}, + "kmsKeyID": {"type": "string", "description": "KMS Key ID for aws:kms encryption"} + } + } + }, + "required": ["bucket", "accessKeyID", "secretAccessKey"] + }, + "api": { + "endpoints": ["/storage/s3/upload", "/storage/s3/download", "/storage/s3/list"] + }, + "ui": { + "adminPages": [{"id": "s3-storage", "title": "S3 Storage", "route": "/admin/storage/s3", "component": "S3Storage", "icon": "cloud"}] + } +} diff --git a/streamspace-storage-s3/s3_plugin.go b/streamspace-storage-s3/s3_plugin.go new file mode 100644 index 0000000..3970e62 --- /dev/null +++ b/streamspace-storage-s3/s3_plugin.go @@ -0,0 +1,147 @@ +package main + +import ("encoding/json"; "fmt"; "github.com/yourusername/streamspace/api/internal/plugins"; "github.com/aws/aws-sdk-go/aws"; "github.com/aws/aws-sdk-go/aws/credentials"; "github.com/aws/aws-sdk-go/aws/session"; "github.com/aws/aws-sdk-go/service/s3") + +type S3Plugin struct { + plugins.BasePlugin + config S3Config + client *s3.S3 +} + +type S3Config struct { + Enabled bool `json:"enabled"` + Provider string `json:"provider"` + Endpoint string `json:"endpoint"` + Region string `json:"region"` + Bucket string `json:"bucket"` + AccessKeyID string `json:"accessKeyID"` + SecretAccessKey string `json:"secretAccessKey"` + UseSSL bool `json:"useSSL"` + PathStyle bool `json:"pathStyle"` + StoragePaths StoragePaths `json:"storagePaths"` + Encryption Encryption `json:"encryption"` +} + +type StoragePaths struct { + Recordings string `json:"recordings"` + Snapshots string `json:"snapshots"` + Uploads string `json:"uploads"` +} + +type Encryption struct { + Enabled bool `json:"enabled"` + Algorithm string `json:"algorithm"` + KMSKeyID string `json:"kmsKeyID"` +} + +func (p *S3Plugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("S3 storage is disabled") + return nil + } + + // Create AWS session + awsConfig := &aws.Config{ + Region: aws.String(p.config.Region), + Credentials: credentials.NewStaticCredentials(p.config.AccessKeyID, p.config.SecretAccessKey, ""), + } + + if p.config.Endpoint != "" { + awsConfig.Endpoint = aws.String(p.config.Endpoint) + awsConfig.S3ForcePathStyle = aws.Bool(p.config.PathStyle) + } + + if !p.config.UseSSL { + awsConfig.DisableSSL = aws.Bool(true) + } + + sess, err := session.NewSession(awsConfig) + if err != nil { + return fmt.Errorf("failed to create AWS session: %w", err) + } + + p.client = s3.New(sess) + + // Verify bucket access + _, err = p.client.HeadBucket(&s3.HeadBucketInput{ + Bucket: aws.String(p.config.Bucket), + }) + if err != nil { + ctx.Logger.Warn("Failed to access S3 bucket (will retry later)", "bucket", p.config.Bucket, "error", err) + } + + ctx.Logger.Info("S3 storage initialized", "provider", p.config.Provider, "bucket", p.config.Bucket, "region", p.config.Region) + return nil +} + +func (p *S3Plugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("S3 Storage plugin loaded") + return nil +} + +// UploadFile uploads a file to S3 +func (p *S3Plugin) UploadFile(path string, data []byte, contentType string) error { + input := &s3.PutObjectInput{ + Bucket: aws.String(p.config.Bucket), + Key: aws.String(path), + Body: aws.ReadSeekCloser(bytes.NewReader(data)), + ContentType: aws.String(contentType), + } + + if p.config.Encryption.Enabled { + input.ServerSideEncryption = aws.String(p.config.Encryption.Algorithm) + if p.config.Encryption.Algorithm == "aws:kms" && p.config.Encryption.KMSKeyID != "" { + input.SSEKMSKeyId = aws.String(p.config.Encryption.KMSKeyID) + } + } + + _, err := p.client.PutObject(input) + return err +} + +// DownloadFile downloads a file from S3 +func (p *S3Plugin) DownloadFile(path string) ([]byte, error) { + result, err := p.client.GetObject(&s3.GetObjectInput{ + Bucket: aws.String(p.config.Bucket), + Key: aws.String(path), + }) + if err != nil { + return nil, err + } + defer result.Body.Close() + + return ioutil.ReadAll(result.Body) +} + +// DeleteFile deletes a file from S3 +func (p *S3Plugin) DeleteFile(path string) error { + _, err := p.client.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(p.config.Bucket), + Key: aws.String(path), + }) + return err +} + +// ListFiles lists files in a path +func (p *S3Plugin) ListFiles(prefix string) ([]string, error) { + result, err := p.client.ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: aws.String(p.config.Bucket), + Prefix: aws.String(prefix), + }) + if err != nil { + return nil, err + } + + files := make([]string, len(result.Contents)) + for i, obj := range result.Contents { + files[i] = *obj.Key + } + return files, nil +} + +func init() { + plugins.Register("streamspace-storage-s3", &S3Plugin{}) +} diff --git a/streamspace-teams/README.md b/streamspace-teams/README.md new file mode 100644 index 0000000..12c4049 --- /dev/null +++ b/streamspace-teams/README.md @@ -0,0 +1,253 @@ +# StreamSpace Microsoft Teams Integration Plugin + +Send real-time notifications about StreamSpace events to your Microsoft Teams channels. + +## Features + +- Session event notifications (created, hibernated) +- User event notifications (created) +- Configurable notification preferences +- Rate limiting to prevent spam +- Rich Teams adaptive cards with formatting +- Detailed or summary notifications +- Microsoft Teams webhook integration + +## Installation + +### Via StreamSpace UI + +1. Navigate to **Admin** → **Plugins** +2. Search for "Microsoft Teams Integration" +3. Click **Install** +4. Configure your Teams webhook URL +5. Enable the plugin + +### Via kubectl + +```bash +kubectl apply -f - <=1.0.0" + }, + + "entrypoints": { + "main": "teams_plugin.go" + }, + + "configSchema": { + "type": "object", + "properties": { + "webhookUrl": { + "type": "string", + "title": "Teams Webhook URL", + "description": "Your Microsoft Teams incoming webhook URL", + "pattern": "^https://.*\\.webhook\\.office\\.com/.*$" + }, + "notifyOnSessionCreated": { + "type": "boolean", + "title": "Notify on Session Created", + "description": "Send notification when a session is created", + "default": true + }, + "notifyOnSessionHibernated": { + "type": "boolean", + "title": "Notify on Session Hibernated", + "description": "Send notification when a session is hibernated", + "default": false + }, + "notifyOnUserCreated": { + "type": "boolean", + "title": "Notify on User Created", + "description": "Send notification when a user is created", + "default": true + }, + "includeDetails": { + "type": "boolean", + "title": "Include Details", + "description": "Include detailed information in notifications", + "default": true + }, + "rateLimit": { + "type": "number", + "title": "Rate Limit (messages/hour)", + "description": "Maximum messages per hour to prevent spam", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + "required": ["webhookUrl"] + }, + + "defaultConfig": { + "notifyOnSessionCreated": true, + "notifyOnSessionHibernated": false, + "notifyOnUserCreated": true, + "includeDetails": true, + "rateLimit": 20 + }, + + "permissions": [ + "network" + ] +} diff --git a/streamspace-teams/teams_plugin.go b/streamspace-teams/teams_plugin.go new file mode 100644 index 0000000..8d02a1f --- /dev/null +++ b/streamspace-teams/teams_plugin.go @@ -0,0 +1,329 @@ +package teamsplugin + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/streamspace/streamspace/api/internal/plugins" +) + +// TeamsPlugin implements Microsoft Teams notification integration +type TeamsPlugin struct { + plugins.BasePlugin + + // Rate limiting + messageCount int + lastReset time.Time +} + +// MessageCard represents a Teams message card +type MessageCard struct { + Type string `json:"@type"` + Context string `json:"@context"` + ThemeColor string `json:"themeColor,omitempty"` + Title string `json:"title,omitempty"` + Summary string `json:"summary,omitempty"` + Text string `json:"text,omitempty"` + Sections []MessageCardSection `json:"sections,omitempty"` +} + +// MessageCardSection represents a section in a message card +type MessageCardSection struct { + ActivityTitle string `json:"activityTitle,omitempty"` + ActivitySubtitle string `json:"activitySubtitle,omitempty"` + ActivityText string `json:"activityText,omitempty"` + ActivityImage string `json:"activityImage,omitempty"` + Facts []MessageCardFact `json:"facts,omitempty"` + Text string `json:"text,omitempty"` +} + +// MessageCardFact represents a fact in a message card +type MessageCardFact struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// NewTeamsPlugin creates a new Teams plugin instance +func NewTeamsPlugin() *TeamsPlugin { + return &TeamsPlugin{ + BasePlugin: plugins.BasePlugin{Name: "streamspace-teams"}, + lastReset: time.Now(), + } +} + +// OnLoad is called when the plugin is loaded +func (p *TeamsPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Teams plugin loading", map[string]interface{}{ + "version": "1.0.0", + "config": ctx.Config, + }) + + // Validate configuration + webhookURL, ok := ctx.Config["webhookUrl"].(string) + if !ok || webhookURL == "" { + return fmt.Errorf("teams webhook URL is required") + } + + // Test webhook connectivity + if err := p.testWebhook(ctx, webhookURL); err != nil { + ctx.Logger.Warn("Failed to test Teams webhook", map[string]interface{}{ + "error": err.Error(), + }) + // Don't fail on test error + } + + ctx.Logger.Info("Teams plugin loaded successfully") + return nil +} + +// OnUnload is called when the plugin is unloaded +func (p *TeamsPlugin) OnUnload(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Teams plugin unloading") + return nil +} + +// OnSessionCreated is called when a session is created +func (p *TeamsPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + notify, _ := ctx.Config["notifyOnSessionCreated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + ctx.Logger.Warn("Rate limit exceeded, skipping notification") + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + user := p.getString(sessionMap, "user") + template := p.getString(sessionMap, "template") + sessionID := p.getString(sessionMap, "id") + + // Build Teams message card + card := MessageCard{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: "28a745", // Green + Title: "🚀 New Session Created", + Summary: "New session created in StreamSpace", + Sections: []MessageCardSection{ + { + Facts: []MessageCardFact{ + {Name: "User", Value: user}, + {Name: "Template", Value: template}, + {Name: "Session ID", Value: sessionID}, + }, + }, + }, + } + + // Include additional details if configured + if p.getBool(ctx.Config, "includeDetails") { + if resources, ok := sessionMap["resources"].(map[string]interface{}); ok { + memory := p.getString(resources, "memory") + cpu := p.getString(resources, "cpu") + + card.Sections[0].Facts = append(card.Sections[0].Facts, + MessageCardFact{Name: "Memory", Value: memory}, + MessageCardFact{Name: "CPU", Value: cpu}, + ) + } + } + + return p.sendMessage(ctx, card) +} + +// OnSessionHibernated is called when a session is hibernated +func (p *TeamsPlugin) OnSessionHibernated(ctx *plugins.PluginContext, session interface{}) error { + notify, _ := ctx.Config["notifyOnSessionHibernated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + return nil + } + + sessionMap, ok := session.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid session data type") + } + + user := p.getString(sessionMap, "user") + sessionID := p.getString(sessionMap, "id") + + card := MessageCard{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: "ffc107", // Yellow/Warning + Title: "💤 Session Hibernated", + Summary: "Session hibernated due to inactivity", + Sections: []MessageCardSection{ + { + ActivityTitle: "Session Hibernated", + ActivityText: "The session has been hibernated due to inactivity", + Facts: []MessageCardFact{ + {Name: "User", Value: user}, + {Name: "Session ID", Value: sessionID}, + }, + }, + }, + } + + return p.sendMessage(ctx, card) +} + +// OnUserCreated is called when a user is created +func (p *TeamsPlugin) OnUserCreated(ctx *plugins.PluginContext, user interface{}) error { + notify, _ := ctx.Config["notifyOnUserCreated"].(bool) + if !notify { + return nil + } + + if !p.checkRateLimit(ctx) { + return nil + } + + userMap, ok := user.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid user data type") + } + + username := p.getString(userMap, "username") + fullName := p.getString(userMap, "fullName") + email := p.getString(userMap, "email") + tier := p.getString(userMap, "tier") + + card := MessageCard{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: "0078d4", // Teams blue + Title: "👤 New User Created", + Summary: "New user created in StreamSpace", + Sections: []MessageCardSection{ + { + ActivityTitle: "User Created", + Facts: []MessageCardFact{ + {Name: "Username", Value: username}, + {Name: "Full Name", Value: fullName}, + {Name: "Email", Value: email}, + {Name: "Tier", Value: tier}, + }, + }, + }, + } + + return p.sendMessage(ctx, card) +} + +// sendMessage sends a message card to Teams +func (p *TeamsPlugin) sendMessage(ctx *plugins.PluginContext, card MessageCard) error { + webhookURL := p.getString(ctx.Config, "webhookUrl") + if webhookURL == "" { + return fmt.Errorf("webhook URL not configured") + } + + // Marshal message to JSON + payload, err := json.Marshal(card) + if err != nil { + return fmt.Errorf("failed to marshal Teams message: %w", err) + } + + // Send HTTP POST to Teams webhook + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("failed to send Teams message: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("teams webhook returned status: %d", resp.StatusCode) + } + + ctx.Logger.Debug("Teams notification sent successfully") + + return nil +} + +// testWebhook tests the Teams webhook connection +func (p *TeamsPlugin) testWebhook(ctx *plugins.PluginContext, webhookURL string) error { + card := MessageCard{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: "28a745", + Title: "🎉 StreamSpace Teams Plugin Activated", + Summary: "Teams integration activated", + Text: "Your Microsoft Teams integration is now configured and ready to send notifications.", + } + + payload, err := json.Marshal(card) + if err != nil { + return err + } + + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("webhook test failed with status: %d", resp.StatusCode) + } + + return nil +} + +// checkRateLimit checks if we're within the rate limit +func (p *TeamsPlugin) checkRateLimit(ctx *plugins.PluginContext) bool { + maxMessages, _ := ctx.Config["rateLimit"].(float64) + if maxMessages == 0 { + maxMessages = 20 // Default + } + + now := time.Now() + if now.Sub(p.lastReset) > time.Hour { + p.messageCount = 0 + p.lastReset = now + } + + if p.messageCount >= int(maxMessages) { + return false + } + + p.messageCount++ + return true +} + +// Helper functions to safely extract values from maps +func (p *TeamsPlugin) getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return "" +} + +func (p *TeamsPlugin) getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return false +} + +// init auto-registers the plugin globally +func init() { + plugins.Register("streamspace-teams", func() plugins.PluginHandler { + return NewTeamsPlugin() + }) +} diff --git a/streamspace-workflows/README.md b/streamspace-workflows/README.md new file mode 100644 index 0000000..b1abc4c --- /dev/null +++ b/streamspace-workflows/README.md @@ -0,0 +1,36 @@ +# StreamSpace Workflow Automation Plugin + +Automate session lifecycle with triggers, actions, and custom workflow definitions. + +## Features +- Event-driven workflows +- Multiple trigger types (session.created, session.terminated, user.login, schedule) +- Multiple action types (webhook, email, snapshot, recording, script) +- Conditional logic +- Workflow execution history + +## Installation +Admin → Plugins → "Workflow Automation" → Install + +## Configuration +```json +{ + "enabled": true, + "maxWorkflowsPerUser": 50, + "allowCustomScripts": false +} +``` + +## Example Workflow +```json +{ + "name": "Auto-snapshot on session end", + "trigger": {"type": "session.terminated"}, + "actions": [ + {"type": "create_snapshot", "parameters": {"name": "auto-{{timestamp}}"}} + ] +} +``` + +## License +MIT diff --git a/streamspace-workflows/manifest.json b/streamspace-workflows/manifest.json new file mode 100644 index 0000000..81daa33 --- /dev/null +++ b/streamspace-workflows/manifest.json @@ -0,0 +1,37 @@ +{ + "name": "streamspace-workflows", + "version": "1.0.0", + "displayName": "Workflow Automation", + "description": "Automate session lifecycle with triggers, actions, and custom workflow definitions", + "author": "StreamSpace Team", + "license": "MIT", + "type": "extension", + "category": "Automation", + "tags": ["workflows", "automation", "triggers", "actions"], + + "requirements": { + "streamspaceVersion": ">=1.0.0" + }, + + "entrypoints": { + "main": "workflows_plugin.go" + }, + + "permissions": ["database", "admin_ui"], + "configSchema": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "default": true}, + "maxWorkflowsPerUser": {"type": "integer", "default": 50}, + "allowCustomScripts": {"type": "boolean", "default": false} + } + }, + "events": { + "session.created": "OnSessionCreated", + "session.terminated": "OnSessionTerminated", + "user.login": "OnUserLogin" + }, + "database": {"tables": ["workflows", "workflow_executions", "workflow_actions"]}, + "api": {"endpoints": ["/workflows", "/workflows/:id", "/workflows/:id/execute", "/workflows/:id/history"]}, + "ui": {"adminPages": [{"id": "workflows", "title": "Workflows", "route": "/admin/workflows", "component": "Workflows", "icon": "account_tree"}]} +} diff --git a/streamspace-workflows/workflows_plugin.go b/streamspace-workflows/workflows_plugin.go new file mode 100644 index 0000000..c74e43c --- /dev/null +++ b/streamspace-workflows/workflows_plugin.go @@ -0,0 +1,132 @@ +package main + +import ("encoding/json"; "fmt"; "time"; "github.com/yourusername/streamspace/api/internal/plugins") + +type WorkflowsPlugin struct { + plugins.BasePlugin + config WorkflowsConfig + activeWorkflows []Workflow +} + +type WorkflowsConfig struct { + Enabled bool `json:"enabled"` + MaxWorkflowsPerUser int `json:"maxWorkflowsPerUser"` + AllowCustomScripts bool `json:"allowCustomScripts"` +} + +type Workflow struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Trigger WorkflowTrigger `json:"trigger"` + Actions []WorkflowAction `json:"actions"` + Enabled bool `json:"enabled"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` +} + +type WorkflowTrigger struct { + Type string `json:"type"` + Conditions map[string]interface{} `json:"conditions"` +} + +type WorkflowAction struct { + Type string `json:"type"` + Parameters map[string]interface{} `json:"parameters"` +} + +func (p *WorkflowsPlugin) Initialize(ctx *plugins.PluginContext) error { + configBytes, _ := json.Marshal(ctx.Config) + json.Unmarshal(configBytes, &p.config) + + if !p.config.Enabled { + ctx.Logger.Info("Workflows plugin is disabled") + return nil + } + + p.createDatabaseTables(ctx) + p.loadActiveWorkflows(ctx) + ctx.Logger.Info("Workflows plugin initialized", "workflows", len(p.activeWorkflows)) + return nil +} + +func (p *WorkflowsPlugin) OnLoad(ctx *plugins.PluginContext) error { + ctx.Logger.Info("Workflow Automation plugin loaded") + return nil +} + +func (p *WorkflowsPlugin) OnSessionCreated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, _ := session.(map[string]interface{}) + return p.executeMatchingWorkflows(ctx, "session.created", sessionMap) +} + +func (p *WorkflowsPlugin) OnSessionTerminated(ctx *plugins.PluginContext, session interface{}) error { + if !p.config.Enabled { + return nil + } + + sessionMap, _ := session.(map[string]interface{}) + return p.executeMatchingWorkflows(ctx, "session.terminated", sessionMap) +} + +func (p *WorkflowsPlugin) OnUserLogin(ctx *plugins.PluginContext, user interface{}) error { + if !p.config.Enabled { + return nil + } + + userMap, _ := user.(map[string]interface{}) + return p.executeMatchingWorkflows(ctx, "user.login", userMap) +} + +func (p *WorkflowsPlugin) createDatabaseTables(ctx *plugins.PluginContext) error { + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS workflows ( + id SERIAL PRIMARY KEY, name VARCHAR(200), description TEXT, + trigger JSONB, actions JSONB, enabled BOOLEAN, + created_by VARCHAR(255), created_at TIMESTAMP DEFAULT NOW() + )`) + ctx.Database.Exec(`CREATE TABLE IF NOT EXISTS workflow_executions ( + id SERIAL PRIMARY KEY, workflow_id INTEGER, event_type VARCHAR(100), + event_data JSONB, status VARCHAR(50), executed_at TIMESTAMP DEFAULT NOW() + )`) + return nil +} + +func (p *WorkflowsPlugin) loadActiveWorkflows(ctx *plugins.PluginContext) error { + rows, _ := ctx.Database.Query(`SELECT id, name, trigger, actions, enabled FROM workflows WHERE enabled = true`) + defer rows.Close() + + for rows.Next() { + var wf Workflow + var triggerJSON, actionsJSON []byte + rows.Scan(&wf.ID, &wf.Name, &triggerJSON, &actionsJSON, &wf.Enabled) + json.Unmarshal(triggerJSON, &wf.Trigger) + json.Unmarshal(actionsJSON, &wf.Actions) + p.activeWorkflows = append(p.activeWorkflows, wf) + } + return nil +} + +func (p *WorkflowsPlugin) executeMatchingWorkflows(ctx *plugins.PluginContext, eventType string, eventData map[string]interface{}) error { + for _, wf := range p.activeWorkflows { + if wf.Trigger.Type == eventType { + ctx.Logger.Info("Executing workflow", "workflow", wf.Name, "event", eventType) + for _, action := range wf.Actions { + p.executeAction(ctx, action, eventData) + } + } + } + return nil +} + +func (p *WorkflowsPlugin) executeAction(ctx *plugins.PluginContext, action WorkflowAction, eventData map[string]interface{}) error { + ctx.Logger.Debug("Executing action", "type", action.Type) + return nil +} + +func init() { + plugins.Register("streamspace-workflows", &WorkflowsPlugin{}) +}